diff --git a/docs/assets/plugins/metadata/metadata.js b/docs/assets/plugins/metadata/metadata.js
index 17c5e213..72babe86 100644
--- a/docs/assets/plugins/metadata/metadata.js
+++ b/docs/assets/plugins/metadata/metadata.js
@@ -193,6 +193,32 @@
return table.outerHTML;
}
+ function createAnimationsTable(animations) {
+ const table = document.createElement('table');
+ table.innerHTML = `
+
+
+ | Name |
+ Description |
+
+
+
@@ -278,6 +284,32 @@ export default class SlDialog extends LitElement {
}
}
+setDefaultAnimation('dialog.show', {
+ keyframes: [
+ { opacity: 0, transform: 'scale(0.8)' },
+ { opacity: 1, transform: 'scale(1)' }
+ ],
+ options: { duration: 150 }
+});
+
+setDefaultAnimation('dialog.hide', {
+ keyframes: [
+ { opacity: 1, transform: 'scale(1)' },
+ { opacity: 0, transform: 'scale(0.8)' }
+ ],
+ options: { duration: 150 }
+});
+
+setDefaultAnimation('dialog.overlay.show', {
+ keyframes: [{ opacity: 0 }, { opacity: 1 }],
+ options: { duration: 150 }
+});
+
+setDefaultAnimation('dialog.overlay.hide', {
+ keyframes: [{ opacity: 1 }, { opacity: 0 }],
+ options: { duration: 150 }
+});
+
declare global {
interface HTMLElementTagNameMap {
'sl-dialog': SlDialog;
diff --git a/src/internal/animate.ts b/src/internal/animate.ts
index aaea034f..f3a6cd63 100644
--- a/src/internal/animate.ts
+++ b/src/internal/animate.ts
@@ -1,43 +1,47 @@
-export function prefersReducedMotion() {
- const query = window.matchMedia('(prefers-reduced-motion: reduce)');
- return query?.matches;
-}
-
//
-// Performs a finite, keyframe-based animation. Returns a promise that resolves when the animation finishes or cancels.
+// Animates an element using keyframes. Returns a promise that resolves after the animation completes or gets canceled.
//
-export async function animateTo(
+export function animateTo(
el: HTMLElement,
keyframes: Keyframe[] | PropertyIndexedKeyframes,
options?: KeyframeAnimationOptions
) {
return new Promise(async resolve => {
- if (options) {
- if (options.duration === Infinity) {
- throw new Error('Promise-based animations must be finite.');
- }
-
- if (prefersReducedMotion()) {
- options.duration = 0;
- }
+ if (options?.duration === Infinity) {
+ throw new Error('Promise-based animations must be finite.');
}
- const animation = el.animate(keyframes, options);
+ const animation = el.animate(keyframes, {
+ fill: 'both',
+ ...options,
+ duration: prefersReducedMotion() ? 0 : options!.duration
+ });
+
animation.addEventListener('cancel', resolve, { once: true });
animation.addEventListener('finish', resolve, { once: true });
});
}
//
-// Stops all active animations on the target element. Returns a promise that resolves when all animations are canceled.
+// Tells if the user has enabled the "reduced motion" setting in their browser or OS.
//
-export async function stopAnimations(el: HTMLElement) {
- await Promise.all(
- el.getAnimations().map(animation => {
+export function prefersReducedMotion() {
+ const query = window.matchMedia('(prefers-reduced-motion: reduce)');
+ return query?.matches;
+}
+
+//
+// Stops all active animations on the target element. Returns a promise that resolves after all animations are canceled.
+//
+export function stopAnimations(el: HTMLElement) {
+ return Promise.all(
+ el.getAnimations().map((animation: any) => {
return new Promise(resolve => {
+ const handleAnimationEvent = requestAnimationFrame(resolve);
+
+ animation.addEventListener('cancel', () => handleAnimationEvent, { once: true });
+ animation.addEventListener('finish', () => handleAnimationEvent, { once: true });
animation.cancel();
- animation.addEventListener('cancel', resolve, { once: true });
- animation.addEventListener('finish', resolve, { once: true });
});
})
);
diff --git a/src/utilities/animation-registry.ts b/src/utilities/animation-registry.ts
new file mode 100644
index 00000000..a297ac4f
--- /dev/null
+++ b/src/utilities/animation-registry.ts
@@ -0,0 +1,55 @@
+interface ElementAnimation {
+ keyframes: Keyframe[] | PropertyIndexedKeyframes;
+ options?: KeyframeAnimationOptions;
+}
+
+interface ElementAnimationMap {
+ [animationName: string]: ElementAnimation;
+}
+
+const defaultAnimationRegistry = new Map
();
+const customAnimationRegistry = new WeakMap();
+
+//
+// Sets a default animation. Components should use the `name.animation` for primary animations and `name.part.animation`
+// for secondary animations, e.g. `alert.show` and `dialog.overlay.show`.
+//
+export function setDefaultAnimation(animationName: string, animation: ElementAnimation) {
+ defaultAnimationRegistry.set(animationName, animation);
+}
+
+//
+// Sets a custom animation for the specified element.
+//
+export function setAnimation(el: Element, animationName: string, animation: ElementAnimation) {
+ customAnimationRegistry.set(
+ el,
+ Object.assign({}, customAnimationRegistry.get(el), {
+ [animationName]: animation
+ })
+ );
+}
+
+//
+// Gets an element's custom animation. Falls back to the default animation if no custom one is found.
+//
+export function getAnimation(el: Element, animationName: string) {
+ const customAnimation = customAnimationRegistry.get(el);
+
+ // Check for a custom animation
+ if (customAnimation && customAnimation[animationName]) {
+ return customAnimation[animationName];
+ }
+
+ // Check for a default animation
+ const defaultAnimation = defaultAnimationRegistry.get(animationName);
+ if (defaultAnimation) {
+ return defaultAnimation;
+ }
+
+ // Fall back to an empty animation
+ return {
+ keyframes: [],
+ options: { duration: 0 }
+ };
+}