From 53179c475cdf0dd5f1cfda0eef7cf724f9b4722b Mon Sep 17 00:00:00 2001 From: Manu MA Date: Wed, 17 Jul 2019 17:46:22 +0200 Subject: [PATCH 1/5] fix(inputs): apply ng form classes (#18820) --- angular/src/app-initialize.ts | 7 ++----- .../control-value-accessors/value-accessor.ts | 18 +++++++++++------- angular/src/util/util.ts | 5 +++++ 3 files changed, 18 insertions(+), 12 deletions(-) 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/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() From 978cc39009a9a0fb065540ce17e10c685b6c101a Mon Sep 17 00:00:00 2001 From: Nico L Date: Wed, 17 Jul 2019 17:54:19 +0200 Subject: [PATCH 2/5] =?UTF-8?q?fix(hardwareBackButton):=20added=20missing?= =?UTF-8?q?=20import=20of=20hardware=20back=20button=E2=80=A6=20(#18794)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/src/components/app/app.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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()); }); From 26e6d6f11518cfa925fe1817b1f8be84d1f859a1 Mon Sep 17 00:00:00 2001 From: Manu MA Date: Wed, 17 Jul 2019 19:23:13 +0200 Subject: [PATCH 3/5] fix(textarea): autogrow (#18822) fixes #18744 --- core/src/components/textarea/textarea.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/src/components/textarea/textarea.tsx b/core/src/components/textarea/textarea.tsx index 21aa541952..17c921e208 100644 --- a/core/src/components/textarea/textarea.tsx +++ b/core/src/components/textarea/textarea.tsx @@ -193,10 +193,12 @@ export class Textarea implements ComponentInterface { this.ionInputDidLoad.emit(); } + // TODO: performance hit, this cause layout thrashing private runAutoGrow() { const nativeInput = this.nativeInput; if (nativeInput && this.autoGrow) { readTask(() => { + nativeInput.style.height = 'inherit'; nativeInput.style.height = nativeInput.scrollHeight + 'px'; }); } From 97fec9236565bdd4eb84c2442645fac6c3ecdcab Mon Sep 17 00:00:00 2001 From: Manu MA Date: Thu, 18 Jul 2019 10:26:54 +0200 Subject: [PATCH 4/5] fix(router-outlet): attach entering view before first change detection (#18821) --- .../navigation/ion-router-outlet.ts | 1 - .../directives/navigation/stack-controller.ts | 44 ++++++++++------ angular/src/providers/angular-delegate.ts | 33 ++++++------ .../test-app/e2e/src/router-link.e2e-spec.ts | 50 +++++++++++-------- angular/test/test-app/e2e/src/utils.ts | 18 +++++-- .../src/app/alert/alert.component.html | 4 +- .../test-app/src/app/alert/alert.component.ts | 7 +++ .../router-link-page.component.html | 1 + .../router-link-page.component.ts | 13 +++++ core/scripts/swiper.rollup.config.js | 4 +- 10 files changed, 112 insertions(+), 63 deletions(-) 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/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 From 9b075ef52968435a37487ae742e2afeed9fb4b12 Mon Sep 17 00:00:00 2001 From: Adam Bradley Date: Thu, 18 Jul 2019 14:50:56 -0500 Subject: [PATCH 5/5] feat(transition): iOS page transition shadow (#18695) Closes #18661 --- core/scripts/testing/scripts.js | 4 + core/src/components/content/content.scss | 44 ++++++ core/src/components/content/content.tsx | 12 +- .../components/content/test/basic/index.html | 2 +- core/src/utils/config.ts | 5 + core/src/utils/transition/ios.transition.ts | 142 +++++++++++++----- core/src/utils/transition/md.transition.ts | 15 +- 7 files changed, 178 insertions(+), 46 deletions(-) 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/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/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 607bbeea9d..919b3ec4b1 100644 --- a/core/src/utils/transition/ios.transition.ts +++ b/core/src/utils/transition/ios.transition.ts @@ -1,26 +1,31 @@ import { Animation } from '../../interface'; 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 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 @@ -37,18 +42,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); @@ -63,28 +62,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); } @@ -133,10 +165,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) { @@ -145,6 +180,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 @@ -155,25 +222,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); } @@ -216,6 +284,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]); @@ -226,6 +295,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 ae7f0cd680..bb256e7492 100644 --- a/core/src/utils/transition/md.transition.ts +++ b/core/src/utils/transition/md.transition.ts @@ -1,23 +1,22 @@ import { Animation } from '../../interface'; import { TransitionOptions } from '../transition'; -const TRANSLATEY = 'translateY'; -const OFF_BOTTOM = '40px'; -const CENTER = '0px'; - 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 @@ -33,7 +32,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); @@ -63,6 +61,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;