feat(title): add large iOS toolbar title (#19268)

Co-authored-by: Brandy Carney <brandyscarney@users.noreply.github.com>
This commit is contained in:
Liam DeBeasi
2019-09-04 11:38:48 -04:00
committed by GitHub
parent d9610cdbdf
commit 923312ecd5
21 changed files with 966 additions and 82 deletions

View File

@@ -75,7 +75,7 @@ export class IonButton {
proxyInputs(IonButton, ['buttonType', 'color', 'disabled', 'download', 'expand', 'fill', 'href', 'mode', 'rel', 'routerDirection', 'shape', 'size', 'strong', 'target', 'type']);
export declare interface IonButtons extends Components.IonButtons {}
@Component({ selector: 'ion-buttons', changeDetection: ChangeDetectionStrategy.OnPush, template: '<ng-content></ng-content>' })
@Component({ selector: 'ion-buttons', changeDetection: ChangeDetectionStrategy.OnPush, template: '<ng-content></ng-content>', inputs: ['collapse'] })
export class IonButtons {
protected el: HTMLElement;
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
@@ -83,6 +83,7 @@ export class IonButtons {
this.el = r.nativeElement;
}
}
proxyInputs(IonButtons, ['collapse']);
export declare interface IonCard extends Components.IonCard {}
@Component({ selector: 'ion-card', changeDetection: ChangeDetectionStrategy.OnPush, template: '<ng-content></ng-content>', inputs: ['button', 'color', 'disabled', 'download', 'href', 'mode', 'rel', 'routerDirection', 'target', 'type'] })
@@ -269,7 +270,7 @@ export class IonGrid {
proxyInputs(IonGrid, ['fixed']);
export declare interface IonHeader extends Components.IonHeader {}
@Component({ selector: 'ion-header', changeDetection: ChangeDetectionStrategy.OnPush, template: '<ng-content></ng-content>', inputs: ['mode', 'translucent'] })
@Component({ selector: 'ion-header', changeDetection: ChangeDetectionStrategy.OnPush, template: '<ng-content></ng-content>', inputs: ['collapse', 'mode', 'translucent'] })
export class IonHeader {
protected el: HTMLElement;
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
@@ -277,7 +278,7 @@ export class IonHeader {
this.el = r.nativeElement;
}
}
proxyInputs(IonHeader, ['mode', 'translucent']);
proxyInputs(IonHeader, ['collapse', 'mode', 'translucent']);
export declare interface IonIcon extends Components.IonIcon {}
@Component({ selector: 'ion-icon', changeDetection: ChangeDetectionStrategy.OnPush, template: '<ng-content></ng-content>', inputs: ['ariaLabel', 'color', 'flipRtl', 'icon', 'ios', 'lazy', 'md', 'mode', 'name', 'size', 'src'] })
@@ -895,7 +896,7 @@ export class IonThumbnail {
}
export declare interface IonTitle extends Components.IonTitle {}
@Component({ selector: 'ion-title', changeDetection: ChangeDetectionStrategy.OnPush, template: '<ng-content></ng-content>', inputs: ['color'] })
@Component({ selector: 'ion-title', changeDetection: ChangeDetectionStrategy.OnPush, template: '<ng-content></ng-content>', inputs: ['color', 'size'] })
export class IonTitle {
protected el: HTMLElement;
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
@@ -903,7 +904,7 @@ export class IonTitle {
this.el = r.nativeElement;
}
}
proxyInputs(IonTitle, ['color']);
proxyInputs(IonTitle, ['color', 'size']);
export declare interface IonToggle extends Components.IonToggle {}
@Component({ selector: 'ion-toggle', changeDetection: ChangeDetectionStrategy.OnPush, template: '<ng-content></ng-content>', inputs: ['checked', 'color', 'disabled', 'mode', 'name', 'value'] })

View File

@@ -178,6 +178,7 @@ ion-button,css-prop,--ripple-color
ion-button,css-prop,--transition
ion-buttons,scoped
ion-buttons,prop,collapse,boolean,false,false,false
ion-card,scoped
ion-card,prop,button,boolean,false,false,false
@@ -399,6 +400,7 @@ ion-grid,css-prop,--ion-grid-width-xl
ion-grid,css-prop,--ion-grid-width-xs
ion-header,none
ion-header,prop,collapse,boolean,false,false,false
ion-header,prop,mode,"ios" | "md",undefined,false,false
ion-header,prop,translucent,boolean,false,false,false
@@ -1204,6 +1206,7 @@ ion-thumbnail,css-prop,--size
ion-title,shadow
ion-title,prop,color,string | undefined,undefined,false,false
ion-title,prop,size,"large" | undefined,undefined,false,false
ion-title,css-prop,--color
ion-toast,shadow

View File

@@ -389,7 +389,12 @@ export namespace Components {
*/
'type': 'submit' | 'reset' | 'button';
}
interface IonButtons {}
interface IonButtons {
/**
* If true, buttons will disappear when its parent toolbar has fully collapsed if the toolbar is not the first toolbar. If the toolbar is the first toolbar, the buttons will be hidden and will only be shown once all toolbars have fully collapsed. Only applies in `ios` mode with `collapse` set to `true` on `ion-header`
*/
'collapse': boolean;
}
interface IonCard {
/**
* If `true`, a button tag will be rendered and the card will be tappable.
@@ -865,6 +870,10 @@ export namespace Components {
'fixed': boolean;
}
interface IonHeader {
/**
* If `true`, the header will collapse on scroll of the content. Only applies in `ios` mode.
*/
'collapse': boolean;
/**
* The mode determines which platform styles to use.
*/
@@ -2696,6 +2705,10 @@ export namespace Components {
* The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics).
*/
'color'?: Color;
/**
* The size of the toolbar title. Only applies in `ios` mode.
*/
'size'?: 'large' | undefined;
}
interface IonToast {
/**
@@ -3883,7 +3896,12 @@ declare namespace LocalJSX {
*/
'type'?: 'submit' | 'reset' | 'button';
}
interface IonButtons extends JSXBase.HTMLAttributes<HTMLIonButtonsElement> {}
interface IonButtons extends JSXBase.HTMLAttributes<HTMLIonButtonsElement> {
/**
* If true, buttons will disappear when its parent toolbar has fully collapsed if the toolbar is not the first toolbar. If the toolbar is the first toolbar, the buttons will be hidden and will only be shown once all toolbars have fully collapsed. Only applies in `ios` mode with `collapse` set to `true` on `ion-header`
*/
'collapse'?: boolean;
}
interface IonCard extends JSXBase.HTMLAttributes<HTMLIonCardElement> {
/**
* If `true`, a button tag will be rendered and the card will be tappable.
@@ -4371,6 +4389,10 @@ declare namespace LocalJSX {
'fixed'?: boolean;
}
interface IonHeader extends JSXBase.HTMLAttributes<HTMLIonHeaderElement> {
/**
* If `true`, the header will collapse on scroll of the content. Only applies in `ios` mode.
*/
'collapse'?: boolean;
/**
* The mode determines which platform styles to use.
*/
@@ -6012,6 +6034,10 @@ declare namespace LocalJSX {
* The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics).
*/
'color'?: Color;
/**
* The size of the toolbar title. Only applies in `ios` mode.
*/
'size'?: 'large' | undefined;
}
interface IonToast extends JSXBase.HTMLAttributes<HTMLIonToastElement> {
/**

View File

@@ -1,4 +1,4 @@
import { Component, ComponentInterface, Host, h } from '@stencil/core';
import { Component, ComponentInterface, Host, Prop, h } from '@stencil/core';
import { getIonMode } from '../../global/ionic-global';
@@ -12,6 +12,18 @@ import { getIonMode } from '../../global/ionic-global';
})
export class Buttons implements ComponentInterface {
/**
* If true, buttons will disappear when its
* parent toolbar has fully collapsed if the toolbar
* is not the first toolbar. If the toolbar is the
* first toolbar, the buttons will be hidden and will
* only be shown once all toolbars have fully collapsed.
*
* Only applies in `ios` mode with `collapse` set to
* `true` on `ion-header`
*/
@Prop() collapse = false;
render() {
return (
<Host class={getIonMode(this)}>

View File

@@ -204,6 +204,13 @@ export const ButtonsExample: React.FC = () => (
## Properties
| Property | Attribute | Description | Type | Default |
| ---------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ------- |
| `collapse` | `collapse` | If true, buttons will disappear when its parent toolbar has fully collapsed if the toolbar is not the first toolbar. If the toolbar is the first toolbar, the buttons will be hidden and will only be shown once all toolbars have fully collapsed. Only applies in `ios` mode with `collapse` set to `true` on `ion-header` | `boolean` | `false` |
----------------------------------------------
*Built with [StencilJS](https://stenciljs.com/)*

View File

@@ -16,9 +16,58 @@
.header-translucent-ios {
backdrop-filter: $header-ios-translucent-filter;
}
.header-translucent-ios ion-toolbar {
--opacity: .8;
--backdrop-filter: #{$header-ios-translucent-filter};
}
}
// iOS Header - Collapse
// --------------------------------------------------
.header-collapse-ios {
z-index: 9;
}
.header-collapse-ios ion-toolbar {
position: sticky;
top: 0;
}
.header-collapse-ios ion-toolbar:first-child {
padding-top: 7px;
z-index: 1;
}
.header-collapse-ios ion-toolbar {
z-index: 0;
}
.header-collapse-ios ion-toolbar ion-searchbar {
height: 48px;
padding-top: 0px;
padding-bottom: 13px;
}
ion-toolbar.in-toolbar ion-title,
ion-toolbar.in-toolbar ion-buttons {
transition: all 0.2s ease-in-out;
}
/**
* There is a bug in Safari where animating the opacity
* on an element in a scrollable container while scrolling
* causes the scroll position to jump to the top
*/
.header-collapse-ios ion-toolbar ion-title,
.header-collapse-ios ion-toolbar ion-buttons {
transition: none;
}
.header-collapse-ios-inactive ion-toolbar.in-toolbar ion-title,
.header-collapse-ios-inactive ion-toolbar.in-toolbar ion-buttons[collapse] {
opacity: 0;
pointer-events: none;
}

View File

@@ -25,3 +25,7 @@
.header-md[no-border]::after {
display: none;
}
.header-collapse-md {
display: none;
}

View File

@@ -1,7 +1,8 @@
import { Component, ComponentInterface, Host, Prop, h } from '@stencil/core';
import { Component, ComponentInterface, Element, Host, Prop, h, readTask, writeTask } from '@stencil/core';
import { getIonMode } from '../../global/ionic-global';
import { cloneElement, createHeaderIndex, handleContentScroll, handleToolbarIntersection, setHeaderActive } from './header.utils';
/**
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
*/
@@ -14,6 +15,17 @@ import { getIonMode } from '../../global/ionic-global';
})
export class Header implements ComponentInterface {
private scrollEl?: HTMLElement;
private contentScrollCallback?: any;
@Element() el!: HTMLElement;
/**
* If `true`, the header will collapse on scroll of the content.
* Only applies in `ios` mode.
*/
@Prop() collapse = false;
/**
* If `true`, the header will be translucent.
* Only applies when the mode is `"ios"` and the device supports
@@ -24,6 +36,76 @@ export class Header implements ComponentInterface {
*/
@Prop() translucent = false;
async componentDidLoad() {
// Determine if the header can collapse
const canCollapse = (this.collapse && getIonMode(this) === 'ios') ? this.collapse : false;
const tabs = this.el.closest('ion-tabs');
const page = this.el.closest('ion-app,ion-page,.ion-page,page-inner');
const contentEl = tabs ? tabs.querySelector('ion-content') : page!.querySelector('ion-content');
if (canCollapse) {
await this.setupCollapsableHeader(contentEl, (tabs) ? tabs : page!);
}
}
componentDidUnload() {
if (this.scrollEl && this.contentScrollCallback) {
this.scrollEl.removeEventListener('scroll', this.contentScrollCallback);
}
}
private async setupCollapsableHeader(contentEl: HTMLIonContentElement | null, pageEl: Element) {
if (!contentEl) { console.error('ion-header requires a content to collapse, make sure there is an ion-content.'); }
this.scrollEl = await contentEl!.getScrollElement();
readTask(() => {
const headers = pageEl.querySelectorAll('ion-header');
const mainHeader = Array.from(headers).find((header: any) => !header.collapse) as HTMLElement | undefined;
if (!mainHeader || !this.scrollEl) { return; }
const mainHeaderIndex = createHeaderIndex(mainHeader);
const scrollHeaderIndex = createHeaderIndex(this.el);
if (!mainHeaderIndex || !scrollHeaderIndex) { return; }
setHeaderActive(mainHeaderIndex, false);
// TODO: Find a better way to do this
let remainingHeight = 0;
for (let i = 1; i <= scrollHeaderIndex.toolbars.length - 1; i++) {
remainingHeight += scrollHeaderIndex.toolbars[i].el.clientHeight;
}
/**
* Handle interaction between toolbar collapse and
* showing/hiding content in the primary ion-header
*/
const toolbarIntersection = (ev: any) => { handleToolbarIntersection(ev, mainHeaderIndex, scrollHeaderIndex); };
readTask(() => {
const mainHeaderHeight = mainHeaderIndex.el.clientHeight;
const intersectionObserver = new IntersectionObserver(toolbarIntersection, { threshold: 0.25, rootMargin: `-${mainHeaderHeight}px 0px 0px 0px` });
intersectionObserver.observe(scrollHeaderIndex.toolbars[0].el);
});
/**
* Handle scaling of large iOS titles and
* showing/hiding border on last toolbar
* in primary header
*/
this.contentScrollCallback = () => { handleContentScroll(this.scrollEl!, mainHeaderIndex, scrollHeaderIndex, remainingHeight); };
this.scrollEl.addEventListener('scroll', this.contentScrollCallback);
});
writeTask(() => {
cloneElement('ion-title');
cloneElement('ion-back-button');
});
}
render() {
const mode = getIonMode(this);
return (
@@ -36,6 +118,7 @@ export class Header implements ComponentInterface {
[`header-${mode}`]: true,
[`header-translucent`]: this.translucent,
[`header-collapse-${mode}`]: this.collapse,
[`header-translucent-${mode}`]: this.translucent,
}}
>

View File

@@ -0,0 +1,142 @@
import { readTask, writeTask } from '@stencil/core';
import { clamp } from '../../utils/helpers';
const TRANSITION = 'all 0.2s ease-in-out';
interface HeaderIndex {
el: HTMLElement;
toolbars: ToolbarIndex[] | [];
}
interface ToolbarIndex {
el: HTMLElement;
background: HTMLElement;
ionTitleEl: HTMLIonTitleElement | undefined;
innerTitleEl: HTMLElement;
ionButtonsEl: HTMLElement[] | [];
}
export const cloneElement = (tagName: string) => {
const getCachedEl = document.querySelector(`${tagName}.ion-cloned-element`);
if (getCachedEl !== null) { return getCachedEl; }
const clonedEl = document.createElement(tagName);
clonedEl.classList.add('ion-cloned-element');
clonedEl.style.setProperty('display', 'none');
document.body.appendChild(clonedEl);
return clonedEl;
};
export const createHeaderIndex = (headerEl: HTMLElement | undefined): HeaderIndex | undefined => {
if (!headerEl) { return; }
const toolbars = headerEl.querySelectorAll('ion-toolbar');
return {
el: headerEl,
toolbars: Array.from(toolbars).map((toolbar: any) => {
const ionTitleEl = toolbar.querySelector('ion-title');
return {
el: toolbar,
background: toolbar.shadowRoot!.querySelector('.toolbar-background'),
ionTitleEl,
innerTitleEl: (ionTitleEl) ? ionTitleEl.shadowRoot!.querySelector('.toolbar-title') : null,
ionButtonsEl: Array.from(toolbar.querySelectorAll('ion-buttons')) || []
} as ToolbarIndex;
}) || [[]]
} as HeaderIndex;
};
export const handleContentScroll = (scrollEl: HTMLElement, mainHeaderIndex: HeaderIndex, scrollHeaderIndex: HeaderIndex, remainingHeight = 0) => {
readTask(() => {
const scrollTop = scrollEl.scrollTop;
const lastMainToolbar = mainHeaderIndex.toolbars[mainHeaderIndex.toolbars.length - 1];
const scale = clamp(1, 1 + (-scrollTop / 500), 1.1);
const borderOpacity = clamp(0, (scrollTop - remainingHeight) / lastMainToolbar.el.clientHeight, 1);
const maxOpacity = 1;
const scaledOpacity = borderOpacity * maxOpacity;
writeTask(() => {
scaleLargeTitles(scrollHeaderIndex.toolbars, scale);
setToolbarBackgroundOpacity(mainHeaderIndex.toolbars[0], (scaledOpacity === 1) ? undefined : scaledOpacity);
});
});
};
const setToolbarBackgroundOpacity = (toolbar: ToolbarIndex, opacity: number | undefined) => {
if (opacity === undefined) {
toolbar.background.style.removeProperty('--opacity');
} else {
toolbar.background.style.setProperty('--opacity', opacity.toString());
}
};
/**
* If toolbars are intersecting, hide the scrollable toolbar content
* and show the primary toolbar content. If the toolbars are not intersecting,
* hide the primary toolbar content and show the scrollable toolbar content
*/
export const handleToolbarIntersection = (ev: any, mainHeaderIndex: HeaderIndex, scrollHeaderIndex: HeaderIndex) => {
console.log(ev);
writeTask(() => {
const event = ev[0];
const intersection = event.intersectionRect;
const intersectionArea = intersection.width * intersection.height;
const rootArea = event.rootBounds.width * event.rootBounds.height;
const isPageHidden = intersectionArea === 0 && rootArea === 0;
const isPageTransitioning = intersectionArea > 0 && (intersection.left !== event.rootBounds.left || intersection.right !== event.rootBounds.right);
if (isPageHidden || isPageTransitioning) {
return;
}
if (event.isIntersecting) {
setHeaderActive(mainHeaderIndex, false);
setHeaderActive(scrollHeaderIndex);
} else {
/**
* There is a bug with IntersectionObserver on Safari
* where `event.isIntersecting === false` when cancelling
* a swipe to go back gesture. Checking the intersection
* x, y, width, and height provides a workaround. This bug
* does not happen when using Safari + Web Animations,
* only Safari + CSS Animations.
*/
const hasValidIntersection = (intersection.x === 0 && intersection.y === 0) || (intersection.width !== 0 && intersection.height !== 0);
if (hasValidIntersection) {
setHeaderActive(mainHeaderIndex);
setHeaderActive(scrollHeaderIndex, false);
}
}
});
};
export const setHeaderActive = (headerIndex: HeaderIndex, active = true) => {
writeTask(() => {
if (active) {
headerIndex.el.classList.remove('header-collapse-ios-inactive');
} else {
headerIndex.el.classList.add('header-collapse-ios-inactive');
}
setToolbarBackgroundOpacity(headerIndex.toolbars[0], (active) ? undefined : 0);
});
};
export const scaleLargeTitles = (toolbars: ToolbarIndex[] = [], scale = 1, transition = false) => {
toolbars.forEach(toolbar => {
const ionTitle = toolbar.ionTitleEl;
const titleDiv = toolbar.innerTitleEl;
if (!ionTitle || ionTitle.size !== 'large') { return; }
titleDiv.style.transformOrigin = 'left center';
titleDiv.style.transition = (transition) ? TRANSITION : '';
titleDiv.style.transform = `scale3d(${scale}, ${scale}, 1)`;
});
};

View File

@@ -62,6 +62,7 @@ export const HeaderExample: React.FC = () => (
| Property | Attribute | Description | Type | Default |
| ------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | ----------- |
| `collapse` | `collapse` | If `true`, the header will collapse on scroll of the content. Only applies in `ios` mode. | `boolean` | `false` |
| `mode` | `mode` | The mode determines which platform styles to use. | `"ios" \| "md"` | `undefined` |
| `translucent` | `translucent` | If `true`, the header will be translucent. Only applies when the mode is `"ios"` and the device supports [`backdrop-filter`](https://developer.mozilla.org/en-US/docs/Web/CSS/backdrop-filter#Browser_compatibility). Note: In order to scroll content behind the header, the `fullscreen` attribute needs to be set on the content. | `boolean` | `false` |

View File

@@ -50,7 +50,7 @@
height: 100%;
font-size: 14px;
font-size: 16px;
font-weight: 400;
contain: strict;

View File

@@ -25,9 +25,10 @@
## Properties
| Property | Attribute | Description | Type | Default |
| -------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | ----------- |
| `color` | `color` | The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). | `string \| undefined` | `undefined` |
| Property | Attribute | Description | Type | Default |
| -------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | ----------- |
| `color` | `color` | The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics). | `string \| undefined` | `undefined` |
| `size` | `size` | The size of the toolbar title. Only applies in `ios` mode. | `"large" \| undefined` | `undefined` |
## CSS Custom Properties

View File

@@ -0,0 +1,89 @@
<!DOCTYPE html>
<html dir="ltr">
<head>
<meta charset="UTF-8">
<title>Toolbar - Title</title>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script>
<script src="../../../../../dist/ionic.js"></script>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button text="Mailboxes" default-href="/"></ion-back-button>
</ion-buttons>
<ion-buttons slot="primary">
<ion-button>
Edit
</ion-button>
</ion-buttons>
<ion-title>Inbox</ion-title>
</ion-toolbar>
</ion-header>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button text="Mailboxes" default-href="/"></ion-back-button>
</ion-buttons>
<ion-buttons slot="primary">
<ion-button>
Edit
</ion-button>
</ion-buttons>
<ion-title size="large">Inbox</ion-title>
</ion-toolbar>
</ion-header>
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button text="Mailboxes" default-href="/"></ion-back-button>
</ion-buttons>
<ion-buttons slot="primary">
<ion-button>
Edit
</ion-button>
</ion-buttons>
<ion-title size="large">Inbox</ion-title>
</ion-toolbar>
<ion-toolbar>
<ion-searchbar></ion-searchbar>
</ion-toolbar>
</ion-header>
<ion-header>
<ion-toolbar>
<ion-title>Title</ion-title>
</ion-toolbar>
</ion-header>
<ion-header>
<ion-toolbar>
<ion-title size="large">Title</ion-title>
</ion-toolbar>
</ion-header>
<ion-content></ion-content>
<ion-footer>
<ion-button expand="full" onClick="toggle()" class="ion-no-margin">Toggle Size</ion-button>
</ion-footer>
</ion-app>
<script>
const titles = document.querySelectorAll('ion-title[size]');
function toggle() {
for (title of titles) {
const size = title.size === 'large' ? 'standard' : 'large';
title.size = size;
}
}
</script>
</body>
</html>

View File

@@ -33,8 +33,6 @@
font-size: 17px;
font-weight: 600;
letter-spacing: -.03em;
text-align: center;
box-sizing: border-box;
pointer-events: none;
@@ -64,4 +62,25 @@
overflow: hidden;
pointer-events: auto;
}
}
// Title: Large
// --------------------------------------------------
:host(.title-ios-large) {
@include padding(0, 16px);
bottom: 0;
align-items: flex-end;
min-width: 100%;
padding-bottom: 6px;
font-size: 34px;
font-weight: 700;
text-align: left;
}

View File

@@ -1,7 +1,7 @@
import { Component, ComponentInterface, Element, Host, Prop, h } from '@stencil/core';
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Prop, Watch, h } from '@stencil/core';
import { getIonMode } from '../../global/ionic-global';
import { Color } from '../../interface';
import { Color, StyleEventDetail } from '../../interface';
import { createColorClasses } from '../../utils/theme';
@Component({
@@ -20,6 +20,37 @@ export class ToolbarTitle implements ComponentInterface {
*/
@Prop() color?: Color;
/**
* The size of the toolbar title.
* Only applies in `ios` mode.
*/
@Prop() size?: 'large' | undefined;
/**
* Emitted when the styles change.
* @internal
*/
@Event() ionStyle!: EventEmitter<StyleEventDetail>;
@Watch('size')
protected sizeChanged() {
this.emitStyle();
}
connectedCallback() {
this.emitStyle();
}
private emitStyle() {
this.ionStyle.emit({
[`title-${this.getSize()}`]: true
});
}
private getSize() {
return (this.size !== undefined) ? this.size : 'standard';
}
private getMode() {
const mode = getIonMode(this);
const toolbar = this.el.closest('ion-toolbar');
@@ -33,6 +64,7 @@ export class ToolbarTitle implements ComponentInterface {
class={{
[mode]: true,
[`title-${mode}`]: true,
[`title-${mode}-${this.getSize()}`]: true,
...createColorClasses(this.color),
}}

View File

@@ -0,0 +1,204 @@
<!DOCTYPE html>
<html dir="ltr">
<head>
<meta charset="UTF-8">
<title>Toolbar - Title</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script></head>
<style>
.more, .create {
--dimension: 28px;
width: var(--dimension);
height: var(--dimension) !important;
--border-radius: var(--dimension) !important;
}
.more {
--background: #e9e9ea;
}
.create {
--background: #3777ff;
--color: white !important;
}
.create ion-icon {
transform: scale(0.9);
}
/*
ion-toolbar {
--background: red;
--border-color: #dfe1e0;
}
.main-content {
--background: #f1f2f6;
}
*/
</style>
</head>
<!--<ion-button slot="icon-only" class="more">
<ion-icon name="ios-more"></ion-icon>
</ion-button>
<ion-button slot="icon-only" class="create">
<ion-icon name="ios-create"></ion-icon>
</ion-button>-->
<body>
<ion-app>
<ion-nav root="page-one"></ion-nav>
</ion-app>
<style>
ion-item {
--padding-start: 10px;
}
.item-badge {
padding: 0;
margin-top: 14px;
margin-right: 10px;
width: 14px;
height: 14px;
}
</style>
<script>
class PageTwo extends HTMLElement {
generateList() {
let html = '';
for (let i = 0; i < 100; i++) {
html += `
<ion-nav-link component="page-three">
<ion-item button>
<ion-badge slot="start" class="item-badge ion-align-self-start">&nbsp;</ion-badge>
<ion-label>
<h2>Heading ${i}</h2>
<h3>Smaller heading ${i}</h2>
<p>Paragraph ${i}</p>
</ion-label>
</ion-item>
</ion-nav-link>
`;
}
return html;
}
connectedCallback() {
this.innerHTML = `
<ion-header class="main-header">
<ion-toolbar>
<ion-buttons collapse>
<ion-back-button default-href="/"></ion-back-button>
</ion-buttons>
<ion-title>Page Two</ion-title>
<ion-buttons slot="end">
<ion-button>Hello Page 2</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content id="content" class="main-content">
<ion-header collapse>
<ion-toolbar>
<ion-title size="large">Page Two</ion-title>
</ion-toolbar>
<ion-toolbar>
<ion-searchbar></ion-searchbar>
</ion-toolbar>
</ion-header>
<ion-list id="dynamicList">${this.generateList()}</ion-list>
</ion-content>
`;
}
}
class PageOne extends HTMLElement {
connectedCallback() {
this.innerHTML = `
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button text="Settings"></ion-back-button>
</ion-buttons>
<ion-title>Page main</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<h1>Page One</h1>
<ion-nav-link component="page-two">
<ion-item button>
<ion-label>Go to Page Two</ion-label>
</ion-item>
</ion-nav-link>
</ion-content>
`;
}
}
class PageThree extends HTMLElement {
generateList() {
let html = '';
for (let i = 0; i < 100; i++) {
html += `
<ion-nav-link component="page-two">
<ion-item button color="dark">
<ion-badge slot="start" class="item-badge ion-align-self-start">&nbsp;</ion-badge>
<ion-label>
<h2>Heading ${i}</h2>
<h3>Smaller heading ${i}</h2>
<p>Paragraph ${i}</p>
</ion-label>
</ion-item>
</ion-nav-link>
`;
}
return html;
}
connectedCallback() {
this.innerHTML = `
<ion-header class="three-header" color="dark" translucent>
<ion-toolbar color="dark">
<ion-buttons>
<ion-back-button default-href="/" text="Page Two"></ion-back-button>
</ion-buttons>
<ion-title>Page Three</ion-title>
<ion-buttons slot="end" collapse>
<ion-button>Hello Page 3</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content id="content" class="three-content" fullscreen color="dark">
<ion-header collapse color="dark">
<ion-toolbar color="dark">
<ion-title size="large">Page Three</ion-title>
</ion-toolbar>
</ion-header>
<ion-list id="dynamicList">${this.generateList()}</ion-list>
</ion-content>
`;
}
}
customElements.define('page-one', PageOne);
customElements.define('page-two', PageTwo);
customElements.define('page-three', PageThree);
</script>
</body>
</html>

View File

@@ -8,11 +8,11 @@
--background: #{$toolbar-ios-background};
--color: #{$toolbar-ios-color};
--border-color: #{$toolbar-ios-border-color};
--padding-top: 4px;
--padding-bottom: 4px;
--padding-start: 4px;
--padding-end: 4px;
--min-height: 44px;
--padding-top: #{$toolbar-ios-padding-top};
--padding-bottom: #{$toolbar-ios-padding-bottom};
--padding-start: #{$toolbar-ios-padding-start};
--padding-end: #{$toolbar-ios-padding-end};
--min-height: #{$toolbar-ios-min-height};
}
// Toolbar: Content
@@ -37,6 +37,10 @@
// iOS Toolbar Slot Placement
// --------------------------------------------------
::slotted(ion-buttons) {
min-height: 38px;
}
::slotted([slot="start"]) {
order: map-get($toolbar-order-ios, slot-start);
}
@@ -56,3 +60,19 @@
text-align: end;
}
// Toolbar: Large Title
// --------------------------------------------------
:host(.toolbar-title-large) .toolbar-container {
flex-wrap: wrap;
align-items: flex-start;
}
:host(.toolbar-title-large) .toolbar-content ion-title {
flex: 1;
order: map-get($toolbar-order-ios, title-large);
min-width: 100%;
}

View File

@@ -13,41 +13,26 @@ $toolbar-order-ios: (
slot-primary: 5,
slot-end: 6,
menu-toggle-end: 7,
title-large: 8,
);
/// @prop - Minimum height of the toolbar
$toolbar-ios-min-height: 44px !default;
/// @prop - Padding top of the toolbar
$toolbar-ios-padding-top: 3px !default;
/// @prop - Padding end of the toolbar
$toolbar-ios-padding-end: 4px !default;
/// @prop - Padding bottom of the toolbar
$toolbar-ios-padding-bottom: $toolbar-ios-padding-top !default;
/// @prop - Padding start of the toolbar
$toolbar-ios-padding-start: $toolbar-ios-padding-end !default;
/// @prop - Font size of the toolbar button
$toolbar-ios-button-font-size: 17px !default;
/// @prop - Text color of the toolbar button
$toolbar-ios-button-color: ion-color(primary, base) !default;
/// @prop - Background color of the toolbar button
$toolbar-ios-button-background-color: $toolbar-ios-background !default;
/// @prop - Background color of the toolbar button when activated
$toolbar-ios-button-background-color-activated: $toolbar-ios-color-activated !default;
/// @prop - Border radius of the toolbar button
$toolbar-ios-button-border-radius: 4px !default;
/// @prop - Font weight of the strong toolbar button
$toolbar-ios-button-strong-font-weight: 600 !default;
/// @prop - Fill color of the toolbar button icon
$toolbar-ios-button-icon-fill-color: currentColor !default;
// iOS Title
// --------------------------------------------------
/// @prop - Font size of the toolbar title
$toolbar-ios-title-font-size: 17px !default;
/// @prop - Font weight of the toolbar title
$toolbar-ios-title-font-weight: 600 !default;
/// @prop - Text alignment of the toolbar title
$toolbar-ios-title-text-align: center !default;
/// @prop - Text color of the toolbar title
$toolbar-ios-title-text-color: $toolbar-ios-color !default;
$toolbar-ios-button-border-radius: 4px !default;

View File

@@ -2,7 +2,7 @@ import { Component, ComponentInterface, Element, Host, Listen, Prop, h } from '@
import { getIonMode } from '../../global/ionic-global';
import { Color, CssClassMap, StyleEventDetail } from '../../interface';
import { createColorClasses } from '../../utils/theme';
import { createColorClasses, hostContext } from '../../utils/theme';
/**
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
@@ -89,6 +89,7 @@ export class Toolbar implements ComponentInterface {
return (
<Host
class={{
'in-toolbar': hostContext('ion-toolbar', this.el),
[mode]: true,
...childStyles,
...createColorClasses(this.color),

View File

@@ -7,7 +7,7 @@ const iosTransitionAnimation = () => import('./ios.transition');
const mdTransitionAnimation = () => import('./md.transition');
// TODO: Remove when removing AnimationBuilder
export type IonicAnimationInterface = (navEl: HTMLElement, opts: TransitionOptions) => IonicAnimation;
export type IonicAnimationInterface = ((navEl: HTMLElement, opts: TransitionOptions) => IonicAnimation) | ((navEl: HTMLElement, opts: TransitionOptions) => Promise<IonicAnimation>);
export const transition = (opts: TransitionOptions): Promise<TransitionResult> => {
return new Promise((resolve, reject) => {
@@ -88,7 +88,7 @@ const animation = async (animationBuilder: IonicAnimationInterface | AnimationBu
const mod = await import('../animation/old-animation');
trans = await mod.create(animationBuilder as AnimationBuilder, opts.baseEl, opts);
} catch (err) {
trans = (animationBuilder as IonicAnimationInterface)(opts.baseEl, opts);
trans = (animationBuilder as IonicAnimationInterface)(opts.baseEl, opts) as IonicAnimation;
}
fireWillEvents(opts.enteringEl, opts.leavingEl);

View File

@@ -2,13 +2,173 @@ import { IonicAnimation } from '../../interface';
import { createAnimation } from '../animation/animation';
import { TransitionOptions } from '../transition';
const DURATION = 540;
const addSafeArea = (val: number, side = 'top'): string => {
return `calc(${val}px + var(--ion-safe-area-${side}))`;
};
const getClonedElement = (tagName: string): any => {
return document.querySelector(`${tagName}.ion-cloned-element`) as any ;
};
export const shadow = <T extends Element>(el: T): ShadowRoot | T => {
return el.shadowRoot || el;
};
const getLargeTitle = (refEl: any) => {
return refEl.querySelector('ion-header:not(.header-collapse-ios-inactive) ion-title[size=large]');
};
const getBackButton = (refEl: any, backDirection: boolean) => {
const buttonsList = refEl.querySelectorAll('ion-buttons');
for (const buttons of buttonsList) {
const parentHeader = buttons.closest('ion-header');
const activeHeader = parentHeader && !parentHeader.classList.contains('header-collapse-ios-inactive');
const backButton = buttons.querySelector('ion-back-button');
if (backButton !== null && ((buttons.collapse && activeHeader && backDirection) || !buttons.collapse)) {
return backButton;
}
}
return null;
};
const createLargeTitleTransition = (rootAnimation: IonicAnimation, rtl: boolean, backDirection: boolean, enteringEl: any, leavingEl: any) => {
const enteringBackButton = getBackButton(enteringEl, backDirection);
const leavingLargeTitle = getLargeTitle(leavingEl);
const enteringLargeTitle = getLargeTitle(enteringEl);
const leavingBackButton = getBackButton(leavingEl, backDirection);
const shouldAnimationForward = enteringBackButton !== null && leavingLargeTitle !== null && !backDirection;
const shouldAnimationBackward = enteringLargeTitle !== null && leavingBackButton !== null && backDirection;
if (shouldAnimationForward) {
animateLargeTitle(rootAnimation, rtl, backDirection, leavingLargeTitle);
animateBackButton(rootAnimation, rtl, backDirection, enteringBackButton);
} else if (shouldAnimationBackward) {
animateLargeTitle(rootAnimation, rtl, backDirection, enteringLargeTitle);
animateBackButton(rootAnimation, rtl, backDirection, leavingBackButton);
}
return {
forward: shouldAnimationForward,
backward: shouldAnimationBackward
};
};
const animateBackButton = (rootAnimation: IonicAnimation, rtl: boolean, backDirection: boolean, backButtonEl: any) => {
console.log(rtl);
const FORWARD_TEXT_KEYFRAMES = [
{ offset: 0, opacity: 0, transform: `translate(-7px, ${addSafeArea(8)}) scale(2.1)` },
{ offset: 1, opacity: 1, transform: `translate(4px, ${addSafeArea(-40)}) scale(1)` }
];
const BACKWARD_TEXT_KEYFRAMES = [
{ offset: 0, opacity: 1, transform: `translate(4px, ${addSafeArea(-40)}) scale(1)` },
{ offset: 0.6, opacity: 0 },
{ offset: 1, opacity: 0, transform: `translate(-7px, ${addSafeArea(8)}) scale(2.1)` }
];
const TEXT_KEYFRAMES = (backDirection) ? BACKWARD_TEXT_KEYFRAMES : FORWARD_TEXT_KEYFRAMES;
const FORWARD_ICON_KEYFRAMES = [
{ offset: 0, opacity: 0, transform: `translate3d(4px, ${addSafeArea(-35)}, 0) scale(0.6)` },
{ offset: 1, opacity: 1, transform: `translate3d(4px, ${addSafeArea(-40)}, 0) scale(1)` }
];
const BACKWARD_ICON_KEYFRAMES = [
{ offset: 0, opacity: 1, transform: `translate(4px, ${addSafeArea(-40)}) scale(1)` },
{ offset: 0.2, opacity: 0, transform: `translate(4px, ${addSafeArea(-35)}) scale(0.6)` },
{ offset: 1, opacity: 0, transform: `translate(4px, ${addSafeArea(-35)}) scale(0.6)` }
];
const ICON_KEYFRAMES = (backDirection) ? BACKWARD_ICON_KEYFRAMES : FORWARD_ICON_KEYFRAMES;
const enteringBackButtonTextAnimation = createAnimation();
const enteringBackButtonIconAnimation = createAnimation();
const clonedBackButtonEl = getClonedElement('ion-back-button');
const backButtonTextEl = clonedBackButtonEl.querySelector('.button-text');
const backButtonIconEl = clonedBackButtonEl.querySelector('ion-icon');
clonedBackButtonEl.text = backButtonEl.text;
clonedBackButtonEl.mode = backButtonEl.mode;
clonedBackButtonEl.icon = backButtonEl.icon;
clonedBackButtonEl.color = backButtonEl.color;
clonedBackButtonEl.disabled = backButtonEl.disabled;
clonedBackButtonEl.style.setProperty('display', 'block');
clonedBackButtonEl.style.setProperty('position', 'fixed');
enteringBackButtonIconAnimation.addElement(backButtonIconEl);
enteringBackButtonTextAnimation.addElement(backButtonTextEl);
enteringBackButtonTextAnimation
.beforeStyles({
'transform-origin': 'left center'
})
.beforeAddWrite(() => {
backButtonEl.style.setProperty('display', 'none');
})
.afterAddWrite(() => {
backButtonEl.style.setProperty('display', '');
clonedBackButtonEl.style.setProperty('display', 'none');
})
.keyframes(TEXT_KEYFRAMES);
enteringBackButtonIconAnimation
.beforeStyles({
'transform-origin': 'right center'
})
.keyframes(ICON_KEYFRAMES);
rootAnimation.addAnimation([enteringBackButtonTextAnimation, enteringBackButtonIconAnimation]);
};
const animateLargeTitle = (rootAnimation: IonicAnimation, rtl: boolean, backDirection: boolean, largeTitleEl: any) => {
const TRANSLATE = (rtl) ? '-18px' : '18px';
const BACKWARDS_KEYFRAMES = [
{ offset: 0, opacity: 0, transform: `translate(${TRANSLATE}, ${addSafeArea(0)}) scale(0.49)` },
{ offset: 0.1, opacity: 0 },
{ offset: 1, opacity: 1, transform: `translate(0, ${addSafeArea(49)}) scale(1)` }
];
const FORWARDS_KEYFRAMES = [
{ offset: 0, opacity: 0.99, transform: `translate(0, ${addSafeArea(49)}) scale(1)` },
{ offset: 0.6, opacity: 0 },
{ offset: 1, opacity: 0, transform: `translate(${TRANSLATE}, ${addSafeArea(0)}) scale(0.5)` }
];
const KEYFRAMES = (backDirection) ? BACKWARDS_KEYFRAMES : FORWARDS_KEYFRAMES;
const clonedTitleEl = getClonedElement('ion-title');
const clonedLargeTitleAnimation = createAnimation();
clonedTitleEl.innerText = largeTitleEl.innerText;
clonedTitleEl.size = largeTitleEl.size;
clonedTitleEl.color = largeTitleEl.color;
clonedLargeTitleAnimation.addElement(clonedTitleEl);
clonedLargeTitleAnimation
.beforeStyles({
'transform-origin': 'left center',
'height': '46px',
'display': '',
'position': 'relative'
})
.beforeAddWrite(() => {
largeTitleEl.style.setProperty('display', 'none');
})
.afterAddWrite(() => {
largeTitleEl.style.setProperty('display', '');
clonedTitleEl.style.setProperty('display', 'none');
})
.keyframes(KEYFRAMES);
rootAnimation.addAnimation(clonedLargeTitleAnimation);
};
export const iosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOptions): IonicAnimation => {
try {
const DURATION = 540;
const EASING = 'cubic-bezier(0.32,0.72,0,1)';
const OPACITY = 'opacity';
const TRANSFORM = 'transform';
@@ -94,6 +254,10 @@ export const iosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOptio
}
}
const enteringContentHasLargeTitle = enteringEl.querySelector('ion-header.header-collapse-ios');
const { forward, backward } = createLargeTitleTransition(rootAnimation, isRTL, backDirection, enteringEl, leavingEl);
enteringToolBarEls.forEach(enteringToolBarEl => {
const enteringToolBar = createAnimation();
enteringToolBar.addElement(enteringToolBarEl);
@@ -103,7 +267,22 @@ export const iosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOptio
enteringTitle.addElement(enteringToolBarEl.querySelector('ion-title'));
const enteringToolBarButtons = createAnimation();
enteringToolBarButtons.addElement(enteringToolBarEl.querySelectorAll('ion-buttons,[menuToggle]'));
const buttons = Array.from(enteringToolBarEl.querySelectorAll('ion-buttons,[menuToggle]'));
const parentHeader = enteringToolBarEl.closest('ion-header');
const inactiveHeader = parentHeader && parentHeader.classList.contains('header-collapse-ios-inactive');
let buttonsToAnimate;
if (backDirection) {
buttonsToAnimate = buttons.filter(button => {
const isCollapseButton = (button as any).collapse;
return (isCollapseButton && !inactiveHeader) || !isCollapseButton;
});
} else {
buttonsToAnimate = buttons.filter(button => !(button as any).collapse);
}
enteringToolBarButtons.addElement(buttonsToAnimate);
const enteringToolBarItems = createAnimation();
enteringToolBarItems.addElement(enteringToolBarEl.querySelectorAll(':scope > *:not(ion-title):not(ion-buttons):not([menuToggle])'));
@@ -119,12 +298,16 @@ export const iosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOptio
}
enteringToolBar.addAnimation([enteringTitle, enteringToolBarButtons, enteringToolBarItems, enteringToolBarBg, enteringBackButton]);
enteringTitle.fromTo(OPACITY, 0.01, 1);
enteringToolBarButtons.fromTo(OPACITY, 0.01, 1);
enteringToolBarItems.fromTo(OPACITY, 0.01, 1);
if (backDirection) {
enteringTitle.fromTo('transform', `translateX(${OFF_LEFT})`, `translateX(${CENTER})`);
if (!inactiveHeader) {
enteringTitle
.fromTo('transform', `translateX(${OFF_LEFT})`, `translateX(${CENTER})`)
.fromTo(OPACITY, 0.01, 1);
}
enteringToolBarItems.fromTo('transform', `translateX(${OFF_LEFT})`, `translateX(${CENTER})`);
@@ -132,23 +315,24 @@ export const iosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOptio
enteringBackButton.fromTo(OPACITY, 0.01, 1);
} else {
// entering toolbar, forward direction
enteringTitle.fromTo('transform', `translateX(${OFF_RIGHT})`, `translateX(${CENTER})`);
if (!enteringContentHasLargeTitle) {
enteringTitle
.fromTo('transform', `translateX(${OFF_RIGHT})`, `translateX(${CENTER})`)
.fromTo(OPACITY, 0.01, 1);
}
enteringToolBarItems.fromTo('transform', `translateX(${OFF_RIGHT})`, `translateX(${CENTER})`);
enteringToolBarBg
.beforeClearStyles([OPACITY])
.keyframes([
{ offset: 0, opacity: 0.01 },
{ offset: 0.99, opacity: 1 },
{ offset: 1, opacity: 'var(--opacity)' }
// TODO: Find a way to support clearing properties from Web Animations
]);
.fromTo(OPACITY, 0.01, 1);
// forward direction, entering page has a back button
enteringBackButton.fromTo(OPACITY, 0.01, 1);
if (!forward) {
enteringBackButton.fromTo(OPACITY, 0.01, 1);
}
if (backButtonEl) {
if (backButtonEl && !forward) {
const enteringBackBtnText = createAnimation();
enteringBackBtnText
.addElement(shadow(backButtonEl).querySelector('.button-text'))
@@ -222,7 +406,17 @@ export const iosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOptio
leavingTitle.addElement(leavingToolBarEl.querySelector('ion-title'));
const leavingToolBarButtons = createAnimation();
leavingToolBarButtons.addElement(leavingToolBarEl.querySelectorAll('ion-buttons,[menuToggle]'));
const buttons = leavingToolBarEl.querySelectorAll('ion-buttons,[menuToggle]');
const parentHeader = leavingToolBarEl.closest('ion-header');
const inactiveHeader = parentHeader && parentHeader.classList.contains('header-collapse-ios-inactive');
const buttonsToAnimate = Array.from(buttons).filter(button => {
const isCollapseButton = (button as any).collapse;
return (isCollapseButton && !inactiveHeader) || !isCollapseButton;
});
leavingToolBarButtons.addElement(buttonsToAnimate);
const leavingToolBarItems = createAnimation();
const leavingToolBarItemEls = leavingToolBarEl.querySelectorAll(':scope > *:not(ion-title):not(ion-buttons):not([menuToggle])');
@@ -244,13 +438,19 @@ export const iosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOptio
// fade out leaving toolbar items
leavingBackButton.fromTo(OPACITY, 0.99, 0);
leavingTitle.fromTo(OPACITY, 0.99, 0);
leavingToolBarButtons.fromTo(OPACITY, 0.99, 0);
leavingToolBarItems.fromTo(OPACITY, 0.99, 0);
if (backDirection) {
// leaving toolbar, back direction
leavingTitle.fromTo('transform', `translateX(${CENTER})`, (isRTL ? 'translateX(-100%)' : 'translateX(100%)'));
if (!inactiveHeader) {
// leaving toolbar, back direction
leavingTitle
.fromTo('transform', `translateX(${CENTER})`, (isRTL ? 'translateX(-100%)' : 'translateX(100%)'))
.fromTo(OPACITY, 0.99, 0);
}
leavingToolBarItems.fromTo('transform', `translateX(${CENTER})`, (isRTL ? 'translateX(-100%)' : 'translateX(100%)'));
// leaving toolbar, back direction, and there's no entering toolbar
@@ -259,18 +459,23 @@ export const iosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOptio
.beforeClearStyles([OPACITY])
.fromTo(OPACITY, 1, 0.01);
if (backButtonEl) {
if (backButtonEl && !backward) {
const leavingBackBtnText = createAnimation();
leavingBackBtnText.addElement(shadow(backButtonEl).querySelector('.button-text'));
leavingBackBtnText.fromTo('transform', `translateX(${CENTER})`, `translateX(${(isRTL ? -124 : 124) + 'px'})`);
leavingBackBtnText
.addElement(shadow(backButtonEl).querySelector('.button-text'))
.fromTo('transform', `translateX(${CENTER})`, `translateX(${(isRTL ? -124 : 124) + 'px'})`);
leavingToolBar.addAnimation(leavingBackBtnText);
}
} else {
// leaving toolbar, forward direction
leavingTitle
.fromTo('transform', `translateX(${CENTER})`, `translateX(${OFF_LEFT})`)
.afterClearStyles([TRANSFORM]);
if (!inactiveHeader) {
leavingTitle
.fromTo('transform', `translateX(${CENTER})`, `translateX(${OFF_LEFT})`)
.fromTo(OPACITY, 0.99, 0)
.afterClearStyles([TRANSFORM, OPACITY]);
}
leavingToolBarItems
.fromTo('transform', `translateX(${CENTER})`, `translateX(${OFF_LEFT})`)
.afterClearStyles([TRANSFORM, OPACITY]);