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 @@