diff --git a/BREAKING.md b/BREAKING.md index a93fca71fc..a8ce6b1a82 100644 --- a/BREAKING.md +++ b/BREAKING.md @@ -60,8 +60,11 @@ This section details the desktop browser, JavaScript framework, and mobile platf | iOS | 15+ | | Android | 5.1+ with Chromium 89+ | +Ionic Framework v8 removes backwards support for CSS Animations in favor of the [Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API). All minimum browser versions listed above support the Web Animations API. +

Dark Mode

+ In previous versions, it was recommended to define the dark palette in the following way: ```css diff --git a/core/src/utils/animation/animation-utils.ts b/core/src/utils/animation/animation-utils.ts index 0e8a6fde6f..bfc5e1b2f9 100644 --- a/core/src/utils/animation/animation-utils.ts +++ b/core/src/utils/animation/animation-utils.ts @@ -1,41 +1,5 @@ -import type { AnimationKeyFrames } from './animation-interface'; - let animationPrefix: string | undefined; -/** - * Web Animations requires hyphenated CSS properties - * to be written in camelCase when animating - */ -export const processKeyframes = (keyframes: AnimationKeyFrames) => { - keyframes.forEach((keyframe) => { - for (const key in keyframe) { - // eslint-disable-next-line no-prototype-builtins - if (keyframe.hasOwnProperty(key)) { - const value = keyframe[key]; - - if (key === 'easing') { - const newKey = 'animation-timing-function'; - keyframe[newKey] = value; - delete keyframe[key]; - } else { - const newKey = convertCamelCaseToHypen(key); - - if (newKey !== key) { - keyframe[newKey] = value; - delete keyframe[key]; - } - } - } - } - }); - - return keyframes; -}; - -const convertCamelCaseToHypen = (str: string) => { - return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase(); -}; - export const getAnimationPrefix = (el: HTMLElement): string => { if (animationPrefix === undefined) { const supportsUnprefixed = el.style.animationName !== undefined; @@ -50,99 +14,6 @@ export const setStyleProperty = (element: HTMLElement, propertyName: string, val element.style.setProperty(prefix + propertyName, value); }; -export const removeStyleProperty = (element: HTMLElement, propertyName: string) => { - const prefix = propertyName.startsWith('animation') ? getAnimationPrefix(element) : ''; - element.style.removeProperty(prefix + propertyName); -}; - -export const animationEnd = (el: HTMLElement | null, callback: (ev?: TransitionEvent) => void) => { - let unRegTrans: (() => void) | undefined; - const opts: AddEventListenerOptions = { passive: true }; - - const unregister = () => { - if (unRegTrans) { - unRegTrans(); - } - }; - - const onTransitionEnd = (ev: Event) => { - if (el === ev.target) { - unregister(); - callback(ev as TransitionEvent); - } - }; - - if (el) { - el.addEventListener('webkitAnimationEnd', onTransitionEnd, opts); - el.addEventListener('animationend', onTransitionEnd, opts); - - unRegTrans = () => { - el.removeEventListener('webkitAnimationEnd', onTransitionEnd, opts); - el.removeEventListener('animationend', onTransitionEnd, opts); - }; - } - - return unregister; -}; - -// TODO(FW-2832): type -export const generateKeyframeRules = (keyframes: any[] = []) => { - return keyframes - .map((keyframe) => { - const offset = keyframe.offset; - - const frameString = []; - for (const property in keyframe) { - // eslint-disable-next-line no-prototype-builtins - if (keyframe.hasOwnProperty(property) && property !== 'offset') { - frameString.push(`${property}: ${keyframe[property]};`); - } - } - - return `${offset * 100}% { ${frameString.join(' ')} }`; - }) - .join(' '); -}; - -const keyframeIds: string[] = []; - -export const generateKeyframeName = (keyframeRules: string) => { - let index = keyframeIds.indexOf(keyframeRules); - if (index < 0) { - index = keyframeIds.push(keyframeRules) - 1; - } - return `ion-animation-${index}`; -}; - -export const getStyleContainer = (element: HTMLElement) => { - // getRootNode is not always available in SSR environments. - // TODO(FW-2832): types - const rootNode = element.getRootNode !== undefined ? (element.getRootNode() as any) : element; - return rootNode.head || rootNode; -}; - -export const createKeyframeStylesheet = ( - keyframeName: string, - keyframeRules: string, - element: HTMLElement -): HTMLElement => { - const styleContainer = getStyleContainer(element); - const keyframePrefix = getAnimationPrefix(element); - - const existingStylesheet = styleContainer.querySelector('#' + keyframeName); - if (existingStylesheet) { - return existingStylesheet; - } - - const stylesheet = (element.ownerDocument ?? document).createElement('style'); - stylesheet.id = keyframeName; - stylesheet.textContent = `@${keyframePrefix}keyframes ${keyframeName} { ${keyframeRules} } @${keyframePrefix}keyframes ${keyframeName}-alt { ${keyframeRules} }`; - - styleContainer.appendChild(stylesheet); - - return stylesheet; -}; - export const addClassToArray = (classes: string[] = [], className: string | string[] | undefined): string[] => { if (className !== undefined) { const classNameToAppend = Array.isArray(className) ? className : [className]; diff --git a/core/src/utils/animation/animation.ts b/core/src/utils/animation/animation.ts index a2376c312a..c7c57b9f87 100644 --- a/core/src/utils/animation/animation.ts +++ b/core/src/utils/animation/animation.ts @@ -1,5 +1,4 @@ import { win } from '../browser'; -import { raf } from '../helpers'; import type { Animation, @@ -12,16 +11,7 @@ import type { AnimationLifecycle, AnimationPlayOptions, } from './animation-interface'; -import { - addClassToArray, - animationEnd, - createKeyframeStylesheet, - generateKeyframeName, - generateKeyframeRules, - processKeyframes, - removeStyleProperty, - setStyleProperty, -} from './animation-utils'; +import { addClassToArray, setStyleProperty } from './animation-utils'; // TODO(FW-2832): types @@ -64,7 +54,6 @@ export const createAnimation = (animationId?: string): Animation => { let willComplete = true; let finished = false; let shouldCalculateNumAnimations = true; - let keyframeName: string | undefined; let ani: Animation; let paused = false; @@ -83,11 +72,17 @@ export const createAnimation = (animationId?: string): Animation => { const supportsAnimationEffect = typeof (AnimationEffect as any) === 'function' || (win !== undefined && typeof (win as any).AnimationEffect === 'function'); + /** + * This is a feature detection for Web Animations. + * + * Certain environments such as emulated browser environments for testing, + * do not support Web Animations. As a result, we need to check for support + * and provide a fallback to test certain functionality related to Web Animations. + */ const supportsWebAnimations = typeof (Element as any) === 'function' && typeof (Element as any).prototype!.animate === 'function' && supportsAnimationEffect; - const ANIMATION_END_FALLBACK_PADDING_MS = 100; const getWebAnimations = () => { return webAnimations; @@ -198,21 +193,6 @@ export const createAnimation = (animationId?: string): Animation => { }); webAnimations.length = 0; - } else { - const elementsArray = elements.slice(); - - raf(() => { - elementsArray.forEach((element) => { - removeStyleProperty(element, 'animation-name'); - removeStyleProperty(element, 'animation-duration'); - removeStyleProperty(element, 'animation-timing-function'); - removeStyleProperty(element, 'animation-iteration-count'); - removeStyleProperty(element, 'animation-delay'); - removeStyleProperty(element, 'animation-play-state'); - removeStyleProperty(element, 'animation-fill-mode'); - removeStyleProperty(element, 'animation-direction'); - }); - }); } }; @@ -533,8 +513,6 @@ export const createAnimation = (animationId?: string): Animation => { animation.effect = newEffect; } }); - } else { - initializeCSSAnimation(); } }; @@ -643,39 +621,6 @@ export const createAnimation = (animationId?: string): Animation => { } }; - const initializeCSSAnimation = (toggleAnimationName = true) => { - cleanUpStyleSheets(); - - const processedKeyframes = processKeyframes(_keyframes); - elements.forEach((element) => { - if (processedKeyframes.length > 0) { - const keyframeRules = generateKeyframeRules(processedKeyframes); - keyframeName = animationId !== undefined ? animationId : generateKeyframeName(keyframeRules); - const stylesheet = createKeyframeStylesheet(keyframeName, keyframeRules, element); - stylesheets.push(stylesheet); - - setStyleProperty(element, 'animation-duration', `${getDuration()}ms`); - setStyleProperty(element, 'animation-timing-function', getEasing()); - setStyleProperty(element, 'animation-delay', `${getDelay()}ms`); - setStyleProperty(element, 'animation-fill-mode', getFill()); - setStyleProperty(element, 'animation-direction', getDirection()); - - const iterationsCount = getIterations() === Infinity ? 'infinite' : getIterations().toString(); - - setStyleProperty(element, 'animation-iteration-count', iterationsCount); - setStyleProperty(element, 'animation-play-state', 'paused'); - - if (toggleAnimationName) { - setStyleProperty(element, 'animation-name', `${stylesheet.id}-alt`); - } - - raf(() => { - setStyleProperty(element, 'animation-name', stylesheet.id || null); - }); - } - }); - }; - const initializeWebAnimation = () => { elements.forEach((element) => { const animation = element.animate(_keyframes, { @@ -700,14 +645,12 @@ export const createAnimation = (animationId?: string): Animation => { } }; - const initializeAnimation = (toggleAnimationName = true) => { + const initializeAnimation = () => { beforeAnimation(); if (_keyframes.length > 0) { if (supportsWebAnimations) { initializeWebAnimation(); - } else { - initializeCSSAnimation(toggleAnimationName); } } @@ -722,15 +665,6 @@ export const createAnimation = (animationId?: string): Animation => { animation.currentTime = animation.effect!.getComputedTiming().delay! + getDuration() * step; animation.pause(); }); - } else { - const animationDuration = `-${getDuration() * step}ms`; - - elements.forEach((element) => { - if (_keyframes.length > 0) { - setStyleProperty(element, 'animation-delay', animationDuration); - setStyleProperty(element, 'animation-play-state', 'paused'); - } - }); } }; @@ -751,35 +685,6 @@ export const createAnimation = (animationId?: string): Animation => { } }; - const updateCSSAnimation = (toggleAnimationName = true, step?: number) => { - raf(() => { - elements.forEach((element) => { - setStyleProperty(element, 'animation-name', keyframeName || null); - setStyleProperty(element, 'animation-duration', `${getDuration()}ms`); - setStyleProperty(element, 'animation-timing-function', getEasing()); - setStyleProperty( - element, - 'animation-delay', - step !== undefined ? `-${step! * getDuration()}ms` : `${getDelay()}ms` - ); - setStyleProperty(element, 'animation-fill-mode', getFill() || null); - setStyleProperty(element, 'animation-direction', getDirection() || null); - - const iterationsCount = getIterations() === Infinity ? 'infinite' : getIterations().toString(); - - setStyleProperty(element, 'animation-iteration-count', iterationsCount); - - if (toggleAnimationName) { - setStyleProperty(element, 'animation-name', `${keyframeName}-alt`); - } - - raf(() => { - setStyleProperty(element, 'animation-name', keyframeName || null); - }); - }); - }); - }; - const update = (deep = false, toggleAnimationName = true, step?: number) => { if (deep) { childAnimations.forEach((animation) => { @@ -789,8 +694,6 @@ export const createAnimation = (animationId?: string): Animation => { if (supportsWebAnimations) { updateWebAnimation(step); - } else { - updateCSSAnimation(toggleAnimationName, step); } return ani; @@ -892,11 +795,6 @@ export const createAnimation = (animationId?: string): Animation => { return ani; }; - const onAnimationEndFallback = () => { - cssAnimationsTimerFallback = undefined; - animationFinish(); - }; - const clearCSSAnimationsTimeout = () => { if (cssAnimationsTimerFallback) { clearTimeout(cssAnimationsTimerFallback); @@ -906,63 +804,7 @@ export const createAnimation = (animationId?: string): Animation => { const playCSSAnimations = () => { clearCSSAnimationsTimeout(); - raf(() => { - elements.forEach((element) => { - if (_keyframes.length > 0) { - setStyleProperty(element, 'animation-play-state', 'running'); - } - }); - }); - - if (_keyframes.length === 0 || elements.length === 0) { - animationFinish(); - } else { - /** - * This is a catchall in the event that a CSS Animation did not finish. - * The Web Animations API has mechanisms in place for preventing this. - * CSS Animations will not fire an `animationend` event - * for elements with `display: none`. The Web Animations API - * accounts for this, but using raw CSS Animations requires - * this workaround. - */ - const animationDelay = getDelay() || 0; - const animationDuration = getDuration() || 0; - const animationIterations = getIterations() || 1; - - // No need to set a timeout when animation has infinite iterations - if (isFinite(animationIterations)) { - cssAnimationsTimerFallback = setTimeout( - onAnimationEndFallback, - animationDelay + animationDuration * animationIterations + ANIMATION_END_FALLBACK_PADDING_MS - ); - } - - animationEnd(elements[0], () => { - clearCSSAnimationsTimeout(); - - /** - * Ensure that clean up - * is always done a frame - * before the onFinish handlers - * are fired. Otherwise, there - * may be flickering if a new - * animation is started on the same - * element too quickly - */ - raf(() => { - clearCSSAnimationPlayState(); - raf(animationFinish); - }); - }); - } - }; - - const clearCSSAnimationPlayState = () => { - elements.forEach((element) => { - removeStyleProperty(element, 'animation-duration'); - removeStyleProperty(element, 'animation-delay'); - removeStyleProperty(element, 'animation-play-state'); - }); + animationFinish(); }; const playWebAnimations = () => { @@ -979,8 +821,6 @@ export const createAnimation = (animationId?: string): Animation => { if (supportsWebAnimations) { setAnimationStep(0); updateWebAnimation(); - } else { - updateCSSAnimation(); } }; diff --git a/core/src/utils/animation/test/animation.spec.ts b/core/src/utils/animation/test/animation.spec.ts index 4e485782ef..7156d7097c 100644 --- a/core/src/utils/animation/test/animation.spec.ts +++ b/core/src/utils/animation/test/animation.spec.ts @@ -1,6 +1,5 @@ import { createAnimation } from '../animation'; import type { Animation } from '../animation-interface'; -import { processKeyframes } from '../animation-utils'; import { getTimeGivenProgression } from '../cubic-bezier'; describe('Animation Class', () => { @@ -53,104 +52,6 @@ describe('Animation Class', () => { animation.play(); expect(animation.isRunning()).toEqual(false); }); - - it('should be running', () => { - const el = document.createElement('div'); - animation.addElement(el); - animation.keyframes([ - { transform: 'scale(1)', opacity: 1, offset: 0 }, - { transform: 'scale(0)', opacity: 0, offset: 1 }, - ]); - animation.duration(250); - - animation.play(); - expect(animation.isRunning()).toEqual(true); - }); - - it('should not be running after finishing the animation', async () => { - const el = document.createElement('div'); - animation.addElement(el); - animation.keyframes([ - { transform: 'scale(1)', opacity: 1, offset: 0 }, - { transform: 'scale(0)', opacity: 0, offset: 1 }, - ]); - animation.duration(250); - - await animation.play(); - - expect(animation.isRunning()).toEqual(false); - }); - - it('should not be running after calling pause', () => { - const el = document.createElement('div'); - animation.addElement(el); - animation.keyframes([ - { transform: 'scale(1)', opacity: 1, offset: 0 }, - { transform: 'scale(0)', opacity: 0, offset: 1 }, - ]); - animation.duration(250); - - animation.play(); - expect(animation.isRunning()).toEqual(true); - - animation.pause(); - expect(animation.isRunning()).toEqual(false); - }); - - it('should not be running when doing progress steps', () => { - const el = document.createElement('div'); - animation.addElement(el); - animation.keyframes([ - { transform: 'scale(1)', opacity: 1, offset: 0 }, - { transform: 'scale(0)', opacity: 0, offset: 1 }, - ]); - animation.duration(250); - - animation.play(); - - animation.progressStart(); - - expect(animation.isRunning()).toEqual(false); - }); - - it('should be running after calling progressEnd', () => { - const el = document.createElement('div'); - animation.addElement(el); - animation.keyframes([ - { transform: 'scale(1)', opacity: 1, offset: 0 }, - { transform: 'scale(0)', opacity: 0, offset: 1 }, - ]); - animation.duration(250); - - animation.play(); - - animation.progressStart(); - animation.progressEnd(1, 0); - - expect(animation.isRunning()).toEqual(true); - }); - - it('should not be running after playing to beginning', async () => { - const el = document.createElement('div'); - animation.addElement(el); - animation.keyframes([ - { transform: 'scale(1)', opacity: 1, offset: 0 }, - { transform: 'scale(0)', opacity: 0, offset: 1 }, - ]); - animation.duration(250); - - await animation.play(); - - animation.progressStart(); - animation.progressEnd(0, 0); - - await new Promise((resolve) => { - animation.onFinish(() => { - expect(animation.isRunning()).toEqual(false); - resolve(); - }); - }); - }); }); describe('addElement()', () => { @@ -228,18 +129,6 @@ describe('Animation Class', () => { expect(animation.getKeyframes().length).toEqual(3); }); - it('should convert properties for CSS Animations', () => { - const processedKeyframes = processKeyframes([ - { borderRadius: '0px', easing: 'ease-in', offset: 0 }, - { borderRadius: '4px', easing: 'ease-out', offset: 1 }, - ]); - - expect(processedKeyframes).toEqual([ - { 'border-radius': '0px', 'animation-timing-function': 'ease-in', offset: 0 }, - { 'border-radius': '4px', 'animation-timing-function': 'ease-out', offset: 1 }, - ]); - }); - it('should set the from keyframe properly', () => { animation.from('opacity', 0).from('background', 'red').from('color', 'purple'); diff --git a/core/src/utils/animation/test/animationbuilder/animation.e2e.ts b/core/src/utils/animation/test/animationbuilder/animation.e2e.ts index 96ea221853..a37b7b0694 100644 --- a/core/src/utils/animation/test/animationbuilder/animation.e2e.ts +++ b/core/src/utils/animation/test/animationbuilder/animation.e2e.ts @@ -12,11 +12,6 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) => await page.goto('/src/utils/animation/test/animationbuilder', config); await testNavigation(page); }); - - test('ios-transition css', async ({ page }) => { - await page.goto('/src/utils/animation/test/animationbuilder?ionic:_forceCSSAnimations=true', config); - await testNavigation(page); - }); }); }); diff --git a/core/src/utils/animation/test/animationbuilder/index.html b/core/src/utils/animation/test/animationbuilder/index.html index a61fa92ee7..795b95efeb 100644 --- a/core/src/utils/animation/test/animationbuilder/index.html +++ b/core/src/utils/animation/test/animationbuilder/index.html @@ -13,11 +13,6 @@