Compare commits

...

3 Commits

Author SHA1 Message Date
Liam DeBeasi
74a33e7b8e test: remove css animations e2e tests 2024-02-13 08:49:11 -05:00
Sean Perkins
c91bd63ed5 docs: breaking change 2024-02-12 20:30:19 -05:00
Sean Perkins
905e98c546 feat: remove css animations support for ionic animations 2024-02-12 20:25:23 -05:00
15 changed files with 50 additions and 466 deletions

View File

@@ -51,6 +51,8 @@ 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.
<h2 id="version-8x-dark-theme">Dark Theme</h2>
In previous versions, it was recommended to define the dark theme in the following way:

View File

@@ -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,70 +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
@@ -121,28 +21,6 @@ export const getStyleContainer = (element: HTMLElement) => {
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];

View File

@@ -1,6 +1,3 @@
import { win } from '../browser';
import { raf } from '../helpers';
import type {
Animation,
AnimationCallbackOptions,
@@ -12,18 +9,7 @@ import type {
AnimationLifecycle,
AnimationPlayOptions,
} from './animation-interface';
import {
addClassToArray,
animationEnd,
createKeyframeStylesheet,
generateKeyframeName,
generateKeyframeRules,
processKeyframes,
removeStyleProperty,
setStyleProperty,
} from './animation-utils';
// TODO(FW-2832): types
import { addClassToArray, setStyleProperty } from './animation-utils';
interface AnimationOnFinishCallback {
c: AnimationLifecycle;
@@ -64,7 +50,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;
@@ -80,14 +65,6 @@ export const createAnimation = (animationId?: string): Animation => {
const _afterAddReadFunctions: AnimationReadWriteCallback[] = [];
const _afterAddWriteFunctions: AnimationReadWriteCallback[] = [];
const webAnimations: globalThis.Animation[] = [];
const supportsAnimationEffect =
typeof (AnimationEffect as any) === 'function' ||
(win !== undefined && typeof (win as any).AnimationEffect === 'function');
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;
@@ -192,28 +169,11 @@ export const createAnimation = (animationId?: string): Animation => {
* the animation's elements.
*/
const cleanUpElements = () => {
if (supportsWebAnimations) {
webAnimations.forEach((animation) => {
animation.cancel();
});
webAnimations.forEach((animation) => {
animation.cancel();
});
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');
});
});
}
webAnimations.length = 0;
};
/**
@@ -436,15 +396,6 @@ export const createAnimation = (animationId?: string): Animation => {
};
const duration = (animationDuration: number) => {
/**
* CSS Animation Durations of 0ms work fine on Chrome
* but do not run on Safari, so force it to 1ms to
* get it to run on both platforms.
*/
if (!supportsWebAnimations && animationDuration === 0) {
animationDuration = 1;
}
_duration = animationDuration;
update(true);
@@ -469,10 +420,10 @@ export const createAnimation = (animationId?: string): Animation => {
const addElement = (el: Element | Element[] | Node | Node[] | NodeList | undefined | null) => {
if (el != null) {
if ((el as Node).nodeType === 1) {
elements.push(el as any);
elements.push(el as HTMLElement);
} else if ((el as NodeList).length >= 0) {
for (let i = 0; i < (el as NodeList).length; i++) {
elements.push((el as any)[i]);
elements.push((el as HTMLElement[])[i]);
}
} else {
console.error('Invalid addElement value');
@@ -483,7 +434,7 @@ export const createAnimation = (animationId?: string): Animation => {
};
const addAnimation = (animationToAdd: Animation | Animation[]) => {
if ((animationToAdd as any) != null) {
if (animationToAdd != null) {
if (Array.isArray(animationToAdd)) {
for (const animation of animationToAdd) {
animation.parent(ani);
@@ -509,33 +460,29 @@ export const createAnimation = (animationId?: string): Animation => {
};
const updateKeyframes = (keyframeValues: AnimationKeyFrames) => {
if (supportsWebAnimations) {
getWebAnimations().forEach((animation) => {
/**
* animation.effect's type is AnimationEffect.
* However, in this case we have a more specific
* type of AnimationEffect called KeyframeEffect which
* inherits from AnimationEffect. As a result,
* we cast animation.effect to KeyframeEffect.
*/
const keyframeEffect = animation.effect as KeyframeEffect;
getWebAnimations().forEach((animation) => {
/**
* animation.effect's type is AnimationEffect.
* However, in this case we have a more specific
* type of AnimationEffect called KeyframeEffect which
* inherits from AnimationEffect. As a result,
* we cast animation.effect to KeyframeEffect.
*/
const keyframeEffect = animation.effect as KeyframeEffect;
/**
* setKeyframes is not supported in all browser
* versions that Ionic supports, so we need to
* check for support before using it.
*/
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (keyframeEffect.setKeyframes) {
keyframeEffect.setKeyframes(keyframeValues);
} else {
const newEffect = new KeyframeEffect(keyframeEffect.target, keyframeValues, keyframeEffect.getTiming());
animation.effect = newEffect;
}
});
} else {
initializeCSSAnimation();
}
/**
* setKeyframes is not supported in all browser
* versions that Ionic supports, so we need to
* check for support before using it.
*/
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (keyframeEffect.setKeyframes) {
keyframeEffect.setKeyframes(keyframeValues);
} else {
const newEffect = new KeyframeEffect(keyframeEffect.target, keyframeValues, keyframeEffect.getTiming());
animation.effect = newEffect;
}
});
};
/**
@@ -643,39 +590,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,15 +614,11 @@ export const createAnimation = (animationId?: string): Animation => {
}
};
const initializeAnimation = (toggleAnimationName = true) => {
const initializeAnimation = () => {
beforeAnimation();
if (_keyframes.length > 0) {
if (supportsWebAnimations) {
initializeWebAnimation();
} else {
initializeCSSAnimation(toggleAnimationName);
}
initializeWebAnimation();
}
initialized = true;
@@ -716,22 +626,11 @@ export const createAnimation = (animationId?: string): Animation => {
const setAnimationStep = (step: number) => {
step = Math.min(Math.max(step, 0), 0.9999);
if (supportsWebAnimations) {
webAnimations.forEach((animation) => {
// When creating the animation the delay is guaranteed to be set to a number.
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');
}
});
}
webAnimations.forEach((animation) => {
// When creating the animation the delay is guaranteed to be set to a number.
animation.currentTime = animation.effect!.getComputedTiming().delay! + getDuration() * step;
animation.pause();
});
};
const updateWebAnimation = (step?: number) => {
@@ -751,35 +650,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) => {
@@ -787,11 +657,7 @@ export const createAnimation = (animationId?: string): Animation => {
});
}
if (supportsWebAnimations) {
updateWebAnimation(step);
} else {
updateCSSAnimation(toggleAnimationName, step);
}
updateWebAnimation(step);
return ani;
};
@@ -842,21 +708,11 @@ export const createAnimation = (animationId?: string): Animation => {
willComplete = false;
}
if (supportsWebAnimations) {
update();
setAnimationStep(1 - step);
} else {
forceDelayValue = (1 - step) * getDuration() * -1;
update(false, false);
}
update();
setAnimationStep(1 - step);
} else if (playTo === 1) {
if (supportsWebAnimations) {
update();
setAnimationStep(step);
} else {
forceDelayValue = step * getDuration() * -1;
update(false, false);
}
update();
setAnimationStep(step);
}
if (playTo !== undefined && !parentAnimation) {
@@ -868,15 +724,9 @@ export const createAnimation = (animationId?: string): Animation => {
const pauseAnimation = () => {
if (initialized) {
if (supportsWebAnimations) {
webAnimations.forEach((animation) => {
animation.pause();
});
} else {
elements.forEach((element) => {
setStyleProperty(element, 'animation-play-state', 'paused');
});
}
webAnimations.forEach((animation) => {
animation.pause();
});
paused = true;
}
@@ -892,79 +742,12 @@ export const createAnimation = (animationId?: string): Animation => {
return ani;
};
const onAnimationEndFallback = () => {
cssAnimationsTimerFallback = undefined;
animationFinish();
};
const clearCSSAnimationsTimeout = () => {
if (cssAnimationsTimerFallback) {
clearTimeout(cssAnimationsTimerFallback);
}
};
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');
});
};
const playWebAnimations = () => {
webAnimations.forEach((animation) => {
animation.play();
@@ -976,12 +759,8 @@ export const createAnimation = (animationId?: string): Animation => {
};
const resetAnimation = () => {
if (supportsWebAnimations) {
setAnimationStep(0);
updateWebAnimation();
} else {
updateCSSAnimation();
}
setAnimationStep(0);
updateWebAnimation();
};
const play = (opts?: AnimationPlayOptions) => {
@@ -1038,11 +817,7 @@ export const createAnimation = (animationId?: string): Animation => {
animation.play();
});
if (supportsWebAnimations) {
playWebAnimations();
} else {
playCSSAnimations();
}
playWebAnimations();
paused = false;
});

View File

@@ -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', () => {
@@ -228,18 +227,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');

View File

@@ -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);
});
});
});

View File

@@ -13,11 +13,6 @@
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<script>
const forceCSSAnimations = new URLSearchParams(window.location.search).get('ionic:_forceCSSAnimations');
if (forceCSSAnimations) {
Element.prototype.animate = null;
}
class PageRoot extends HTMLElement {
connectedCallback() {
this.innerHTML = `

View File

@@ -7,11 +7,6 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
await page.goto('/src/utils/animation/test/basic', config);
await testPage(page);
});
test(`should resolve using css animations`, async ({ page }) => {
await page.goto('/src/utils/animation/test/basic?ionic:_forceCSSAnimations=true', config);
await testPage(page);
});
});
});

View File

@@ -13,11 +13,6 @@
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<script type="module">
const forceCSSAnimations = new URLSearchParams(window.location.search).get('ionic:_forceCSSAnimations');
if (forceCSSAnimations) {
Element.prototype.animate = null;
}
import { createAnimation } from '../../../../dist/ionic/index.esm.js';
const squareA = document.querySelector('.square-a');

View File

@@ -8,11 +8,6 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
await page.goto('/src/utils/animation/test/display', config);
await testDisplay(page);
});
test(`should resolve using css animations`, async ({ page }) => {
await page.goto('/src/utils/animation/test/display?ionic:_forceCSSAnimations=true', config);
await testDisplay(page);
});
});
});

View File

@@ -13,11 +13,6 @@
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<script type="module">
const forceCSSAnimations = new URLSearchParams(window.location.search).get('ionic:_forceCSSAnimations');
if (forceCSSAnimations) {
Element.prototype.animate = null;
}
import { createAnimation } from '../../../../dist/ionic/index.esm.js';
const squareA = document.querySelector('.square-a');

View File

@@ -8,11 +8,6 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
await page.goto('/src/utils/animation/test/hooks', config);
await testHooks(page);
});
test(`should fire hooks using css animations`, async ({ page }) => {
await page.goto('/src/utils/animation/test/hooks?ionic:_forceCSSAnimations=true', config);
await testHooks(page);
});
});
});

View File

@@ -13,11 +13,6 @@
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<script type="module">
const forceCSSAnimations = new URLSearchParams(window.location.search).get('ionic:_forceCSSAnimations');
if (forceCSSAnimations) {
Element.prototype.animate = null;
}
import { createAnimation } from '../../../../dist/ionic/index.esm.js';
const squareA = document.querySelector('.square-a');

View File

@@ -8,14 +8,6 @@ configs({ modes: ['md'], directions: ['ltr'] }).forEach(({ title, config }) => {
await page.goto('/src/utils/animation/test/multiple', config);
await testMultiple(page);
});
/**
* CSS animations will occasionally resolve out of order, so we skip for now
*/
test.skip(`should resolve grouped animations using css animations`, async ({ page }) => {
await page.goto('/src/utils/animation/test/multiple?ionic:_forceCSSAnimations=true', config);
await testMultiple(page);
});
});
});

View File

@@ -13,11 +13,6 @@
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<script type="module">
const forceCSSAnimations = new URLSearchParams(window.location.search).get('ionic:_forceCSSAnimations');
if (forceCSSAnimations) {
Element.prototype.animate = null;
}
import { createAnimation } from '../../../../dist/ionic/index.esm.js';
const squareA = document.querySelector('.square-a');

View File

@@ -13,11 +13,6 @@
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<script type="module">
const forceCSSAnimations = new URLSearchParams(window.location.search).get('ionic:_forceCSSAnimations');
if (forceCSSAnimations) {
Element.prototype.animate = null;
}
import { createAnimation } from '../../../../dist/ionic/index.esm.js';
createAnimation()