kopia lustrzana https://github.com/shoelace-style/shoelace
fixes #784
rodzic
c8d92e41b2
commit
8a28d66393
|
@ -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.
|
||||
|
||||
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.
|
||||
|
||||
|
@ -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`.
|
||||
|
||||
In this example, only the target dialog will use a custom show animation.
|
||||
|
|
|
@ -8,6 +8,8 @@ _During the beta period, these restrictions may be relaxed in the event of a mis
|
|||
|
||||
## 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 the toast stack [#785](https://github.com/shoelace-style/shoelace/issues/785)
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import { emit, waitForEvent } from '../../internal/event';
|
|||
import { HasSlotController } from '../../internal/slot';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import styles from './alert.styles';
|
||||
|
||||
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 readonly hasSlotController = new HasSlotController(this, 'icon', 'suffix');
|
||||
private readonly localize = new LocalizeController(this);
|
||||
|
||||
@query('[part="base"]') base: HTMLElement;
|
||||
|
||||
|
@ -148,7 +150,7 @@ export default class SlAlert extends LitElement {
|
|||
|
||||
await stopAnimations(this.base);
|
||||
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);
|
||||
|
||||
emit(this, 'sl-after-show');
|
||||
|
@ -159,7 +161,7 @@ export default class SlAlert extends LitElement {
|
|||
clearTimeout(this.autoHideTimeout);
|
||||
|
||||
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);
|
||||
this.base.hidden = true;
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import { animateTo, shimKeyframesHeightAuto, stopAnimations } from '../../intern
|
|||
import { emit, waitForEvent } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import styles from './details.styles';
|
||||
|
||||
/**
|
||||
|
@ -39,6 +40,8 @@ export default class SlDetails extends LitElement {
|
|||
@query('.details__header') header: 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. */
|
||||
@property({ type: Boolean, reflect: true }) open = false;
|
||||
|
||||
|
@ -116,7 +119,7 @@ export default class SlDetails extends LitElement {
|
|||
await stopAnimations(this.body);
|
||||
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);
|
||||
this.body.style.height = 'auto';
|
||||
|
||||
|
@ -127,7 +130,7 @@ export default class SlDetails extends LitElement {
|
|||
|
||||
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);
|
||||
this.body.hidden = true;
|
||||
this.body.style.height = 'auto';
|
||||
|
|
|
@ -129,7 +129,7 @@ export default class SlDialog extends LitElement {
|
|||
});
|
||||
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
@ -187,8 +187,8 @@ export default class SlDialog extends LitElement {
|
|||
}
|
||||
});
|
||||
|
||||
const panelAnimation = getAnimation(this, 'dialog.show');
|
||||
const overlayAnimation = getAnimation(this, 'dialog.overlay.show');
|
||||
const panelAnimation = getAnimation(this, 'dialog.show', { dir: this.localize.dir() });
|
||||
const overlayAnimation = getAnimation(this, 'dialog.overlay.show', { dir: this.localize.dir() });
|
||||
await Promise.all([
|
||||
animateTo(this.panel, panelAnimation.keyframes, panelAnimation.options),
|
||||
animateTo(this.overlay, overlayAnimation.keyframes, overlayAnimation.options)
|
||||
|
@ -201,8 +201,8 @@ export default class SlDialog extends LitElement {
|
|||
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');
|
||||
const panelAnimation = getAnimation(this, 'dialog.hide', { dir: this.localize.dir() });
|
||||
const overlayAnimation = getAnimation(this, 'dialog.overlay.hide', { dir: this.localize.dir() });
|
||||
await Promise.all([
|
||||
animateTo(this.panel, panelAnimation.keyframes, panelAnimation.options),
|
||||
animateTo(this.overlay, overlayAnimation.keyframes, overlayAnimation.options)
|
||||
|
|
|
@ -15,7 +15,7 @@ export default css`
|
|||
|
||||
.drawer {
|
||||
top: 0;
|
||||
left: 0;
|
||||
inset-inline-start: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
|
@ -52,36 +52,36 @@ export default css`
|
|||
|
||||
.drawer--top .drawer__panel {
|
||||
top: 0;
|
||||
right: auto;
|
||||
inset-inline-end: auto;
|
||||
bottom: auto;
|
||||
left: 0;
|
||||
inset-inline-start: 0;
|
||||
width: 100%;
|
||||
height: var(--size);
|
||||
}
|
||||
|
||||
.drawer--end .drawer__panel {
|
||||
top: 0;
|
||||
right: 0;
|
||||
inset-inline-end: 0;
|
||||
bottom: auto;
|
||||
left: auto;
|
||||
inset-inline-start: auto;
|
||||
width: var(--size);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.drawer--bottom .drawer__panel {
|
||||
top: auto;
|
||||
right: auto;
|
||||
inset-inline-end: auto;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
inset-inline-start: 0;
|
||||
width: 100%;
|
||||
height: var(--size);
|
||||
}
|
||||
|
||||
.drawer--start .drawer__panel {
|
||||
top: 0;
|
||||
right: auto;
|
||||
inset-inline-end: auto;
|
||||
bottom: auto;
|
||||
left: 0;
|
||||
inset-inline-start: 0;
|
||||
width: var(--size);
|
||||
height: 100%;
|
||||
}
|
||||
|
|
|
@ -146,7 +146,7 @@ export default class SlDrawer extends LitElement {
|
|||
});
|
||||
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
@ -207,8 +207,10 @@ export default class SlDrawer extends LitElement {
|
|||
}
|
||||
});
|
||||
|
||||
const panelAnimation = getAnimation(this, `drawer.show${uppercaseFirstLetter(this.placement)}`);
|
||||
const overlayAnimation = getAnimation(this, 'drawer.overlay.show');
|
||||
const panelAnimation = getAnimation(this, `drawer.show${uppercaseFirstLetter(this.placement)}`, {
|
||||
dir: this.localize.dir()
|
||||
});
|
||||
const overlayAnimation = getAnimation(this, 'drawer.overlay.show', { dir: this.localize.dir() });
|
||||
await Promise.all([
|
||||
animateTo(this.panel, panelAnimation.keyframes, panelAnimation.options),
|
||||
animateTo(this.overlay, overlayAnimation.keyframes, overlayAnimation.options)
|
||||
|
@ -222,8 +224,10 @@ export default class SlDrawer extends LitElement {
|
|||
unlockBodyScrolling(this);
|
||||
|
||||
await Promise.all([stopAnimations(this.drawer), stopAnimations(this.overlay)]);
|
||||
const panelAnimation = getAnimation(this, `drawer.hide${uppercaseFirstLetter(this.placement)}`);
|
||||
const overlayAnimation = getAnimation(this, 'drawer.overlay.hide');
|
||||
const panelAnimation = getAnimation(this, `drawer.hide${uppercaseFirstLetter(this.placement)}`, {
|
||||
dir: this.localize.dir()
|
||||
});
|
||||
const overlayAnimation = getAnimation(this, 'drawer.overlay.hide', { dir: this.localize.dir() });
|
||||
await Promise.all([
|
||||
animateTo(this.panel, panelAnimation.keyframes, panelAnimation.options),
|
||||
animateTo(this.overlay, overlayAnimation.keyframes, overlayAnimation.options)
|
||||
|
@ -255,6 +259,7 @@ export default class SlDrawer extends LitElement {
|
|||
'drawer--start': this.placement === 'start',
|
||||
'drawer--contained': this.contained,
|
||||
'drawer--fixed': !this.contained,
|
||||
'drawer--rtl': this.localize.dir() === 'rtl',
|
||||
'drawer--has-footer': this.hasSlotController.test('footer')
|
||||
})}
|
||||
@keydown=${this.handleKeyDown}
|
||||
|
@ -328,6 +333,10 @@ setDefaultAnimation('drawer.showEnd', {
|
|||
{ opacity: 0, transform: 'translateX(100%)' },
|
||||
{ opacity: 1, transform: 'translateX(0)' }
|
||||
],
|
||||
rtlKeyframes: [
|
||||
{ opacity: 0, transform: 'translateX(-100%)' },
|
||||
{ opacity: 1, transform: 'translateX(0)' }
|
||||
],
|
||||
options: { duration: 250, easing: 'ease' }
|
||||
});
|
||||
|
||||
|
@ -336,6 +345,10 @@ setDefaultAnimation('drawer.hideEnd', {
|
|||
{ opacity: 1, transform: 'translateX(0)' },
|
||||
{ opacity: 0, transform: 'translateX(100%)' }
|
||||
],
|
||||
rtlKeyframes: [
|
||||
{ opacity: 1, transform: 'translateX(0)' },
|
||||
{ opacity: 0, transform: 'translateX(-100%)' }
|
||||
],
|
||||
options: { duration: 250, easing: 'ease' }
|
||||
});
|
||||
|
||||
|
@ -362,6 +375,10 @@ setDefaultAnimation('drawer.showStart', {
|
|||
{ opacity: 0, transform: 'translateX(-100%)' },
|
||||
{ opacity: 1, transform: 'translateX(0)' }
|
||||
],
|
||||
rtlKeyframes: [
|
||||
{ opacity: 0, transform: 'translateX(100%)' },
|
||||
{ opacity: 1, transform: 'translateX(0)' }
|
||||
],
|
||||
options: { duration: 250, easing: 'ease' }
|
||||
});
|
||||
|
||||
|
@ -370,6 +387,10 @@ setDefaultAnimation('drawer.hideStart', {
|
|||
{ opacity: 1, transform: 'translateX(0)' },
|
||||
{ opacity: 0, transform: 'translateX(-100%)' }
|
||||
],
|
||||
rtlKeyframes: [
|
||||
{ opacity: 1, transform: 'translateX(0)' },
|
||||
{ opacity: 0, transform: 'translateX(100%)' }
|
||||
],
|
||||
options: { duration: 250, easing: 'ease' }
|
||||
});
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import { scrollIntoView } from '../../internal/scroll';
|
|||
import { getTabbableBoundary } from '../../internal/tabbable';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import styles from './dropdown.styles';
|
||||
import type SlButton from '../../components/button/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__positioner') positioner: HTMLElement;
|
||||
|
||||
private readonly localize = new LocalizeController(this);
|
||||
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. */
|
||||
|
@ -371,7 +373,7 @@ export default class SlDropdown extends LitElement {
|
|||
await stopAnimations(this);
|
||||
this.startPositioner();
|
||||
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);
|
||||
|
||||
emit(this, 'sl-after-show');
|
||||
|
@ -381,7 +383,7 @@ export default class SlDropdown extends LitElement {
|
|||
this.removeOpenListeners();
|
||||
|
||||
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);
|
||||
this.panel.hidden = true;
|
||||
this.stopPositioner();
|
||||
|
|
|
@ -6,6 +6,7 @@ import { animateTo, parseDuration, stopAnimations } from '../../internal/animate
|
|||
import { emit, waitForEvent } from '../../internal/event';
|
||||
import { watch } from '../../internal/watch';
|
||||
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
|
||||
import { LocalizeController } from '../../utilities/localize';
|
||||
import styles from './tooltip.styles';
|
||||
|
||||
/**
|
||||
|
@ -39,6 +40,7 @@ export default class SlTooltip extends LitElement {
|
|||
|
||||
private target: HTMLElement;
|
||||
private hoverTimeout: number;
|
||||
private readonly localize = new LocalizeController(this);
|
||||
private positionerCleanup: ReturnType<typeof autoUpdate> | undefined;
|
||||
|
||||
/** 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);
|
||||
this.startPositioner();
|
||||
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);
|
||||
|
||||
emit(this, 'sl-after-show');
|
||||
|
@ -229,7 +231,7 @@ export default class SlTooltip extends LitElement {
|
|||
emit(this, 'sl-hide');
|
||||
|
||||
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);
|
||||
this.tooltip.hidden = true;
|
||||
this.stopPositioner();
|
||||
|
|
|
@ -1,12 +1,21 @@
|
|||
interface ElementAnimation {
|
||||
export interface ElementAnimation {
|
||||
keyframes: Keyframe[];
|
||||
rtlKeyframes?: Keyframe[];
|
||||
options?: KeyframeAnimationOptions;
|
||||
}
|
||||
|
||||
interface ElementAnimationMap {
|
||||
export interface ElementAnimationMap {
|
||||
[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 customAnimationRegistry = new WeakMap<Element, ElementAnimationMap>();
|
||||
|
||||
|
@ -14,6 +23,21 @@ function ensureAnimation(animation: ElementAnimation | null) {
|
|||
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`
|
||||
// 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.
|
||||
//
|
||||
export function getAnimation(el: Element, animationName: string) {
|
||||
export function getAnimation(el: Element, animationName: string, options: GetAnimationOptions) {
|
||||
const customAnimation = customAnimationRegistry.get(el);
|
||||
|
||||
// Check for a custom animation
|
||||
if (customAnimation?.[animationName]) {
|
||||
return customAnimation[animationName];
|
||||
return getLogicalAnimation(customAnimation[animationName], options.dir);
|
||||
}
|
||||
|
||||
// Check for a default animation
|
||||
const defaultAnimation = defaultAnimationRegistry.get(animationName);
|
||||
if (defaultAnimation) {
|
||||
return defaultAnimation;
|
||||
return getLogicalAnimation(defaultAnimation, options.dir);
|
||||
}
|
||||
|
||||
// Fall back to an empty animation
|
||||
|
|
Ładowanie…
Reference in New Issue