diff --git a/angular/src/app-initialize.ts b/angular/src/app-initialize.ts index 65cdadf37b..8df7b9ddf9 100644 --- a/angular/src/app-initialize.ts +++ b/angular/src/app-initialize.ts @@ -3,6 +3,7 @@ import { applyPolyfills, defineCustomElements } from '@ionic/core/loader'; import { Config } from './providers/config'; import { IonicWindow } from './types/interfaces'; +import { raf } from './util/util'; export function appInitialize(config: Config, doc: Document, zone: NgZone) { return (): any => { @@ -23,12 +24,8 @@ export function appInitialize(config: Config, doc: Document, zone: NgZone) { return defineCustomElements(win, { exclude: ['ion-tabs', 'ion-tab'], syncQueue: true, + raf, jmp: (h: any) => zone.runOutsideAngular(h), - raf: h => { - return zone.runOutsideAngular(() => { - return (win.__zone_symbol__requestAnimationFrame) ? win.__zone_symbol__requestAnimationFrame(h) : requestAnimationFrame(h); - }); - }, ael(elm, eventName, cb, opts) { (elm as any)[aelFn](eventName, cb, opts); }, diff --git a/angular/src/directives/control-value-accessors/value-accessor.ts b/angular/src/directives/control-value-accessors/value-accessor.ts index 9ce45ecf51..4bb10db791 100644 --- a/angular/src/directives/control-value-accessors/value-accessor.ts +++ b/angular/src/directives/control-value-accessors/value-accessor.ts @@ -1,6 +1,8 @@ import { ElementRef, HostListener } from '@angular/core'; import { ControlValueAccessor } from '@angular/forms'; +import { raf } from '../../util/util'; + export class ValueAccessor implements ControlValueAccessor { private onChange: (value: any) => void = () => {/**/}; @@ -42,14 +44,16 @@ export class ValueAccessor implements ControlValueAccessor { } export function setIonicClasses(element: ElementRef) { - const input = element.nativeElement as HTMLElement; - const classes = getClasses(input); - setClasses(input, classes); + raf(() => { + const input = element.nativeElement as HTMLElement; + const classes = getClasses(input); + setClasses(input, classes); - const item = input.closest('ion-item'); - if (item) { - setClasses(item, classes); - } + const item = input.closest('ion-item'); + if (item) { + setClasses(item, classes); + } + }); } function getClasses(element: HTMLElement) { diff --git a/angular/src/directives/navigation/ion-router-outlet.ts b/angular/src/directives/navigation/ion-router-outlet.ts index 55cf6e3610..9bfacd11f3 100644 --- a/angular/src/directives/navigation/ion-router-outlet.ts +++ b/angular/src/directives/navigation/ion-router-outlet.ts @@ -205,7 +205,6 @@ export class IonRouterOutlet implements OnDestroy, OnInit { // Calling `markForCheck` to make sure we will run the change detection when the // `RouterOutlet` is inside a `ChangeDetectionStrategy.OnPush` component. enteringView = this.stackCtrl.createView(this.activated, activatedRoute); - enteringView.ref.changeDetectorRef.detectChanges(); // Store references to the proxy by component this.proxyMap.set(cmpRef.instance, activatedRouteProxy); diff --git a/angular/src/directives/navigation/stack-controller.ts b/angular/src/directives/navigation/stack-controller.ts index ba647815c6..e4b04436ac 100644 --- a/angular/src/directives/navigation/stack-controller.ts +++ b/angular/src/directives/navigation/stack-controller.ts @@ -31,7 +31,7 @@ export class StackController { createView(ref: ComponentRef, activatedRoute: ActivatedRoute): RouteView { const url = getUrl(this.router, activatedRoute); const element = (ref && ref.location && ref.location.nativeElement) as HTMLElement; - const unlistenEvents = bindLifecycleEvents(ref.instance, element); + const unlistenEvents = bindLifecycleEvents(this.zone, ref.instance, element); return { id: this.nextId++, stackId: computeStackId(this.tabsPrefix, url), @@ -90,16 +90,28 @@ export class StackController { } } + const reused = this.views.includes(enteringView); const views = this.insertView(enteringView, direction); - return this.wait(() => { - return this.transition(enteringView, leavingView, animation, this.canGoBack(1), false) - .then(() => cleanupAsync(enteringView, views, viewsSnapshot, this.location)) - .then(() => ({ - enteringView, - direction, - animation, - tabSwitch - })); + + // Trigger change detection before transition starts + // This will call ngOnInit() the first time too, just after the view + // was attached to the dom, but BEFORE the transition starts + if (!reused) { + enteringView.ref.changeDetectorRef.detectChanges(); + } + + // Wait until previous transitions finish + return this.zone.runOutsideAngular(() => { + return this.wait(() => { + return this.transition(enteringView, leavingView, animation, this.canGoBack(1), false) + .then(() => cleanupAsync(enteringView, views, viewsSnapshot, this.location)) + .then(() => ({ + enteringView, + direction, + animation, + tabSwitch + })); + }); }); } @@ -199,11 +211,11 @@ export class StackController { if (enteringView) { enteringView.ref.changeDetectorRef.reattach(); } - // TODO: disconnect leaving page from change detection to + // disconnect leaving page from change detection to // reduce jank during the page transition - // if (leavingView) { - // leavingView.ref.changeDetectorRef.detach(); - // } + if (leavingView) { + leavingView.ref.changeDetectorRef.detach(); + } const enteringEl = enteringView ? enteringView.element : undefined; const leavingEl = leavingView ? leavingView.element : undefined; const containerEl = this.containerEl; @@ -213,13 +225,13 @@ export class StackController { containerEl.appendChild(enteringEl); } - return this.zone.runOutsideAngular(() => containerEl.commit(enteringEl, leavingEl, { + return containerEl.commit(enteringEl, leavingEl, { deepWait: true, duration: direction === undefined ? 0 : undefined, direction, showGoBack, progressAnimation - })); + }); } return Promise.resolve(false); } diff --git a/angular/src/providers/angular-delegate.ts b/angular/src/providers/angular-delegate.ts index 36ac4fc001..301823e91a 100644 --- a/angular/src/providers/angular-delegate.ts +++ b/angular/src/providers/angular-delegate.ts @@ -34,10 +34,10 @@ export class AngularFrameworkDelegate implements FrameworkDelegate { ) {} attachViewToDom(container: any, component: any, params?: any, cssClasses?: string[]): Promise { - return new Promise(resolve => { - this.zone.run(() => { + return this.zone.run(() => { + return new Promise(resolve => { const el = attachView( - this.resolver, this.injector, this.location, this.appRef, + this.zone, this.resolver, this.injector, this.location, this.appRef, this.elRefMap, this.elEventsMap, container, component, params, cssClasses ); @@ -47,8 +47,8 @@ export class AngularFrameworkDelegate implements FrameworkDelegate { } removeViewFromDom(_container: any, component: any): Promise { - return new Promise(resolve => { - this.zone.run(() => { + return this.zone.run(() => { + return new Promise(resolve => { const componentRef = this.elRefMap.get(component); if (componentRef) { componentRef.destroy(); @@ -66,6 +66,7 @@ export class AngularFrameworkDelegate implements FrameworkDelegate { } export function attachView( + zone: NgZone, resolver: ComponentFactoryResolver, injector: Injector, location: ViewContainerRef | undefined, @@ -93,7 +94,7 @@ export function attachView( hostElement.classList.add(clazz); } } - const unbindEvents = bindLifecycleEvents(instance, hostElement); + const unbindEvents = bindLifecycleEvents(zone, instance, hostElement); container.appendChild(hostElement); if (!location) { @@ -113,15 +114,17 @@ const LIFECYCLES = [ LIFECYCLE_WILL_UNLOAD ]; -export function bindLifecycleEvents(instance: any, element: HTMLElement) { - const unregisters = LIFECYCLES - .filter(eventName => typeof instance[eventName] === 'function') - .map(eventName => { - const handler = (ev: any) => instance[eventName](ev.detail); - element.addEventListener(eventName, handler); - return () => element.removeEventListener(eventName, handler); - }); - return () => unregisters.forEach(fn => fn()); +export function bindLifecycleEvents(zone: NgZone, instance: any, element: HTMLElement) { + return zone.run(() => { + const unregisters = LIFECYCLES + .filter(eventName => typeof instance[eventName] === 'function') + .map(eventName => { + const handler = (ev: any) => instance[eventName](ev.detail); + element.addEventListener(eventName, handler); + return () => element.removeEventListener(eventName, handler); + }); + return () => unregisters.forEach(fn => fn()); + }); } const NavParamsToken = new InjectionToken('NavParamsToken'); diff --git a/angular/src/util/util.ts b/angular/src/util/util.ts index ee2026ab72..3c6329fb24 100644 --- a/angular/src/util/util.ts +++ b/angular/src/util/util.ts @@ -1,5 +1,10 @@ import { HTMLStencilElement } from '../types/interfaces'; +export const raf = (h: any) => { + const win = window as any; + return (win.__zone_symbol__requestAnimationFrame) ? win.__zone_symbol__requestAnimationFrame(h) : requestAnimationFrame(h); +}; + export function proxyMethod(ctrlName: string, doc: Document, methodName: string, ...args: any[]) { const controller = ensureElementInBody(ctrlName, doc); return controller.componentOnReady() diff --git a/angular/test/test-app/e2e/src/router-link.e2e-spec.ts b/angular/test/test-app/e2e/src/router-link.e2e-spec.ts index 1f7f8b3904..20fcbb23cf 100644 --- a/angular/test/test-app/e2e/src/router-link.e2e-spec.ts +++ b/angular/test/test-app/e2e/src/router-link.e2e-spec.ts @@ -1,5 +1,5 @@ import { browser, element, by, protractor } from 'protractor'; -import { waitTime, testStack, testLifeCycle, handleErrorMessages } from './utils'; +import { waitTime, testStack, testLifeCycle, handleErrorMessages, getText } from './utils'; const EC = protractor.ExpectedConditions; @@ -23,12 +23,13 @@ describe('router-link params and fragments', () => { it('should return to a page with preserved query param and fragment', async () => { await browser.get('/router-link?ionic:_testing=true'); + await waitTime(30); await element(by.css('#queryParamsFragment')).click(); - await waitTime(200); + await waitTime(400); await element(by.css('#goToPage3')).click(); browser.wait(EC.urlContains('router-link-page3'), 5000); - await waitTime(200); + await waitTime(400); await element(by.css('#goBackFromPage3')).click(); @@ -38,6 +39,7 @@ describe('router-link params and fragments', () => { it('should preserve query param and fragment with defaultHref string', async () => { await browser.get('/router-link-page3?ionic:_testing=true'); + await waitTime(30); await element(by.css('#goBackFromPage3')).click(); @@ -71,18 +73,6 @@ describe('router-link', () => { it('should go forward with ion-button[routerLink]', async () => { await element(by.css('#routerLink')).click(); await testForward(); - - // test go back - await element(by.css('ion-back-button')).click(); - await waitTime(500); - - await testStack('ion-router-outlet', ['app-router-link']); - await testLifeCycle('app-router-link', { - ionViewWillEnter: 2, - ionViewDidEnter: 2, - ionViewWillLeave: 1, - ionViewDidLeave: 1, - }); }); it('should go forward with a[routerLink]', async () => { @@ -140,18 +130,23 @@ describe('router-link', () => { async function testForward() { await waitTime(2500); await testStack('ion-router-outlet', ['app-router-link', 'app-router-link-page']); - await testLifeCycle('app-router-link', { - ionViewWillEnter: 1, - ionViewDidEnter: 1, - ionViewWillLeave: 1, - ionViewDidLeave: 1, - }); await testLifeCycle('app-router-link-page', { ionViewWillEnter: 1, ionViewDidEnter: 1, ionViewWillLeave: 0, ionViewDidLeave: 0, }); + expect(await getText(`app-router-link-page #canGoBack`)).toEqual('true'); + + await browser.navigate().back(); + await waitTime(100); + await testStack('ion-router-outlet', ['app-router-link']); + await testLifeCycle('app-router-link', { + ionViewWillEnter: 2, + ionViewDidEnter: 2, + ionViewWillLeave: 1, + ionViewDidLeave: 1, + }); } async function testRoot() { @@ -163,6 +158,8 @@ async function testRoot() { ionViewWillLeave: 0, ionViewDidLeave: 0, }); + expect(await getText(`app-router-link-page #canGoBack`)).toEqual('false'); + await browser.navigate().back(); await waitTime(100); await testStack('ion-router-outlet', ['app-router-link']); @@ -183,4 +180,15 @@ async function testBack() { ionViewWillLeave: 0, ionViewDidLeave: 0, }); + expect(await getText(`app-router-link-page #canGoBack`)).toEqual('false'); + + await browser.navigate().back(); + await waitTime(100); + await testStack('ion-router-outlet', ['app-router-link']); + await testLifeCycle('app-router-link', { + ionViewWillEnter: 1, + ionViewDidEnter: 1, + ionViewWillLeave: 0, + ionViewDidLeave: 0, + }); } diff --git a/angular/test/test-app/e2e/src/utils.ts b/angular/test/test-app/e2e/src/utils.ts index eab27612cf..cc85e9ab6c 100644 --- a/angular/test/test-app/e2e/src/utils.ts +++ b/angular/test/test-app/e2e/src/utils.ts @@ -48,11 +48,19 @@ export function handleErrorMessages() { export async function testLifeCycle(selector: string, expected: LifeCycleCount) { await waitTime(50); - expect(await getText(`${selector} #ngOnInit`)).toEqual('1'); - expect(await getText(`${selector} #ionViewWillEnter`)).toEqual(expected.ionViewWillEnter.toString()); - expect(await getText(`${selector} #ionViewDidEnter`)).toEqual(expected.ionViewDidEnter.toString()); - expect(await getText(`${selector} #ionViewWillLeave`)).toEqual(expected.ionViewWillLeave.toString()); - expect(await getText(`${selector} #ionViewDidLeave`)).toEqual(expected.ionViewDidLeave.toString()); + const results = await Promise.all([ + getText(`${selector} #ngOnInit`), + getText(`${selector} #ionViewWillEnter`), + getText(`${selector} #ionViewDidEnter`), + getText(`${selector} #ionViewWillLeave`), + getText(`${selector} #ionViewDidLeave`), + ]); + + expect(results[0]).toEqual('1'); + expect(results[1]).toEqual(expected.ionViewWillEnter.toString()); + expect(results[2]).toEqual(expected.ionViewDidEnter.toString()); + expect(results[3]).toEqual(expected.ionViewWillLeave.toString()); + expect(results[4]).toEqual(expected.ionViewDidLeave.toString()); } export async function testStack(selector: string, expected: string[]) { diff --git a/angular/test/test-app/src/app/alert/alert.component.html b/angular/test/test-app/src/app/alert/alert.component.html index 68f6210040..ed8c16e3e6 100644 --- a/angular/test/test-app/src/app/alert/alert.component.html +++ b/angular/test/test-app/src/app/alert/alert.component.html @@ -1,10 +1,10 @@ - Modal test + Alert test - Open Alert +

Change Detections: {{counter()}}

diff --git a/angular/test/test-app/src/app/alert/alert.component.ts b/angular/test/test-app/src/app/alert/alert.component.ts index 8ff209ab62..95c3d33433 100644 --- a/angular/test/test-app/src/app/alert/alert.component.ts +++ b/angular/test/test-app/src/app/alert/alert.component.ts @@ -8,10 +8,17 @@ import { NavComponent } from '../nav/nav.component'; }) export class AlertComponent { + changes = 0; + constructor( private alertCtrl: AlertController ) { } + counter() { + this.changes++; + return Math.floor(this.changes / 2); + } + async openAlert() { const alert = await this.alertCtrl.create({ header: 'Hello', diff --git a/angular/test/test-app/src/app/router-link-page/router-link-page.component.html b/angular/test/test-app/src/app/router-link-page/router-link-page.component.html index 8077d198b0..aa12296afb 100644 --- a/angular/test/test-app/src/app/router-link-page/router-link-page.component.html +++ b/angular/test/test-app/src/app/router-link-page/router-link-page.component.html @@ -9,6 +9,7 @@

ngOnInit: {{onInit}}

+

canGoBack: {{canGoBack}}

ionViewWillEnter: {{willEnter}}

ionViewDidEnter: {{didEnter}}

ionViewWillLeave: {{willLeave}}

diff --git a/angular/test/test-app/src/app/router-link-page/router-link-page.component.ts b/angular/test/test-app/src/app/router-link-page/router-link-page.component.ts index 882b0ffa70..74149e8a48 100644 --- a/angular/test/test-app/src/app/router-link-page/router-link-page.component.ts +++ b/angular/test/test-app/src/app/router-link-page/router-link-page.component.ts @@ -1,4 +1,5 @@ import { Component, OnInit, NgZone } from '@angular/core'; +import { IonRouterOutlet } from '@ionic/angular'; @Component({ selector: 'app-router-link-page', @@ -11,9 +12,15 @@ export class RouterLinkPageComponent implements OnInit { didEnter = 0; willLeave = 0; didLeave = 0; + canGoBack: boolean = null; + + constructor( + private ionRouterOutlet: IonRouterOutlet + ) {} ngOnInit() { NgZone.assertInAngularZone(); + this.canGoBack = this.ionRouterOutlet.canGoBack(); this.onInit++; } @@ -21,10 +28,16 @@ export class RouterLinkPageComponent implements OnInit { if (this.onInit !== 1) { throw new Error('ngOnInit was not called'); } + if (this.canGoBack !== this.ionRouterOutlet.canGoBack()) { + throw new Error('canGoBack() changed'); + } NgZone.assertInAngularZone(); this.willEnter++; } ionViewDidEnter() { + if (this.canGoBack !== this.ionRouterOutlet.canGoBack()) { + throw new Error('canGoBack() changed'); + } NgZone.assertInAngularZone(); this.didEnter++; } diff --git a/core/scripts/swiper.rollup.config.js b/core/scripts/swiper.rollup.config.js index a0974615f4..13b8e450db 100644 --- a/core/scripts/swiper.rollup.config.js +++ b/core/scripts/swiper.rollup.config.js @@ -7,8 +7,6 @@ export default { format: 'es' }, plugins: [ - resolve({ - module: true - }) + resolve() ] }; \ No newline at end of file diff --git a/core/scripts/testing/scripts.js b/core/scripts/testing/scripts.js index 29ae5499d5..d5cc361166 100644 --- a/core/scripts/testing/scripts.js +++ b/core/scripts/testing/scripts.js @@ -5,4 +5,8 @@ document.documentElement.setAttribute('dir', 'rtl'); } + window.Ionic = window.Ionic || {}; + window.Ionic.config = window.Ionic.config || {}; + window.Ionic.config.experimentalTransitionShadow = true; + })(); \ No newline at end of file diff --git a/core/src/components/app/app.tsx b/core/src/components/app/app.tsx index 9568f01567..8f2a2a9507 100644 --- a/core/src/components/app/app.tsx +++ b/core/src/components/app/app.tsx @@ -15,15 +15,19 @@ export class App implements ComponentInterface { componentDidLoad() { rIC(() => { + const isHybrid = isPlatform(window, 'hybrid'); if (!config.getBoolean('_testing')) { import('../../utils/tap-click').then(module => module.startTapClick(config)); } - if (config.getBoolean('statusTap', isPlatform(window, 'hybrid'))) { + if (config.getBoolean('statusTap', isHybrid)) { import('../../utils/status-tap').then(module => module.startStatusTap()); } if (config.getBoolean('inputShims', needInputShims())) { import('../../utils/input-shims/input-shims').then(module => module.startInputShims(config)); } + if (config.getBoolean('hardwareBackButton', isHybrid)) { + import('../../utils/hardware-back-button').then(module => module.startHardwareBackButton()); + } import('../../utils/focus-visible').then(module => module.startFocusVisible()); }); diff --git a/core/src/components/content/content.scss b/core/src/components/content/content.scss index 822f50df5f..a52045c89b 100644 --- a/core/src/components/content/content.scss +++ b/core/src/components/content/content.scss @@ -118,3 +118,47 @@ :host(.content-sizing) .inner-scroll { position: relative; } + +.transition-effect { + position: absolute; + + /* stylelint-disable property-blacklist */ + left: -100%; + /* stylelint-enable property-blacklist */ + + width: 100%; + height: 100%; + + opacity: 0; +} + +.transition-cover { + position: absolute; + + /* stylelint-disable property-blacklist */ + right: 0; + /* stylelint-enable property-blacklist */ + + width: 100%; + height: 100%; + + background: black; + + opacity: 0.1; +} + +.transition-shadow { + display: block; + position: absolute; + + /* stylelint-disable property-blacklist */ + right: 0; + /* stylelint-enable property-blacklist */ + + width: 10px; + height: 100%; + + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAgCAYAAAAIXrg4AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTQ1IDc5LjE2MzQ5OSwgMjAxOC8wOC8xMy0xNjo0MDoyMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTkgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MTE3MDgzRkQ5QTkyMTFFOUEwNzQ5MkJFREE1NUY2MjQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MTE3MDgzRkU5QTkyMTFFOUEwNzQ5MkJFREE1NUY2MjQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDoxMTcwODNGQjlBOTIxMUU5QTA3NDkyQkVEQTU1RjYyNCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDoxMTcwODNGQzlBOTIxMUU5QTA3NDkyQkVEQTU1RjYyNCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PmePEuQAAABNSURBVHjaYvz//z8DIxAwMDAwATGMhmFmPDQuOSZks0AMmoJBaQHjkPfB0Lfg/2gQjVow+HPy/yHvg9GiYjQfjMbBqAWjFgy/4hogwADYqwdzxy5BuwAAAABJRU5ErkJggg==); + background-repeat: repeat-y; + background-size: 10px 16px; +} diff --git a/core/src/components/content/content.tsx b/core/src/components/content/content.tsx index 38e9db67e0..4cd5254c26 100644 --- a/core/src/components/content/content.tsx +++ b/core/src/components/content/content.tsx @@ -1,5 +1,6 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Listen, Method, Prop, h, readTask } from '@stencil/core'; +import { config } from '../../global/config'; import { getIonMode } from '../../global/ionic-global'; import { Color, ScrollBaseDetail, ScrollDetail } from '../../interface'; import { isPlatform } from '../../utils/platform'; @@ -301,6 +302,8 @@ export class Content implements ComponentInterface { const mode = getIonMode(this); const { scrollX, scrollY, forceOverscroll } = this; + const transitionShadow = (mode === 'ios' && config.getBoolean('experimentalTransitionShadow', false)); + this.resize(); return ( @@ -328,6 +331,12 @@ export class Content implements ComponentInterface { > + {transitionShadow ? ( +
+
+
+
+ ) : null} ); @@ -370,6 +379,8 @@ const updateScrollDetail = ( const prevT = detail.timeStamp; const currentX = el.scrollLeft; const currentY = el.scrollTop; + const timeDelta = timestamp - prevT; + if (shouldStart) { // remember the start positions detail.startTimeStamp = timestamp; @@ -383,7 +394,6 @@ const updateScrollDetail = ( detail.deltaX = currentX - detail.startX; detail.deltaY = currentY - detail.startY; - const timeDelta = timestamp - prevT; if (timeDelta > 0 && timeDelta < 100) { const velocityX = (currentX - prevX) / timeDelta; const velocityY = (currentY - prevY) / timeDelta; diff --git a/core/src/components/content/test/basic/index.html b/core/src/components/content/test/basic/index.html index bf0e4472e5..5983d8dc99 100644 --- a/core/src/components/content/test/basic/index.html +++ b/core/src/components/content/test/basic/index.html @@ -154,4 +154,4 @@ - + \ No newline at end of file diff --git a/core/src/components/nav/test/routing/index.html b/core/src/components/nav/test/routing/index.html index 594a6e0d73..e0667e0bb1 100644 --- a/core/src/components/nav/test/routing/index.html +++ b/core/src/components/nav/test/routing/index.html @@ -10,6 +10,9 @@ + + + + + + + + + + + Animations + + + + +
+ play step +
+
Hello
+
+
+
+ + + + + diff --git a/core/src/utils/config.ts b/core/src/utils/config.ts index bd3377ac40..d4c971b9e8 100644 --- a/core/src/utils/config.ts +++ b/core/src/utils/config.ts @@ -165,6 +165,11 @@ export interface IonicConfig { */ pickerLeave?: AnimationBuilder; + /** + * EXPERIMENTAL: Adds a page shadow to transitioning pages on iOS. Disabled by default. + */ + experimentalTransitionShadow?: boolean; + // PRIVATE configs keyboardHeight?: number; inputShims?: boolean; diff --git a/core/src/utils/transition/ios.transition.ts b/core/src/utils/transition/ios.transition.ts index 3d257c3d6e..a64e680fe9 100644 --- a/core/src/utils/transition/ios.transition.ts +++ b/core/src/utils/transition/ios.transition.ts @@ -2,20 +2,19 @@ import { Animation } from '../../interface'; import { Animation as AnimationNew, createAnimation } from '../animation/animation'; import { TransitionOptions } from '../transition'; -const DURATION = 500; -const EASING = 'cubic-bezier(0.36,0.66,0.04,1)'; -const OPACITY = 'opacity'; -const TRANSFORM = 'transform'; -const TRANSLATEX = 'translateX'; -const CENTER = '0%'; -const OFF_OPACITY = 0.8; - export const shadow = (el: T): ShadowRoot | T => { return el.shadowRoot || el; }; export const newIosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOptions): Promise => { try { + const DURATION = 540; + const EASING = 'cubic-bezier(0.32,0.72,0,1)'; + const OPACITY = 'opacity'; + const TRANSFORM = 'transform'; + const CENTER = '0%'; + const OFF_OPACITY = 0.8; + const isRTL = (navEl.ownerDocument as any).dir === 'rtl'; const OFF_RIGHT = isRTL ? '-99.5%' : '99.5%'; const OFF_LEFT = isRTL ? '33%' : '-33%'; @@ -61,7 +60,37 @@ export const newIosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOp // entering content, forward direction enteringContentAnimation .beforeClearStyles([OPACITY]) - .fromTo('transform', `translateX(${OFF_RIGHT})`, `translateX(${CENTER})`); + .fromTo('transform', `translateX(${OFF_RIGHT})`, `translateX(${CENTER})`); + } + + if (contentEl) { + const enteringTransitionEffectEl = shadow(contentEl).querySelector('.transition-effect'); + if (enteringTransitionEffectEl) { + const enteringTransitionCoverEl = enteringTransitionEffectEl.querySelector('.transition-cover'); + const enteringTransitionShadowEl = enteringTransitionEffectEl.querySelector('.transition-shadow'); + + const enteringTransitionEffect = createAnimation('ios-entering-transition-effect'); + const enteringTransitionCover = createAnimation('ios-entering-transition-cover'); + const enteringTransitionShadow = createAnimation('ios-entering-transition-shadow'); + + enteringTransitionEffect + .addElement(enteringTransitionEffectEl) + .beforeStyles({ opacity: '1' }) + .afterStyles({ opacity: '' }); + + enteringTransitionCover + .addElement(enteringTransitionCoverEl) + .beforeClearStyles([OPACITY]) + .fromTo(OPACITY, 0, 0.1); + + enteringTransitionShadow + .addElement(enteringTransitionShadowEl) + .beforeClearStyles([OPACITY]) + .fromTo(OPACITY, 0.03, 0.70); + + enteringTransitionEffect.addAnimation([enteringTransitionCover, enteringTransitionShadow]); + enteringContentAnimation.addAnimation([enteringTransitionEffect]); + } } enteringToolBarEls.forEach((enteringToolBarEl, i) => { @@ -127,8 +156,10 @@ export const newIosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOp // setup leaving view if (leavingEl) { - const leavingContent = createAnimation(`ios-leaving-content-animation`); - leavingContent.addElement(leavingEl.querySelector(':scope > ion-content')); + let leavingContent = createAnimation(`ios-leaving-content-animation`); + const leavingContentEl = leavingEl.querySelector(':scope > ion-content'); + + leavingContent.addElement(leavingContentEl); leavingContent.addElement(leavingEl.querySelectorAll(':scope > ion-header > *:not(ion-toolbar), :scope > ion-footer > *')); rootAnimation.addAnimation(leavingContent); @@ -144,6 +175,37 @@ export const newIosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOp .fromTo('transform', `translateX(${CENTER})`, `translateX(${OFF_LEFT})`) .fromTo(OPACITY, 1, OFF_OPACITY); } + + if (leavingContentEl) { + const leavingTransitionEffectEl = shadow(leavingContentEl).querySelector('.transition-effect'); + + if (leavingTransitionEffectEl) { + const leavingTransitionCoverEl = leavingTransitionEffectEl.querySelector('.transition-cover'); + const leavingTransitionShadowEl = leavingTransitionEffectEl.querySelector('.transition-shadow'); + + const leavingTransitionEffect = createAnimation('ios-leaving-transition-effect'); + const leavingTransitionCover = createAnimation('ios-leaving-transition-cover'); + const leavingTransitionShadow = createAnimation('ios-leaving-transition-shadow'); + + leavingTransitionEffect + .addElement(leavingTransitionEffectEl) + .beforeStyles({ opacity: '1' }) + .afterStyles({ opacity: '' }); + + leavingTransitionCover + .addElement(leavingTransitionCoverEl) + .beforeClearStyles([OPACITY]) + .fromTo(OPACITY, 0.1, 0); + + leavingTransitionShadow + .addElement(leavingTransitionShadowEl) + .beforeClearStyles([OPACITY]) + .fromTo(OPACITY, 0.70, 0.03); + + leavingTransitionEffect.addAnimation([leavingTransitionCover, leavingTransitionShadow]); + leavingContent.addAnimation([leavingTransitionEffect]); + } + } const leavingToolBarEls = leavingEl.querySelectorAll(':scope > ion-header > ion-toolbar'); leavingToolBarEls.forEach((leavingToolBarEl, i) => { @@ -192,7 +254,7 @@ export const newIosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOp .fromTo(OPACITY, 1, 0.01); if (backButtonEl) { - const leavingBackBtnText = createAnimation('ios-leaving-toolbar-${i}-back-button-text'); + const leavingBackBtnText = createAnimation(`ios-leaving-toolbar-${i}-back-button-text`); leavingBackBtnText.addElement(shadow(backButtonEl).querySelector('.button-text')); leavingBackBtnText.fromTo('transform', `translateX(${CENTER})`, `translateX(${(isRTL ? -124 : 124) + 'px'})`); leavingToolBar.addAnimation(leavingBackBtnText); @@ -222,12 +284,25 @@ export const newIosTransitionAnimation = (navEl: HTMLElement, opts: TransitionOp export const iosTransitionAnimation = (AnimationC: Animation, navEl: HTMLElement, opts: TransitionOptions): Promise => { + const DURATION = 540; + const EASING = 'cubic-bezier(0.32,0.72,0,1)'; + const OPACITY = 'opacity'; + const TRANSFORM = 'transform'; + const TRANSLATEX = 'translateX'; + const CENTER = '0%'; + const OFF_OPACITY = 0.8; + + const backDirection = (opts.direction === 'back'); const isRTL = (navEl.ownerDocument as any).dir === 'rtl'; const OFF_RIGHT = isRTL ? '-99.5%' : '99.5%'; const OFF_LEFT = isRTL ? '33%' : '-33%'; const enteringEl = opts.enteringEl; const leavingEl = opts.leavingEl; + const contentEl = enteringEl.querySelector(':scope > ion-content'); + const headerEls = enteringEl.querySelectorAll(':scope > ion-header > *:not(ion-toolbar), :scope > ion-footer > *'); + const enteringToolBarEls = enteringEl.querySelectorAll(':scope > ion-header > ion-toolbar'); + const enteringContent = new AnimationC(); const rootTransition = new AnimationC(); rootTransition @@ -244,18 +319,12 @@ export const iosTransitionAnimation = (AnimationC: Animation, navEl: HTMLElement rootTransition.add(navDecor); } - const backDirection = (opts.direction === 'back'); - // setting up enter view - const contentEl = enteringEl.querySelector(':scope > ion-content'); - const headerEls = enteringEl.querySelectorAll(':scope > ion-header > *:not(ion-toolbar), :scope > ion-footer > *'); - const enteringToolBarEls = enteringEl.querySelectorAll(':scope > ion-header > ion-toolbar'); - const enteringContent = new AnimationC(); - if (!contentEl && enteringToolBarEls.length === 0 && headerEls.length === 0) { enteringContent.addElement(enteringEl.querySelector(':scope > .ion-page, :scope > ion-nav, :scope > ion-tabs')); } else { - enteringContent.addElement(contentEl); - enteringContent.addElement(headerEls); + enteringContent + .addElement(contentEl) + .addElement(headerEls); } rootTransition.add(enteringContent); @@ -270,28 +339,61 @@ export const iosTransitionAnimation = (AnimationC: Animation, navEl: HTMLElement enteringContent .beforeClearStyles([OPACITY]) .fromTo(TRANSLATEX, OFF_RIGHT, CENTER, true); + + if (contentEl) { + const enteringTransitionEffectEl = shadow(contentEl).querySelector('.transition-effect'); + + if (enteringTransitionEffectEl) { + const enteringTransitionCoverEl = enteringTransitionEffectEl.querySelector('.transition-cover'); + const enteringTransitionShadowEl = enteringTransitionEffectEl.querySelector('.transition-shadow'); + + const enteringTransitionEffect = new AnimationC(); + const enteringTransitionCover = new AnimationC(); + const enteringTransitionShadow = new AnimationC(); + + enteringTransitionEffect + .addElement(enteringTransitionEffectEl) + .beforeStyles({ opacity: '1' }) + .afterStyles({ opacity: '' }); + + enteringTransitionCover + .addElement(enteringTransitionCoverEl) + .beforeClearStyles([OPACITY]) + .fromTo(OPACITY, 0, 0.1, true); + + enteringTransitionShadow + .addElement(enteringTransitionShadowEl) + .beforeClearStyles([OPACITY]) + .fromTo(OPACITY, 0.70, 0.03, true); + + enteringContent + .add(enteringTransitionEffect) + .add(enteringTransitionCover) + .add(enteringTransitionShadow); + } + } } enteringToolBarEls.forEach(enteringToolBarEl => { const enteringToolBar = new AnimationC(); + const enteringTitle = new AnimationC(); + const enteringToolBarButtons = new AnimationC(); + const enteringToolBarItems = new AnimationC(); + const enteringToolBarBg = new AnimationC(); + const enteringBackButton = new AnimationC(); + const backButtonEl = enteringToolBarEl.querySelector('ion-back-button'); + enteringToolBar.addElement(enteringToolBarEl); rootTransition.add(enteringToolBar); - const enteringTitle = new AnimationC(); enteringTitle.addElement(enteringToolBarEl.querySelector('ion-title')); - const enteringToolBarButtons = new AnimationC(); enteringToolBarButtons.addElement(enteringToolBarEl.querySelectorAll('ion-buttons,[menuToggle]')); - const enteringToolBarItems = new AnimationC(); enteringToolBarItems.addElement(enteringToolBarEl.querySelectorAll(':scope > *:not(ion-title):not(ion-buttons):not([menuToggle])')); - const enteringToolBarBg = new AnimationC(); enteringToolBarBg.addElement(shadow(enteringToolBarEl).querySelector('.toolbar-background')); - const enteringBackButton = new AnimationC(); - const backButtonEl = enteringToolBarEl.querySelector('ion-back-button'); - if (backButtonEl) { enteringBackButton.addElement(backButtonEl); } @@ -340,10 +442,13 @@ export const iosTransitionAnimation = (AnimationC: Animation, navEl: HTMLElement // setup leaving view if (leavingEl) { - const leavingContent = new AnimationC(); - leavingContent.addElement(leavingEl.querySelector(':scope > ion-content')); - leavingContent.addElement(leavingEl.querySelectorAll(':scope > ion-header > *:not(ion-toolbar), :scope > ion-footer > *')); + const leavingContentEl = leavingEl.querySelector(':scope > ion-content'); + + leavingContent + .addElement(leavingContentEl) + .addElement(leavingEl.querySelectorAll(':scope > ion-header > *:not(ion-toolbar), :scope > ion-footer > *')); + rootTransition.add(leavingContent); if (backDirection) { @@ -352,6 +457,38 @@ export const iosTransitionAnimation = (AnimationC: Animation, navEl: HTMLElement .beforeClearStyles([OPACITY]) .fromTo(TRANSLATEX, CENTER, (isRTL ? '-100%' : '100%')); + if (leavingContentEl) { + const leavingTransitionEffectEl = shadow(leavingContentEl).querySelector('.transition-effect'); + if (leavingTransitionEffectEl) { + const leavingTransitionCoverEl = leavingTransitionEffectEl.querySelector('.transition-cover'); + const leavingTransitionShadowEl = leavingTransitionEffectEl.querySelector('.transition-shadow'); + + const leavingTransitionEffect = new AnimationC(); + const leavingTransitionCover = new AnimationC(); + const leavingTransitionShadow = new AnimationC(); + + leavingTransitionEffect + .addElement(leavingTransitionEffectEl) + .beforeStyles({ opacity: '1' }) + .afterStyles({ opacity: '' }); + + leavingTransitionCover + .addElement(leavingTransitionCoverEl) + .beforeClearStyles([OPACITY]) + .fromTo(OPACITY, 0.1, 0, true); + + leavingTransitionShadow + .addElement(leavingTransitionShadowEl) + .beforeClearStyles([OPACITY]) + .fromTo(OPACITY, 0.70, 0.03, true); + + leavingContent + .add(leavingTransitionEffect) + .add(leavingTransitionCover) + .add(leavingTransitionShadow); + } + } + } else { // leaving content, forward direction leavingContent @@ -362,25 +499,26 @@ export const iosTransitionAnimation = (AnimationC: Animation, navEl: HTMLElement const leavingToolBarEls = leavingEl.querySelectorAll(':scope > ion-header > ion-toolbar'); leavingToolBarEls.forEach(leavingToolBarEl => { const leavingToolBar = new AnimationC(); - leavingToolBar.addElement(leavingToolBarEl); - const leavingTitle = new AnimationC(); - leavingTitle.addElement(leavingToolBarEl.querySelector('ion-title')); - const leavingToolBarButtons = new AnimationC(); - leavingToolBarButtons.addElement(leavingToolBarEl.querySelectorAll('ion-buttons,[menuToggle]')); - const leavingToolBarItems = new AnimationC(); const leavingToolBarItemEls = leavingToolBarEl.querySelectorAll(':scope > *:not(ion-title):not(ion-buttons):not([menuToggle])'); + const leavingToolBarBg = new AnimationC(); + const leavingBackButton = new AnimationC(); + const backButtonEl = leavingToolBarEl.querySelector('ion-back-button'); + + leavingToolBar.addElement(leavingToolBarEl); + + leavingTitle.addElement(leavingToolBarEl.querySelector('ion-title')); + + leavingToolBarButtons.addElement(leavingToolBarEl.querySelectorAll('ion-buttons,[menuToggle]')); + if (leavingToolBarItemEls.length > 0) { leavingToolBarItems.addElement(leavingToolBarItemEls); } - const leavingToolBarBg = new AnimationC(); leavingToolBarBg.addElement(shadow(leavingToolBarEl).querySelector('.toolbar-background')); - const leavingBackButton = new AnimationC(); - const backButtonEl = leavingToolBarEl.querySelector('ion-back-button'); if (backButtonEl) { leavingBackButton.addElement(backButtonEl); } @@ -423,6 +561,7 @@ export const iosTransitionAnimation = (AnimationC: Animation, navEl: HTMLElement leavingTitle .fromTo(TRANSLATEX, CENTER, OFF_LEFT) .afterClearStyles([TRANSFORM]); + leavingToolBarItems .fromTo(TRANSLATEX, CENTER, OFF_LEFT) .afterClearStyles([TRANSFORM, OPACITY]); @@ -433,6 +572,7 @@ export const iosTransitionAnimation = (AnimationC: Animation, navEl: HTMLElement } }); } + // Return the rootTransition promise return Promise.resolve(rootTransition); }; diff --git a/core/src/utils/transition/md.transition.ts b/core/src/utils/transition/md.transition.ts index a50a1651b2..f33863f651 100644 --- a/core/src/utils/transition/md.transition.ts +++ b/core/src/utils/transition/md.transition.ts @@ -2,12 +2,11 @@ import { Animation } from '../../interface'; import { Animation as AnimationNew, createAnimation } from '../animation/animation'; import { TransitionOptions } from '../transition'; -const TRANSLATEY = 'translateY'; -const OFF_BOTTOM = '40px'; -const CENTER = '0px'; - export const newMdTransitionAnimation = (opts: TransitionOptions): AnimationNew => { try { + const OFF_BOTTOM = '40px'; + const CENTER = '0px'; + const rootAnimation = createAnimation('md-root-animation'); const enteringEl = opts.enteringEl; @@ -61,18 +60,21 @@ export const newMdTransitionAnimation = (opts: TransitionOptions): AnimationNew }; export const mdTransitionAnimation = (AnimationC: Animation, _: HTMLElement, opts: TransitionOptions): Promise => { + const TRANSLATEY = 'translateY'; + const OFF_BOTTOM = '40px'; + const CENTER = '0px'; + const backDirection = (opts.direction === 'back'); const enteringEl = opts.enteringEl; const leavingEl = opts.leavingEl; const ionPageElement = getIonPageElement(enteringEl); - + const enteringToolbarEle = ionPageElement.querySelector('ion-toolbar'); const rootTransition = new AnimationC(); + rootTransition .addElement(ionPageElement) .beforeRemoveClass('ion-page-invisible'); - const backDirection = (opts.direction === 'back'); - // animate the component itself if (backDirection) { rootTransition @@ -88,7 +90,6 @@ export const mdTransitionAnimation = (AnimationC: Animation, _: HTMLElement, opt } // Animate toolbar if it's there - const enteringToolbarEle = ionPageElement.querySelector('ion-toolbar'); if (enteringToolbarEle) { const enteringToolBar = new AnimationC(); enteringToolBar.addElement(enteringToolbarEle); @@ -118,6 +119,7 @@ const getIonPageElement = (element: HTMLElement) => { if (element.classList.contains('ion-page')) { return element; } + const ionPage = element.querySelector(':scope > .ion-page, :scope > ion-nav, :scope > ion-tabs'); if (ionPage) { return ionPage;