pull/792/head
Cory LaViska 2022-06-09 18:14:38 -04:00
rodzic c8d92e41b2
commit 8a28d66393
10 zmienionych plików z 91 dodań i 33 usunięć

Wyświetl plik

@ -108,7 +108,7 @@ Not all components expose CSS custom properties. For those that do, they can be
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. 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` or `null` to disable the animation. 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` or `null` to disable the animation.
This example will make all dialogs use a custom show animation. This example will make all dialogs use a custom show animation.
@ -127,6 +127,8 @@ setDefaultAnimation('dialog.show', {
}); });
``` ```
?> To support RTL languages in your animation, you can pass an additional property called `rtlKeyframes`. This property shares the same type as `keyframes` and will be automatically used when the component's directionality is RTL. If `rtlKeyframes` is not provided, `keyframes` will be used as a fallback.
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`. 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. In this example, only the target dialog will use a custom show animation.

Wyświetl plik

@ -8,6 +8,8 @@ _During the beta period, these restrictions may be relaxed in the event of a mis
## Next ## Next
- Added support for RTL animations in the Animation Registry
- Improved RTL animations for `<sl-drawer>` [#784](https://github.com/shoelace-style/shoelace/issues/784)
- Improved RTL styles for `<sl-button-group>` [#783](https://github.com/shoelace-style/shoelace/issues/783) - Improved RTL styles for `<sl-button-group>` [#783](https://github.com/shoelace-style/shoelace/issues/783)
- Improved RTL styles for the toast stack [#785](https://github.com/shoelace-style/shoelace/issues/785) - Improved RTL styles for the toast stack [#785](https://github.com/shoelace-style/shoelace/issues/785)

Wyświetl plik

@ -7,6 +7,7 @@ import { emit, waitForEvent } from '../../internal/event';
import { HasSlotController } from '../../internal/slot'; import { HasSlotController } from '../../internal/slot';
import { watch } from '../../internal/watch'; import { watch } from '../../internal/watch';
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry'; import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
import { LocalizeController } from '../../utilities/localize';
import styles from './alert.styles'; import styles from './alert.styles';
const toastStack = Object.assign(document.createElement('div'), { className: 'sl-toast-stack' }); const toastStack = Object.assign(document.createElement('div'), { className: 'sl-toast-stack' });
@ -43,6 +44,7 @@ export default class SlAlert extends LitElement {
private autoHideTimeout: number; private autoHideTimeout: number;
private readonly hasSlotController = new HasSlotController(this, 'icon', 'suffix'); private readonly hasSlotController = new HasSlotController(this, 'icon', 'suffix');
private readonly localize = new LocalizeController(this);
@query('[part="base"]') base: HTMLElement; @query('[part="base"]') base: HTMLElement;
@ -148,7 +150,7 @@ export default class SlAlert extends LitElement {
await stopAnimations(this.base); await stopAnimations(this.base);
this.base.hidden = false; this.base.hidden = false;
const { keyframes, options } = getAnimation(this, 'alert.show'); const { keyframes, options } = getAnimation(this, 'alert.show', { dir: this.localize.dir() });
await animateTo(this.base, keyframes, options); await animateTo(this.base, keyframes, options);
emit(this, 'sl-after-show'); emit(this, 'sl-after-show');
@ -159,7 +161,7 @@ export default class SlAlert extends LitElement {
clearTimeout(this.autoHideTimeout); clearTimeout(this.autoHideTimeout);
await stopAnimations(this.base); await stopAnimations(this.base);
const { keyframes, options } = getAnimation(this, 'alert.hide'); const { keyframes, options } = getAnimation(this, 'alert.hide', { dir: this.localize.dir() });
await animateTo(this.base, keyframes, options); await animateTo(this.base, keyframes, options);
this.base.hidden = true; this.base.hidden = true;

Wyświetl plik

@ -6,6 +6,7 @@ import { animateTo, shimKeyframesHeightAuto, stopAnimations } from '../../intern
import { emit, waitForEvent } from '../../internal/event'; import { emit, waitForEvent } from '../../internal/event';
import { watch } from '../../internal/watch'; import { watch } from '../../internal/watch';
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry'; import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
import { LocalizeController } from '../../utilities/localize';
import styles from './details.styles'; import styles from './details.styles';
/** /**
@ -39,6 +40,8 @@ export default class SlDetails extends LitElement {
@query('.details__header') header: HTMLElement; @query('.details__header') header: HTMLElement;
@query('.details__body') body: HTMLElement; @query('.details__body') body: HTMLElement;
private readonly localize = new LocalizeController(this);
/** Indicates whether or not the details is open. You can use this in lieu of the show/hide methods. */ /** Indicates whether or not the details 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;
@ -116,7 +119,7 @@ export default class SlDetails extends LitElement {
await stopAnimations(this.body); await stopAnimations(this.body);
this.body.hidden = false; this.body.hidden = false;
const { keyframes, options } = getAnimation(this, 'details.show'); const { keyframes, options } = getAnimation(this, 'details.show', { dir: this.localize.dir() });
await animateTo(this.body, shimKeyframesHeightAuto(keyframes, this.body.scrollHeight), options); await animateTo(this.body, shimKeyframesHeightAuto(keyframes, this.body.scrollHeight), options);
this.body.style.height = 'auto'; this.body.style.height = 'auto';
@ -127,7 +130,7 @@ export default class SlDetails extends LitElement {
await stopAnimations(this.body); await stopAnimations(this.body);
const { keyframes, options } = getAnimation(this, 'details.hide'); const { keyframes, options } = getAnimation(this, 'details.hide', { dir: this.localize.dir() });
await animateTo(this.body, shimKeyframesHeightAuto(keyframes, this.body.scrollHeight), options); await animateTo(this.body, shimKeyframesHeightAuto(keyframes, this.body.scrollHeight), options);
this.body.hidden = true; this.body.hidden = true;
this.body.style.height = 'auto'; this.body.style.height = 'auto';

Wyświetl plik

@ -129,7 +129,7 @@ export default class SlDialog extends LitElement {
}); });
if (slRequestClose.defaultPrevented) { if (slRequestClose.defaultPrevented) {
const animation = getAnimation(this, 'dialog.denyClose'); const animation = getAnimation(this, 'dialog.denyClose', { dir: this.localize.dir() });
animateTo(this.panel, animation.keyframes, animation.options); animateTo(this.panel, animation.keyframes, animation.options);
return; return;
} }
@ -187,8 +187,8 @@ export default class SlDialog extends LitElement {
} }
}); });
const panelAnimation = getAnimation(this, 'dialog.show'); const panelAnimation = getAnimation(this, 'dialog.show', { dir: this.localize.dir() });
const overlayAnimation = getAnimation(this, 'dialog.overlay.show'); const overlayAnimation = getAnimation(this, 'dialog.overlay.show', { dir: this.localize.dir() });
await Promise.all([ await Promise.all([
animateTo(this.panel, panelAnimation.keyframes, panelAnimation.options), animateTo(this.panel, panelAnimation.keyframes, panelAnimation.options),
animateTo(this.overlay, overlayAnimation.keyframes, overlayAnimation.options) animateTo(this.overlay, overlayAnimation.keyframes, overlayAnimation.options)
@ -201,8 +201,8 @@ export default class SlDialog extends LitElement {
this.modal.deactivate(); this.modal.deactivate();
await Promise.all([stopAnimations(this.dialog), stopAnimations(this.overlay)]); await Promise.all([stopAnimations(this.dialog), stopAnimations(this.overlay)]);
const panelAnimation = getAnimation(this, 'dialog.hide'); const panelAnimation = getAnimation(this, 'dialog.hide', { dir: this.localize.dir() });
const overlayAnimation = getAnimation(this, 'dialog.overlay.hide'); const overlayAnimation = getAnimation(this, 'dialog.overlay.hide', { dir: this.localize.dir() });
await Promise.all([ await Promise.all([
animateTo(this.panel, panelAnimation.keyframes, panelAnimation.options), animateTo(this.panel, panelAnimation.keyframes, panelAnimation.options),
animateTo(this.overlay, overlayAnimation.keyframes, overlayAnimation.options) animateTo(this.overlay, overlayAnimation.keyframes, overlayAnimation.options)

Wyświetl plik

@ -15,7 +15,7 @@ export default css`
.drawer { .drawer {
top: 0; top: 0;
left: 0; inset-inline-start: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
pointer-events: none; pointer-events: none;
@ -52,36 +52,36 @@ export default css`
.drawer--top .drawer__panel { .drawer--top .drawer__panel {
top: 0; top: 0;
right: auto; inset-inline-end: auto;
bottom: auto; bottom: auto;
left: 0; inset-inline-start: 0;
width: 100%; width: 100%;
height: var(--size); height: var(--size);
} }
.drawer--end .drawer__panel { .drawer--end .drawer__panel {
top: 0; top: 0;
right: 0; inset-inline-end: 0;
bottom: auto; bottom: auto;
left: auto; inset-inline-start: auto;
width: var(--size); width: var(--size);
height: 100%; height: 100%;
} }
.drawer--bottom .drawer__panel { .drawer--bottom .drawer__panel {
top: auto; top: auto;
right: auto; inset-inline-end: auto;
bottom: 0; bottom: 0;
left: 0; inset-inline-start: 0;
width: 100%; width: 100%;
height: var(--size); height: var(--size);
} }
.drawer--start .drawer__panel { .drawer--start .drawer__panel {
top: 0; top: 0;
right: auto; inset-inline-end: auto;
bottom: auto; bottom: auto;
left: 0; inset-inline-start: 0;
width: var(--size); width: var(--size);
height: 100%; height: 100%;
} }

Wyświetl plik

@ -146,7 +146,7 @@ export default class SlDrawer extends LitElement {
}); });
if (slRequestClose.defaultPrevented) { if (slRequestClose.defaultPrevented) {
const animation = getAnimation(this, 'drawer.denyClose'); const animation = getAnimation(this, 'drawer.denyClose', { dir: this.localize.dir() });
animateTo(this.panel, animation.keyframes, animation.options); animateTo(this.panel, animation.keyframes, animation.options);
return; return;
} }
@ -207,8 +207,10 @@ export default class SlDrawer extends LitElement {
} }
}); });
const panelAnimation = getAnimation(this, `drawer.show${uppercaseFirstLetter(this.placement)}`); const panelAnimation = getAnimation(this, `drawer.show${uppercaseFirstLetter(this.placement)}`, {
const overlayAnimation = getAnimation(this, 'drawer.overlay.show'); dir: this.localize.dir()
});
const overlayAnimation = getAnimation(this, 'drawer.overlay.show', { dir: this.localize.dir() });
await Promise.all([ await Promise.all([
animateTo(this.panel, panelAnimation.keyframes, panelAnimation.options), animateTo(this.panel, panelAnimation.keyframes, panelAnimation.options),
animateTo(this.overlay, overlayAnimation.keyframes, overlayAnimation.options) animateTo(this.overlay, overlayAnimation.keyframes, overlayAnimation.options)
@ -222,8 +224,10 @@ export default class SlDrawer extends LitElement {
unlockBodyScrolling(this); unlockBodyScrolling(this);
await Promise.all([stopAnimations(this.drawer), stopAnimations(this.overlay)]); await Promise.all([stopAnimations(this.drawer), stopAnimations(this.overlay)]);
const panelAnimation = getAnimation(this, `drawer.hide${uppercaseFirstLetter(this.placement)}`); const panelAnimation = getAnimation(this, `drawer.hide${uppercaseFirstLetter(this.placement)}`, {
const overlayAnimation = getAnimation(this, 'drawer.overlay.hide'); dir: this.localize.dir()
});
const overlayAnimation = getAnimation(this, 'drawer.overlay.hide', { dir: this.localize.dir() });
await Promise.all([ await Promise.all([
animateTo(this.panel, panelAnimation.keyframes, panelAnimation.options), animateTo(this.panel, panelAnimation.keyframes, panelAnimation.options),
animateTo(this.overlay, overlayAnimation.keyframes, overlayAnimation.options) animateTo(this.overlay, overlayAnimation.keyframes, overlayAnimation.options)
@ -255,6 +259,7 @@ export default class SlDrawer extends LitElement {
'drawer--start': this.placement === 'start', 'drawer--start': this.placement === 'start',
'drawer--contained': this.contained, 'drawer--contained': this.contained,
'drawer--fixed': !this.contained, 'drawer--fixed': !this.contained,
'drawer--rtl': this.localize.dir() === 'rtl',
'drawer--has-footer': this.hasSlotController.test('footer') 'drawer--has-footer': this.hasSlotController.test('footer')
})} })}
@keydown=${this.handleKeyDown} @keydown=${this.handleKeyDown}
@ -328,6 +333,10 @@ setDefaultAnimation('drawer.showEnd', {
{ opacity: 0, transform: 'translateX(100%)' }, { opacity: 0, transform: 'translateX(100%)' },
{ opacity: 1, transform: 'translateX(0)' } { opacity: 1, transform: 'translateX(0)' }
], ],
rtlKeyframes: [
{ opacity: 0, transform: 'translateX(-100%)' },
{ opacity: 1, transform: 'translateX(0)' }
],
options: { duration: 250, easing: 'ease' } options: { duration: 250, easing: 'ease' }
}); });
@ -336,6 +345,10 @@ setDefaultAnimation('drawer.hideEnd', {
{ opacity: 1, transform: 'translateX(0)' }, { opacity: 1, transform: 'translateX(0)' },
{ opacity: 0, transform: 'translateX(100%)' } { opacity: 0, transform: 'translateX(100%)' }
], ],
rtlKeyframes: [
{ opacity: 1, transform: 'translateX(0)' },
{ opacity: 0, transform: 'translateX(-100%)' }
],
options: { duration: 250, easing: 'ease' } options: { duration: 250, easing: 'ease' }
}); });
@ -362,6 +375,10 @@ setDefaultAnimation('drawer.showStart', {
{ opacity: 0, transform: 'translateX(-100%)' }, { opacity: 0, transform: 'translateX(-100%)' },
{ opacity: 1, transform: 'translateX(0)' } { opacity: 1, transform: 'translateX(0)' }
], ],
rtlKeyframes: [
{ opacity: 0, transform: 'translateX(100%)' },
{ opacity: 1, transform: 'translateX(0)' }
],
options: { duration: 250, easing: 'ease' } options: { duration: 250, easing: 'ease' }
}); });
@ -370,6 +387,10 @@ setDefaultAnimation('drawer.hideStart', {
{ opacity: 1, transform: 'translateX(0)' }, { opacity: 1, transform: 'translateX(0)' },
{ opacity: 0, transform: 'translateX(-100%)' } { opacity: 0, transform: 'translateX(-100%)' }
], ],
rtlKeyframes: [
{ opacity: 1, transform: 'translateX(0)' },
{ opacity: 0, transform: 'translateX(100%)' }
],
options: { duration: 250, easing: 'ease' } options: { duration: 250, easing: 'ease' }
}); });

Wyświetl plik

@ -8,6 +8,7 @@ import { scrollIntoView } from '../../internal/scroll';
import { getTabbableBoundary } from '../../internal/tabbable'; import { getTabbableBoundary } from '../../internal/tabbable';
import { watch } from '../../internal/watch'; import { watch } from '../../internal/watch';
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry'; import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
import { LocalizeController } from '../../utilities/localize';
import styles from './dropdown.styles'; import styles from './dropdown.styles';
import type SlButton from '../../components/button/button'; import type SlButton from '../../components/button/button';
import type SlIconButton from '../../components/icon-button/icon-button'; import type SlIconButton from '../../components/icon-button/icon-button';
@ -41,6 +42,7 @@ export default class SlDropdown extends LitElement {
@query('.dropdown__panel') panel: HTMLElement; @query('.dropdown__panel') panel: HTMLElement;
@query('.dropdown__positioner') positioner: HTMLElement; @query('.dropdown__positioner') positioner: HTMLElement;
private readonly localize = new LocalizeController(this);
private positionerCleanup: ReturnType<typeof autoUpdate> | undefined; private positionerCleanup: ReturnType<typeof autoUpdate> | undefined;
/** Indicates whether or not the dropdown is open. You can use this in lieu of the show/hide methods. */ /** Indicates whether or not the dropdown is open. You can use this in lieu of the show/hide methods. */
@ -371,7 +373,7 @@ export default class SlDropdown extends LitElement {
await stopAnimations(this); await stopAnimations(this);
this.startPositioner(); this.startPositioner();
this.panel.hidden = false; this.panel.hidden = false;
const { keyframes, options } = getAnimation(this, 'dropdown.show'); const { keyframes, options } = getAnimation(this, 'dropdown.show', { dir: this.localize.dir() });
await animateTo(this.panel, keyframes, options); await animateTo(this.panel, keyframes, options);
emit(this, 'sl-after-show'); emit(this, 'sl-after-show');
@ -381,7 +383,7 @@ export default class SlDropdown extends LitElement {
this.removeOpenListeners(); this.removeOpenListeners();
await stopAnimations(this); await stopAnimations(this);
const { keyframes, options } = getAnimation(this, 'dropdown.hide'); const { keyframes, options } = getAnimation(this, 'dropdown.hide', { dir: this.localize.dir() });
await animateTo(this.panel, keyframes, options); await animateTo(this.panel, keyframes, options);
this.panel.hidden = true; this.panel.hidden = true;
this.stopPositioner(); this.stopPositioner();

Wyświetl plik

@ -6,6 +6,7 @@ import { animateTo, parseDuration, stopAnimations } from '../../internal/animate
import { emit, waitForEvent } from '../../internal/event'; import { emit, waitForEvent } from '../../internal/event';
import { watch } from '../../internal/watch'; import { watch } from '../../internal/watch';
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry'; import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
import { LocalizeController } from '../../utilities/localize';
import styles from './tooltip.styles'; import styles from './tooltip.styles';
/** /**
@ -39,6 +40,7 @@ export default class SlTooltip extends LitElement {
private target: HTMLElement; private target: HTMLElement;
private hoverTimeout: number; private hoverTimeout: number;
private readonly localize = new LocalizeController(this);
private positionerCleanup: ReturnType<typeof autoUpdate> | undefined; private positionerCleanup: ReturnType<typeof autoUpdate> | undefined;
/** The tooltip's content. Alternatively, you can use the content slot. */ /** The tooltip's content. Alternatively, you can use the content slot. */
@ -220,7 +222,7 @@ export default class SlTooltip extends LitElement {
await stopAnimations(this.tooltip); await stopAnimations(this.tooltip);
this.startPositioner(); this.startPositioner();
this.tooltip.hidden = false; this.tooltip.hidden = false;
const { keyframes, options } = getAnimation(this, 'tooltip.show'); const { keyframes, options } = getAnimation(this, 'tooltip.show', { dir: this.localize.dir() });
await animateTo(this.tooltip, keyframes, options); await animateTo(this.tooltip, keyframes, options);
emit(this, 'sl-after-show'); emit(this, 'sl-after-show');
@ -229,7 +231,7 @@ export default class SlTooltip extends LitElement {
emit(this, 'sl-hide'); emit(this, 'sl-hide');
await stopAnimations(this.tooltip); await stopAnimations(this.tooltip);
const { keyframes, options } = getAnimation(this, 'tooltip.hide'); const { keyframes, options } = getAnimation(this, 'tooltip.hide', { dir: this.localize.dir() });
await animateTo(this.tooltip, keyframes, options); await animateTo(this.tooltip, keyframes, options);
this.tooltip.hidden = true; this.tooltip.hidden = true;
this.stopPositioner(); this.stopPositioner();

Wyświetl plik

@ -1,12 +1,21 @@
interface ElementAnimation { export interface ElementAnimation {
keyframes: Keyframe[]; keyframes: Keyframe[];
rtlKeyframes?: Keyframe[];
options?: KeyframeAnimationOptions; options?: KeyframeAnimationOptions;
} }
interface ElementAnimationMap { export interface ElementAnimationMap {
[animationName: string]: ElementAnimation; [animationName: string]: ElementAnimation;
} }
export interface GetAnimationOptions {
/**
* The component's directionality. When set to "rtl", `rtlKeyframes` will be preferred over `keyframes` where
* available using getAnimation().
*/
dir: string;
}
const defaultAnimationRegistry = new Map<string, ElementAnimation>(); const defaultAnimationRegistry = new Map<string, ElementAnimation>();
const customAnimationRegistry = new WeakMap<Element, ElementAnimationMap>(); const customAnimationRegistry = new WeakMap<Element, ElementAnimationMap>();
@ -14,6 +23,21 @@ function ensureAnimation(animation: ElementAnimation | null) {
return animation ?? { keyframes: [], options: { duration: 0 } }; return animation ?? { keyframes: [], options: { duration: 0 } };
} }
//
// Given an ElementAnimation, this function returns a new ElementAnimation where the keyframes property reflects either
// keyframes or rtlKeyframes depending on the specified directionality.
//
function getLogicalAnimation(animation: ElementAnimation, dir: string) {
if (dir.toLowerCase() === 'rtl') {
return {
keyframes: animation.rtlKeyframes || animation.keyframes,
options: animation.options
};
}
return animation;
}
// //
// Sets a default animation. Components should use the `name.animation` for primary animations and `name.part.animation` // Sets a default animation. Components should use the `name.animation` for primary animations and `name.part.animation`
// for secondary animations, e.g. `dialog.show` and `dialog.overlay.show`. For modifiers, use `drawer.showTop`. // for secondary animations, e.g. `dialog.show` and `dialog.overlay.show`. For modifiers, use `drawer.showTop`.
@ -32,18 +56,18 @@ export function setAnimation(el: Element, animationName: string, animation: Elem
// //
// Gets an element's animation. Falls back to the default if no animation is found. // Gets an element's animation. Falls back to the default if no animation is found.
// //
export function getAnimation(el: Element, animationName: string) { export function getAnimation(el: Element, animationName: string, options: GetAnimationOptions) {
const customAnimation = customAnimationRegistry.get(el); const customAnimation = customAnimationRegistry.get(el);
// Check for a custom animation // Check for a custom animation
if (customAnimation?.[animationName]) { if (customAnimation?.[animationName]) {
return customAnimation[animationName]; return getLogicalAnimation(customAnimation[animationName], options.dir);
} }
// Check for a default animation // Check for a default animation
const defaultAnimation = defaultAnimationRegistry.get(animationName); const defaultAnimation = defaultAnimationRegistry.get(animationName);
if (defaultAnimation) { if (defaultAnimation) {
return defaultAnimation; return getLogicalAnimation(defaultAnimation, options.dir);
} }
// Fall back to an empty animation // Fall back to an empty animation