diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 11ee81fad1..192ef71ffd 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -1427,7 +1427,7 @@ export namespace Components { * @param name The name of the animation to register. * @param animation The animation function to register. */ - 'registerAnimation': (name: string, animation: ((menu: MenuI) => IonicAnimation) | AnimationBuilder) => Promise; + 'registerAnimation': (name: string, animation: AnimationBuilder | ((menu: MenuI) => IonicAnimation)) => Promise; /** * Enable or disable the ability to swipe open the menu. * @param enable If `true`, the menu swipe gesture should be enabled. diff --git a/core/src/components/menu/menu.tsx b/core/src/components/menu/menu.tsx index 7ca614fc83..f7bb234794 100644 --- a/core/src/components/menu/menu.tsx +++ b/core/src/components/menu/menu.tsx @@ -314,10 +314,11 @@ export class Menu implements ComponentInterface, MenuI { private async startAnimation(shouldOpen: boolean, animated: boolean): Promise { const isReversed = !shouldOpen; - console.log('play', isReversed); const ani = this.animation! + .fill('both') .direction((isReversed) ? 'reverse' : 'normal') - .easing((isReversed) ? 'cubic-bezier(0.4, 0.0, 0.6, 1)' : 'cubic-bezier(0.0, 0.0, 0.2, 1)'); + .easing((isReversed) ? 'cubic-bezier(0.4, 0.0, 0.6, 1)' : 'cubic-bezier(0.0, 0.0, 0.2, 1)') + .update(true); if (animated) { await ani.playAsync(); diff --git a/core/src/utils/animation/animation-interface.ts b/core/src/utils/animation/animation-interface.ts index 347278ff37..9d4999090b 100644 --- a/core/src/utils/animation/animation-interface.ts +++ b/core/src/utils/animation/animation-interface.ts @@ -30,6 +30,7 @@ export interface Animation { easing(easing: string): Animation; delay(delay: number): Animation; parent(animation: Animation): Animation; + update(deep: boolean): Animation; getKeyframes(): any[]; getDirection(): AnimationDirection | undefined; diff --git a/core/src/utils/animation/animation.ts b/core/src/utils/animation/animation.ts index 6801e04496..3e1b5c0aba 100644 --- a/core/src/utils/animation/animation.ts +++ b/core/src/utils/animation/animation.ts @@ -35,6 +35,7 @@ export const createAnimation = () => { let shouldForceSyncPlayback = false; let shouldForceReverseDirection = false; let willComplete = true; + let shouldCalculateNumAnimations = true; let ani: Animation; /** @@ -534,6 +535,8 @@ export const createAnimation = () => { onFinishCallbacks.forEach(callback => { callback(didComplete, ani); }); + + shouldCalculateNumAnimations = true; }; const animationFinish = () => { @@ -605,13 +608,12 @@ export const createAnimation = () => { animationFinish(); }; } + }; const initializeAnimation = () => { beforeAnimation(); - numAnimationsRunning = childAnimations.length + 1; - if (getKeyframes().length > 0) { if (supportsWebAnimations) { initializeWebAnimation(); @@ -682,12 +684,23 @@ export const createAnimation = () => { }); }; - const updateAnimation = () => { + /** + * Updates any existing animations. + */ + const update = (deep = false) => { + if (deep) { + childAnimations.forEach(animation => { + animation.update(deep); + }); + } + if (supportsWebAnimations) { updateWebAnimation(); } else { updateCSSAnimation(); } + + return ani; }; const progressStart = (forceLinearEasing = false) => { @@ -713,7 +726,7 @@ export const createAnimation = () => { if (!shouldComplete) { shouldForceReverseDirection = true; onFinish(() => { willComplete = true; shouldForceReverseDirection = false; }); - updateAnimation(); + update(); progressStep(1 - step); } @@ -783,6 +796,11 @@ export const createAnimation = () => { initializeAnimation(); } + if (shouldCalculateNumAnimations) { + numAnimationsRunning = childAnimations.length + 1; + shouldCalculateNumAnimations = false; + } + childAnimations.forEach(animation => { animation.play(); }); @@ -890,6 +908,7 @@ export const createAnimation = () => { keyframes, addAnimation, addElement, + update, fill, direction, iterations, diff --git a/core/src/utils/animation/test/animationbuilder/e2e.ts b/core/src/utils/animation/test/animationbuilder/e2e.ts index ffdde09f65..daa4c26d49 100644 --- a/core/src/utils/animation/test/animationbuilder/e2e.ts +++ b/core/src/utils/animation/test/animationbuilder/e2e.ts @@ -1,25 +1,72 @@ import { newE2EPage } from '@stencil/core/testing'; +import { listenForEvent, waitForFunctionTestContext } from '../../../test/utils'; + const navChanged = () => new Promise(resolve => window.addEventListener('ionRouteDidChange', resolve)); +const ROUTE_CHANGED = 'onRouteChanged'; test('animation:backwards-compatibility animationbuilder', async () => { - const page = await newE2EPage({ url: '/src/utils/animation/test/animationbuilder?ionic:_testing=true' }); + const page = await newE2EPage({ url: '/src/utils/animation/test/animationbuilder?ionic:_testing=true&_forceAnimationBuilder=true' }); const screenshotCompares = []; + const body = await page.$('body'); + await listenForEvent(page, 'ionRouteDidChange', body, ROUTE_CHANGED); + const routeChangedCount: any = { count: 0 }; + await page.exposeFunction(ROUTE_CHANGED, () => { + routeChangedCount.count += 1; + }); + screenshotCompares.push(await page.compareScreenshot()); page.click('page-root ion-button.next'); - await page.waitFor(navChanged); + await waitForNavChange(page, routeChangedCount); page.click('page-one ion-button.next'); - await page.waitFor(navChanged); + await waitForNavChange(page, routeChangedCount); page.click('page-two ion-button.next'); - await page.waitFor(navChanged); + await waitForNavChange(page, routeChangedCount); page.click('page-three ion-back-button'); - await page.waitFor(navChanged); + await waitForNavChange(page, routeChangedCount); page.click('page-two ion-back-button'); - await page.waitFor(navChanged); + await waitForNavChange(page, routeChangedCount); page.click('page-one ion-back-button'); - await page.waitFor(navChanged); + await waitForNavChange(page, routeChangedCount); screenshotCompares.push(await page.compareScreenshot()); }); + +test('animation:backwards-compatibility animation', async () => { + const page = await newE2EPage({ url: '/src/utils/animation/test/animationbuilder?ionic:_testing=true' }); + const screenshotCompares = []; + + const body = await page.$('body'); + await listenForEvent(page, 'ionRouteDidChange', body, ROUTE_CHANGED); + const routeChangedCount: any = { count: 0 }; + await page.exposeFunction(ROUTE_CHANGED, () => { + routeChangedCount.count += 1; + }); + + screenshotCompares.push(await page.compareScreenshot()); + + page.click('page-root ion-button.next'); + await waitForNavChange(page, routeChangedCount); + page.click('page-one ion-button.next'); + await waitForNavChange(page, routeChangedCount); + page.click('page-two ion-button.next'); + await waitForNavChange(page, routeChangedCount); + page.click('page-three ion-back-button'); + await waitForNavChange(page, routeChangedCount); + page.click('page-two ion-back-button'); + await waitForNavChange(page, routeChangedCount); + page.click('page-one ion-back-button'); + await waitForNavChange(page, routeChangedCount); + + screenshotCompares.push(await page.compareScreenshot()); +}); + +const waitForNavChange = async (page, routeChangedCount) => { + await waitForFunctionTestContext((payload: any) => { + return payload.routeChangedCount.count === 1; + }, { routeChangedCount }); + + routeChangedCount.count = 0; +}; diff --git a/core/src/utils/animation/test/animationbuilder/index.html b/core/src/utils/animation/test/animationbuilder/index.html index 2898bbd40c..077d5cb8e9 100644 --- a/core/src/utils/animation/test/animationbuilder/index.html +++ b/core/src/utils/animation/test/animationbuilder/index.html @@ -93,62 +93,65 @@ customElements.define('page-two', PageTwo); customElements.define('page-three', PageThree); - - window.Ionic.config.navAnimation = (AnimationC, navEl, opts) => { - 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'); - - // animate the component itself - if (backDirection) { + const forceAnimationBuilder = new URLSearchParams(window.location.search).get('ionic:_forceAnimationBuilder'); + if (forceAnimationBuilder) { + window.Ionic.config.navAnimation = (AnimationC, navEl, opts) => { + 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 - .duration(opts.duration || 200) - .easing('cubic-bezier(0.47,0,0.745,0.715)'); + .addElement(ionPageElement) + .beforeRemoveClass('ion-page-invisible'); + + // animate the component itself + if (backDirection) { + rootTransition + .duration(opts.duration || 200) + .easing('cubic-bezier(0.47,0,0.745,0.715)'); + + } else { + rootTransition + .duration(opts.duration || 280) + .easing('cubic-bezier(0.36,0.66,0.04,1)') + .fromTo(TRANSLATEY, OFF_BOTTOM, CENTER, true) + .fromTo('opacity', 0.01, 1, true); + } + + // Animate toolbar if it's there + if (enteringToolbarEle) { + const enteringToolBar = new AnimationC(); + enteringToolBar.addElement(enteringToolbarEle); + rootTransition.add(enteringToolBar); + } + + // setup leaving view + if (leavingEl && backDirection) { + // leaving content + rootTransition + .duration(opts.duration || 200) + .easing('cubic-bezier(0.47,0,0.745,0.715)'); + + const leavingPage = new AnimationC(); + leavingPage + .addElement(getIonPageElement(leavingEl)) + .fromTo(TRANSLATEY, CENTER, OFF_BOTTOM) + .fromTo('opacity', 1, 0); + + rootTransition.add(leavingPage); + } + + return Promise.resolve(rootTransition); + }; + } - } else { - rootTransition - .duration(opts.duration || 280) - .easing('cubic-bezier(0.36,0.66,0.04,1)') - .fromTo(TRANSLATEY, OFF_BOTTOM, CENTER, true) - .fromTo('opacity', 0.01, 1, true); - } - - // Animate toolbar if it's there - if (enteringToolbarEle) { - const enteringToolBar = new AnimationC(); - enteringToolBar.addElement(enteringToolbarEle); - rootTransition.add(enteringToolBar); - } - - // setup leaving view - if (leavingEl && backDirection) { - // leaving content - rootTransition - .duration(opts.duration || 200) - .easing('cubic-bezier(0.47,0,0.745,0.715)'); - - const leavingPage = new AnimationC(); - leavingPage - .addElement(getIonPageElement(leavingEl)) - .fromTo(TRANSLATEY, CENTER, OFF_BOTTOM) - .fromTo('opacity', 1, 0); - - rootTransition.add(leavingPage); - } - - return Promise.resolve(rootTransition); - }; const getIonPageElement = (element) => { if (element.classList.contains('ion-page')) { return element;