mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
feat(title): add large iOS toolbar title (#19268)
Co-authored-by: Brandy Carney <brandyscarney@users.noreply.github.com>
This commit is contained in:
@@ -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'] })
|
||||
|
||||
@@ -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
|
||||
|
||||
30
core/src/components.d.ts
vendored
30
core/src/components.d.ts
vendored
@@ -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> {
|
||||
/**
|
||||
|
||||
@@ -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)}>
|
||||
|
||||
@@ -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/)*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -25,3 +25,7 @@
|
||||
.header-md[no-border]::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header-collapse-md {
|
||||
display: none;
|
||||
}
|
||||
@@ -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,
|
||||
}}
|
||||
>
|
||||
|
||||
142
core/src/components/header/header.utils.ts
Normal file
142
core/src/components/header/header.utils.ts
Normal 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)`;
|
||||
});
|
||||
};
|
||||
@@ -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` |
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
|
||||
height: 100%;
|
||||
|
||||
font-size: 14px;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
|
||||
contain: strict;
|
||||
|
||||
@@ -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
|
||||
|
||||
89
core/src/components/title/test/scenarios/index.html
Normal file
89
core/src/components/title/test/scenarios/index.html
Normal 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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}}
|
||||
|
||||
204
core/src/components/toolbar/test/title/index.html
Normal file
204
core/src/components/toolbar/test/title/index.html
Normal 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"> </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"> </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>
|
||||
@@ -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%;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]);
|
||||
|
||||
Reference in New Issue
Block a user