Add animation registry and update alert/dialog

pull/463/head
Cory LaViska 2021-05-21 17:53:53 -04:00
rodzic 1ca890b1e9
commit a87596b3a1
10 zmienionych plików z 293 dodań i 140 usunięć

Wyświetl plik

@ -193,6 +193,32 @@
return table.outerHTML; return table.outerHTML;
} }
function createAnimationsTable(animations) {
const table = document.createElement('table');
table.innerHTML = `
<thead>
<tr>
<th>Name</th>
<th>Description</th>
</tr>
</thead>
<tbody>
${animations
.map(
animation => `
<tr>
<td><code>${escapeHtml(animation.name)}</code></td>
<td>${escapeHtml(animation.description)}</td>
</tr>
`
)
.join('')}
</tbody>
`;
return table.outerHTML;
}
function createDependenciesList(targetComponent, allComponents) { function createDependenciesList(targetComponent, allComponents) {
const ul = document.createElement('ul'); const ul = document.createElement('ul');
const dependencies = []; const dependencies = [];
@ -295,7 +321,7 @@
if (!component) { if (!component) {
console.error('Component not found in metadata: ' + tag); console.error('Component not found in metadata: ' + tag);
next(content); return next(content);
} }
let badgeType = 'info'; let badgeType = 'info';
@ -332,7 +358,7 @@
if (!component) { if (!component) {
console.error('Component not found in metadata: ' + tag); console.error('Component not found in metadata: ' + tag);
next(content); return next(content);
} }
if (component.props.length) { if (component.props.length) {
@ -377,6 +403,15 @@
`; `;
} }
if (component.animations.length) {
result += `
## Animations
${createAnimationsTable(component.animations)}
Learn how to [customize animations](/getting-started/customizing#animations).
`;
}
if (component.dependencies.length) { if (component.dependencies.length) {
result += ` result += `
## Dependencies ## Dependencies

Wyświetl plik

@ -106,3 +106,51 @@ Alternatively, you can set them inline directly on the element.
``` ```
Not all components expose CSS custom properties. For those that do, they can be found in the component's API documentation. Not all components expose CSS custom properties. For those that do, they can be found in the component's API documentation.
## Animations
Some components use animation, such as when a dialog is shown or hidden. Animations are performed using the [Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API) rather than CSS. However, you can still customize them through Shoelace's animation registry. If a component has customizable animations, they'll be listed in the "Animation" section of its documentation.
To customize a default animation, use the `setDefaultAnimation()` method. The function accepts an animation name (found in the component's docs) and an object with `keyframes` and `options`.
This example will make all dialogs use a custom show animation.
```js
import { setDefaultAnimation } from '/dist/utilities/animation-registry.js';
// Change the default animation for all dialogs
setDefaultAnimation('dialog.show', {
keyframes: [
{ transform: 'rotate(-10deg) scale(0.5)', opacity: '0' },
{ transform: 'rotate(0deg) scale(1)', opacity: '1' }
],
options: {
duration: 500
}
});
```
If you only want to target a single component, use the `setAnimation()` method instead. This function accepts an element, an animation name, and an object comprised of animation `keyframes` and `options`.
In this example, only the target dialog will use a custom show animation.
```js
import { setAnimation } from '/dist/utilities/animation-registry.js';
// Change the animation for a single dialog
const dialog = document.querySelector('#my-dialog');
setAnimation(dialog, 'dialog.show', {
keyframes: [
{ transform: 'rotate(-10deg) scale(0.5)', opacity: '0' },
{ transform: 'rotate(0deg) scale(1)', opacity: '1' }
],
options: {
duration: 500
}
});
```
To learn more about creating Web Animations, refer to the documentation for [`Element.animate()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/animate).
?> Animations respect the users `prefers-reduced-motion` setting. When this setting is enabled, animations will not be played. To disable animations for all users, set `options.duration` to `0`.

Wyświetl plik

@ -6,6 +6,19 @@ Components with the <sl-badge type="warning" pill>Experimental</sl-badge> badge
_During the beta period, these restrictions may be relaxed in the event of a mission-critical bug._ 🐛 _During the beta period, these restrictions may be relaxed in the event of a mission-critical bug._ 🐛
## Next
This release changes how components animate. In previous versions, CSS transitions were used for most show/hide animations. Transitions are problematic due to the way `transitionend` works. This event fires once _per transition_, and it's impossible to know which transition to look for when users can customize any possible CSS property. Because of this, components previously required the `opacity` property to transition. If a user were to prevent `opacity` from transitioning, the `sl-after-show|hide` events would never emit.
CSS animations, on the other hand, have a more reliable `animationend` event. Alas, we can't use them because `@keyframes` don't cascade and can't be injected into a shadow DOM via CSS.
The most elegant solution I found was to use the [Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API), which offers more control over animations at the expense of customizations being done in JavaScript. Fortunately, through the [Animation Registry](/getting-started/customizing#animations), you can customize animations globally and/or per component with a minimal amount of code.
- Added the Animation Registry
- Updated animations for `sl-alert`, `sl-dialog`, ... to use the Animation Registry instead of CSS transitions
- Improved a11y by respecting `prefers-reduced-motion` for all show/hide animations
## 2.0.0-beta.40 ## 2.0.0-beta.40
- 🚨 BREAKING: renamed `sl-responsive-embed` to `sl-responsive-media` and added support for images and videos [#436](https://github.com/shoelace-style/shoelace/issues/436) - 🚨 BREAKING: renamed `sl-responsive-embed` to `sl-responsive-media` and added support for images and videos [#436](https://github.com/shoelace-style/shoelace/issues/436)

Wyświetl plik

@ -99,6 +99,7 @@ components.map(async component => {
const slots = tags.filter(item => item.tag === 'slot'); const slots = tags.filter(item => item.tag === 'slot');
const parts = tags.filter(item => item.tag === 'part'); const parts = tags.filter(item => item.tag === 'part');
const customProperties = tags.filter(item => item.tag === 'customproperty'); const customProperties = tags.filter(item => item.tag === 'customproperty');
const animations = tags.filter(item => item.tag === 'animation');
api.since = tags.find(item => item.tag === 'since').text.trim(); api.since = tags.find(item => item.tag === 'since').text.trim();
api.status = tags.find(item => item.tag === 'status').text.trim(); api.status = tags.find(item => item.tag === 'status').text.trim();
@ -106,6 +107,7 @@ components.map(async component => {
api.slots = slots.map(tag => splitText(tag.text)); api.slots = slots.map(tag => splitText(tag.text));
api.parts = parts.map(tag => splitText(tag.text)); api.parts = parts.map(tag => splitText(tag.text));
api.cssCustomProperties = customProperties.map(tag => splitText(tag.text)); api.cssCustomProperties = customProperties.map(tag => splitText(tag.text));
api.animations = animations.map(tag => splitText(tag.text));
} else { } else {
console.error(chalk.yellow(`Missing comment block for ${component.name} - skipping metadata`)); console.error(chalk.yellow(`Missing comment block for ${component.name} - skipping metadata`));
} }

Wyświetl plik

@ -23,10 +23,6 @@
line-height: 1.6; line-height: 1.6;
color: var(--sl-color-gray-700); color: var(--sl-color-gray-700);
margin: inherit; margin: inherit;
&[hidden] {
display: none;
}
} }
.alert__icon { .alert__icon {

Wyświetl plik

@ -1,37 +1,13 @@
import { LitElement, html, unsafeCSS } from 'lit'; import { LitElement, html, unsafeCSS } from 'lit';
import { customElement, property, query } from 'lit/decorators'; import { customElement, property, query } from 'lit/decorators';
import { classMap } from 'lit-html/directives/class-map'; import { classMap } from 'lit-html/directives/class-map';
import { event, EventEmitter, watch } from '../../internal/decorators';
import { animateTo, stopAnimations } from '../../internal/animate'; import { animateTo, stopAnimations } from '../../internal/animate';
import { event, EventEmitter, watch } from '../../internal/decorators';
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
import styles from 'sass:./alert.scss'; import styles from 'sass:./alert.scss';
const toastStack = Object.assign(document.createElement('div'), { className: 'sl-toast-stack' }); const toastStack = Object.assign(document.createElement('div'), { className: 'sl-toast-stack' });
//
// TODO - At the component level, expose `animationSettings` which will work like this:
//
// alert.animationSettings = {
// show: {
// keyframes: [],
// options: {}
// },
// hide: {
// keyframes: [],
// options: {}
// }
// };
//
// TODO - To allow users to change the default value for all alerts, export a `setAnimationDefaults()` function. When no
// animationSettings are provided, we'll use the defaults.
//
// TODO - In the changelog, describe why these changes are being made:
//
// - CSS transitions are more easily customizable, but not reliable due to reflow hacks and now knowing which
// transition to wait for via transitionend.
// - Web Animations API is more reliable at the expense of being harder to customize. However, providing the
// setAnimationDefaults() function gives you complete control over individual component animations with one call.
//
/** /**
* @since 2.0 * @since 2.0
* @status stable * @status stable
@ -47,6 +23,9 @@ const toastStack = Object.assign(document.createElement('div'), { className: 'sl
* @part close-button - The close button. * @part close-button - The close button.
* *
* @customProperty --box-shadow - The alert's box shadow. * @customProperty --box-shadow - The alert's box shadow.
*
* @animation alert.show - The animation to use when showing the alert.
* @animation alert.hide - The animation to use when hiding the alert.
*/ */
@customElement('sl-alert') @customElement('sl-alert')
export default class SlAlert extends LitElement { export default class SlAlert extends LitElement {
@ -111,17 +90,10 @@ export default class SlAlert extends LitElement {
this.restartAutoHide(); this.restartAutoHide();
} }
// Animate in
await stopAnimations(this.base); await stopAnimations(this.base);
this.base.hidden = false; this.base.hidden = false;
await animateTo( const animation = getAnimation(this, 'alert.show');
this.base, await animateTo(this.base, animation.keyframes, animation.options);
[
{ opacity: 0, transform: 'scale(0.8)' },
{ opacity: 1, transform: 'scale(1)' }
],
{ duration: 250 }
);
this.slAfterShow.emit(); this.slAfterShow.emit();
} }
@ -142,16 +114,9 @@ export default class SlAlert extends LitElement {
clearTimeout(this.autoHideTimeout); clearTimeout(this.autoHideTimeout);
// Animate out
await stopAnimations(this.base); await stopAnimations(this.base);
await animateTo( const animation = getAnimation(this, 'alert.hide');
this.base, await animateTo(this.base, animation.keyframes, animation.options);
[
{ opacity: 1, transform: 'scale(1)' },
{ opacity: 0, transform: 'scale(0.8)' }
],
{ duration: 250 }
);
this.base.hidden = true; this.base.hidden = true;
this.slAfterHide.emit(); this.slAfterHide.emit();
@ -262,6 +227,22 @@ export default class SlAlert extends LitElement {
} }
} }
setDefaultAnimation('alert.show', {
keyframes: [
{ opacity: 0, transform: 'scale(0.8)' },
{ opacity: 1, transform: 'scale(1)' }
],
options: { duration: 150 }
});
setDefaultAnimation('alert.hide', {
keyframes: [
{ opacity: 1, transform: 'scale(1)' },
{ opacity: 0, transform: 'scale(0.8)' }
],
options: { duration: 150 }
});
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
'sl-alert': SlAlert; 'sl-alert': SlAlert;

Wyświetl plik

@ -20,10 +20,6 @@
bottom: 0; bottom: 0;
left: 0; left: 0;
z-index: var(--sl-z-index-dialog); z-index: var(--sl-z-index-dialog);
&:not(.dialog--visible) {
@include hide.hidden;
}
} }
.dialog__panel { .dialog__panel {
@ -36,9 +32,6 @@
background-color: var(--sl-panel-background-color); background-color: var(--sl-panel-background-color);
border-radius: var(--sl-border-radius-medium); border-radius: var(--sl-border-radius-medium);
box-shadow: var(--sl-shadow-x-large); box-shadow: var(--sl-shadow-x-large);
opacity: 0;
transform: scale(0.8);
transition: var(--sl-transition-medium) opacity, var(--sl-transition-medium) transform;
&:focus { &:focus {
outline: none; outline: none;
@ -106,10 +99,4 @@
bottom: 0; bottom: 0;
left: 0; left: 0;
background-color: var(--sl-overlay-background-color); background-color: var(--sl-overlay-background-color);
opacity: 0;
transition: var(--sl-transition-medium) opacity;
}
.dialog--open .dialog__overlay {
opacity: 1;
} }

Wyświetl plik

@ -2,11 +2,13 @@ import { LitElement, html, unsafeCSS } from 'lit';
import { customElement, property, query, state } from 'lit/decorators'; import { customElement, property, query, state } from 'lit/decorators';
import { classMap } from 'lit-html/directives/class-map'; import { classMap } from 'lit-html/directives/class-map';
import { ifDefined } from 'lit-html/directives/if-defined'; import { ifDefined } from 'lit-html/directives/if-defined';
import { animateTo, stopAnimations } from '../../internal/animate';
import { event, EventEmitter, watch } from '../../internal/decorators'; import { event, EventEmitter, watch } from '../../internal/decorators';
import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll'; import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll';
import { hasSlot } from '../../internal/slot'; import { hasSlot } from '../../internal/slot';
import { isPreventScrollSupported } from '../../internal/support'; import { isPreventScrollSupported } from '../../internal/support';
import Modal from '../../internal/modal'; import Modal from '../../internal/modal';
import { setDefaultAnimation, getAnimation } from '../../utilities/animation-registry';
import styles from 'sass:./dialog.scss'; import styles from 'sass:./dialog.scss';
const hasPreventScroll = isPreventScrollSupported(); const hasPreventScroll = isPreventScrollSupported();
@ -36,6 +38,11 @@ let id = 0;
* @customProperty --header-spacing - The amount of padding to use for the header. * @customProperty --header-spacing - The amount of padding to use for the header.
* @customProperty --body-spacing - The amount of padding to use for the body. * @customProperty --body-spacing - The amount of padding to use for the body.
* @customProperty --footer-spacing - The amount of padding to use for the footer. * @customProperty --footer-spacing - The amount of padding to use for the footer.
*
* @animation dialog.show - The animation to use when showing the dialog.
* @animation dialog.hide - The animation to use when hiding the dialog.
* @animation dialog.overlay.show - The animation to use when showing the dialog's overlay.
* @animation dialog.overlay.hide - The animation to use when hiding the dialog's overlay.
*/ */
@customElement('sl-dialog') @customElement('sl-dialog')
export default class SlDialog extends LitElement { export default class SlDialog extends LitElement {
@ -43,15 +50,14 @@ export default class SlDialog extends LitElement {
@query('.dialog') dialog: HTMLElement; @query('.dialog') dialog: HTMLElement;
@query('.dialog__panel') panel: HTMLElement; @query('.dialog__panel') panel: HTMLElement;
@query('.dialog__overlay') overlay: HTMLElement;
private componentId = `dialog-${++id}`; private componentId = `dialog-${++id}`;
private hasInitialized = false;
private modal: Modal; private modal: Modal;
private originalTrigger: HTMLElement | null; private originalTrigger: HTMLElement | null;
private willShow = false;
private willHide = false;
@state() private hasFooter = false; @state() private hasFooter = false;
@state() private isVisible = false;
/** Indicates whether or not the dialog is open. You can use this in lieu of the show/hide methods. */ /** Indicates whether or not the dialog is open. You can use this in lieu of the show/hide methods. */
@property({ type: Boolean, reflect: true }) open = false; @property({ type: Boolean, reflect: true }) open = false;
@ -101,14 +107,23 @@ export default class SlDialog extends LitElement {
} }
} }
async firstUpdated() {
// Set initial visibility
this.dialog.hidden = !this.open;
// Set the initialized flag after the first update is complete
await this.updateComplete;
this.hasInitialized = true;
}
disconnectedCallback() { disconnectedCallback() {
super.disconnectedCallback(); super.disconnectedCallback();
unlockBodyScrolling(this); unlockBodyScrolling(this);
} }
/** Shows the dialog */ /** Shows the dialog */
show() { async show() {
if (this.willShow) { if (!this.hasInitialized) {
return; return;
} }
@ -119,47 +134,44 @@ export default class SlDialog extends LitElement {
} }
this.originalTrigger = document.activeElement as HTMLElement; this.originalTrigger = document.activeElement as HTMLElement;
this.willShow = true;
this.isVisible = true;
this.open = true; this.open = true;
this.modal.activate(); this.modal.activate();
lockBodyScrolling(this); lockBodyScrolling(this);
if (this.open) { await Promise.all([stopAnimations(this.dialog), stopAnimations(this.overlay)]);
if (hasPreventScroll) { this.dialog.hidden = false;
// Wait for the next frame before setting initial focus so the dialog is technically visible
requestAnimationFrame(() => { // Browsers that support el.focus({ preventScroll }) can set initial focus immediately
const slInitialFocus = this.slInitialFocus.emit(); if (hasPreventScroll) {
if (!slInitialFocus.defaultPrevented) { const slInitialFocus = this.slInitialFocus.emit();
this.panel.focus({ preventScroll: true }); if (!slInitialFocus.defaultPrevented) {
} this.panel.focus({ preventScroll: true });
});
} else {
// Once Safari supports { preventScroll: true } we can remove this nasty little hack, but until then we need to
// wait for the transition to complete before setting focus, otherwise the panel may render in a buggy way
// that's out of view initially.
//
// Fiddle: https://jsfiddle.net/g6buoafq/1/
// Safari: https://bugs.webkit.org/show_bug.cgi?id=178583
//
this.dialog.addEventListener(
'transitionend',
() => {
const slInitialFocus = this.slInitialFocus.emit();
if (!slInitialFocus.defaultPrevented) {
this.panel.focus();
}
},
{ once: true }
);
} }
} }
const panelAnimation = getAnimation(this, 'dialog.show');
const overlayAnimation = getAnimation(this, 'dialog.overlay.show');
await Promise.all([
animateTo(this.panel, panelAnimation.keyframes, panelAnimation.options),
animateTo(this.overlay, overlayAnimation.keyframes, overlayAnimation.options)
]);
// Browsers that don't support el.focus({ preventScroll }) have to wait for the animation to finish before initial
// focus to prevent scrolling issues. See: https://caniuse.com/mdn-api_htmlelement_focus_preventscroll_option
if (!hasPreventScroll) {
const slInitialFocus = this.slInitialFocus.emit();
if (!slInitialFocus.defaultPrevented) {
this.panel.focus({ preventScroll: true });
}
}
this.slAfterShow.emit();
} }
/** Hides the dialog */ /** Hides the dialog */
hide() { async hide() {
if (this.willHide) { if (!this.hasInitialized) {
return; return;
} }
@ -169,15 +181,27 @@ export default class SlDialog extends LitElement {
return; return;
} }
this.willHide = true;
this.open = false; this.open = false;
this.modal.deactivate(); this.modal.deactivate();
await Promise.all([stopAnimations(this.dialog), stopAnimations(this.overlay)]);
const panelAnimation = getAnimation(this, 'dialog.hide');
const overlayAnimation = getAnimation(this, 'dialog.overlay.hide');
await Promise.all([
animateTo(this.panel, panelAnimation.keyframes, panelAnimation.options),
animateTo(this.overlay, overlayAnimation.keyframes, overlayAnimation.options)
]);
this.dialog.hidden = true;
unlockBodyScrolling(this);
// Restore focus to the original trigger // Restore focus to the original trigger
const trigger = this.originalTrigger; const trigger = this.originalTrigger;
if (trigger && typeof trigger.focus === 'function') { if (trigger && typeof trigger.focus === 'function') {
setTimeout(() => trigger.focus()); setTimeout(() => trigger.focus());
} }
this.slAfterHide.emit();
} }
handleCloseClick() { handleCloseClick() {
@ -207,22 +231,6 @@ export default class SlDialog extends LitElement {
this.hasFooter = hasSlot(this, 'footer'); this.hasFooter = hasSlot(this, 'footer');
} }
handleTransitionEnd(event: TransitionEvent) {
const target = event.target as HTMLElement;
// Ensure we only emit one event when the target element is no longer visible
if (event.propertyName === 'opacity' && target.classList.contains('dialog__panel')) {
this.isVisible = this.open;
this.willShow = false;
this.willHide = false;
this.open ? this.slAfterShow.emit() : this.slAfterHide.emit();
if (!this.open) {
unlockBodyScrolling(this);
}
}
}
render() { render() {
return html` return html`
<div <div
@ -230,11 +238,9 @@ export default class SlDialog extends LitElement {
class=${classMap({ class=${classMap({
dialog: true, dialog: true,
'dialog--open': this.open, 'dialog--open': this.open,
'dialog--visible': this.isVisible,
'dialog--has-footer': this.hasFooter 'dialog--has-footer': this.hasFooter
})} })}
@keydown=${this.handleKeyDown} @keydown=${this.handleKeyDown}
@transitionend=${this.handleTransitionEnd}
> >
<div part="overlay" class="dialog__overlay" @click=${this.handleOverlayClick} tabindex="-1"></div> <div part="overlay" class="dialog__overlay" @click=${this.handleOverlayClick} tabindex="-1"></div>
@ -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 { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
'sl-dialog': SlDialog; 'sl-dialog': SlDialog;

Wyświetl plik

@ -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, el: HTMLElement,
keyframes: Keyframe[] | PropertyIndexedKeyframes, keyframes: Keyframe[] | PropertyIndexedKeyframes,
options?: KeyframeAnimationOptions options?: KeyframeAnimationOptions
) { ) {
return new Promise(async resolve => { return new Promise(async resolve => {
if (options) { if (options?.duration === Infinity) {
if (options.duration === Infinity) { throw new Error('Promise-based animations must be finite.');
throw new Error('Promise-based animations must be finite.');
}
if (prefersReducedMotion()) {
options.duration = 0;
}
} }
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('cancel', resolve, { once: true });
animation.addEventListener('finish', 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) { export function prefersReducedMotion() {
await Promise.all( const query = window.matchMedia('(prefers-reduced-motion: reduce)');
el.getAnimations().map(animation => { 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 => { return new Promise(resolve => {
const handleAnimationEvent = requestAnimationFrame(resolve);
animation.addEventListener('cancel', () => handleAnimationEvent, { once: true });
animation.addEventListener('finish', () => handleAnimationEvent, { once: true });
animation.cancel(); animation.cancel();
animation.addEventListener('cancel', resolve, { once: true });
animation.addEventListener('finish', resolve, { once: true });
}); });
}) })
); );

Wyświetl plik

@ -0,0 +1,55 @@
interface ElementAnimation {
keyframes: Keyframe[] | PropertyIndexedKeyframes;
options?: KeyframeAnimationOptions;
}
interface ElementAnimationMap {
[animationName: string]: ElementAnimation;
}
const defaultAnimationRegistry = new Map<String, ElementAnimation>();
const customAnimationRegistry = new WeakMap<Element, ElementAnimationMap>();
//
// 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 }
};
}