kopia lustrzana https://github.com/shoelace-style/shoelace
fix show/hide logic
rodzic
8d8b77ca07
commit
10f045fe6e
|
@ -8,7 +8,17 @@ _During the beta period, these restrictions may be relaxed in the event of a mis
|
|||
|
||||
## Next
|
||||
|
||||
This release addresses an issue with the `open` prop no longer working in a number of components, as a result of the changes in beta.41. It also removes a small but controversial feature that complicated show/hide logic and led to a poor experience for developers and end users.
|
||||
|
||||
There are two ways to show/hide affected components: by calling `show() | hide()` and by toggling the `open` prop. Previously, it was possible to call `event.preventDefault()` in an `sl-show | sl-hide ` handler to stop the component from showing/hiding. The problem becomes obvious when you set `el.open = false`, the event gets canceled, and in the next cycle `el.open` has reverted to `true`. Not only is this unexpected, but it also doesn't play nicely with frameworks. Additionally, this made it impossible to await `show() | hide()` since there was a chance they'd never resolve.
|
||||
|
||||
Technical reasons aside, canceling these events seldom led to a good user experience, so the decision was made to no longer allow `sl-show | sl-hide` to be cancelable.
|
||||
|
||||
- 🚨 BREAKING: `sl-show` and `sl-hide` events are no longer cancelable
|
||||
- Added Iconoir example to the icon docs
|
||||
- Changed the `cancelable` default to `false` for the internal `@event` decorator
|
||||
- Fixed a bug where toggling `open` stopped working in `sl-alert`, `sl-dialog`, `sl-drawer`, `sl-dropdown`, and `sl-tooltip`
|
||||
- Fixed a number of imports that should have been type imports
|
||||
|
||||
## 2.0.0-beta.41
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import { customElement, property, query } from 'lit/decorators';
|
|||
import { classMap } from 'lit-html/directives/class-map';
|
||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
||||
import { event, EventEmitter, watch } from '../../internal/decorators';
|
||||
import { waitForEvent } from '../../internal/event';
|
||||
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
|
||||
import styles from 'sass:./alert.scss';
|
||||
|
||||
|
@ -51,13 +52,13 @@ export default class SlAlert extends LitElement {
|
|||
*/
|
||||
@property({ type: Number }) duration = Infinity;
|
||||
|
||||
/** Emitted when the alert opens. Calling `event.preventDefault()` will prevent it from being opened. */
|
||||
/** Emitted when the alert opens. */
|
||||
@event('sl-show') slShow: EventEmitter<void>;
|
||||
|
||||
/** Emitted after the alert opens and all transitions are complete. */
|
||||
@event('sl-after-show') slAfterShow: EventEmitter<void>;
|
||||
|
||||
/** Emitted when the alert closes. Calling `event.preventDefault()` will prevent it from being closed. */
|
||||
/** Emitted when the alert closes. */
|
||||
@event('sl-hide') slHide: EventEmitter<void>;
|
||||
|
||||
/** Emitted after the alert closes and all transitions are complete. */
|
||||
|
@ -74,52 +75,22 @@ export default class SlAlert extends LitElement {
|
|||
|
||||
/** Shows the alert. */
|
||||
async show() {
|
||||
if (!this.hasInitialized || this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slShow = this.slShow.emit();
|
||||
if (slShow.defaultPrevented) {
|
||||
this.open = false;
|
||||
if (this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.open = true;
|
||||
|
||||
if (this.duration < Infinity) {
|
||||
this.restartAutoHide();
|
||||
}
|
||||
|
||||
await stopAnimations(this.base);
|
||||
this.base.hidden = false;
|
||||
const { keyframes, options } = getAnimation(this, 'alert.show');
|
||||
await animateTo(this.base, keyframes, options);
|
||||
|
||||
this.slAfterShow.emit();
|
||||
return waitForEvent(this, 'sl-after-show');
|
||||
}
|
||||
|
||||
/** Hides the alert */
|
||||
async hide() {
|
||||
if (!this.hasInitialized || !this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slHide = this.slHide.emit();
|
||||
if (slHide.defaultPrevented) {
|
||||
this.open = true;
|
||||
if (!this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.open = false;
|
||||
|
||||
clearTimeout(this.autoHideTimeout);
|
||||
|
||||
await stopAnimations(this.base);
|
||||
const { keyframes, options } = getAnimation(this, 'alert.hide');
|
||||
await animateTo(this.base, keyframes, options);
|
||||
this.base.hidden = true;
|
||||
|
||||
this.slAfterHide.emit();
|
||||
return waitForEvent(this, 'sl-after-hide');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -173,8 +144,38 @@ export default class SlAlert extends LitElement {
|
|||
}
|
||||
|
||||
@watch('open')
|
||||
handleOpenChange() {
|
||||
this.open ? this.show() : this.hide();
|
||||
async handleOpenChange() {
|
||||
if (!this.hasInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.open) {
|
||||
// Show
|
||||
this.slShow.emit();
|
||||
|
||||
if (this.duration < Infinity) {
|
||||
this.restartAutoHide();
|
||||
}
|
||||
|
||||
await stopAnimations(this.base);
|
||||
this.base.hidden = false;
|
||||
const { keyframes, options } = getAnimation(this, 'alert.show');
|
||||
await animateTo(this.base, keyframes, options);
|
||||
|
||||
this.slAfterShow.emit();
|
||||
} else {
|
||||
// Hide
|
||||
this.slHide.emit();
|
||||
|
||||
clearTimeout(this.autoHideTimeout);
|
||||
|
||||
await stopAnimations(this.base);
|
||||
const { keyframes, options } = getAnimation(this, 'alert.hide');
|
||||
await animateTo(this.base, keyframes, options);
|
||||
this.base.hidden = true;
|
||||
|
||||
this.slAfterHide.emit();
|
||||
}
|
||||
}
|
||||
|
||||
@watch('duration')
|
||||
|
|
|
@ -5,8 +5,8 @@ import { ifDefined } from 'lit-html/directives/if-defined';
|
|||
import { styleMap } from 'lit-html/directives/style-map';
|
||||
import { event, EventEmitter, watch } from '../../internal/decorators';
|
||||
import { clamp } from '../../internal/math';
|
||||
import SlDropdown from '../dropdown/dropdown';
|
||||
import SlInput from '../input/input';
|
||||
import type SlDropdown from '../dropdown/dropdown';
|
||||
import type SlInput from '../input/input';
|
||||
import color from 'color';
|
||||
import styles from 'sass:./color-picker.scss';
|
||||
|
||||
|
@ -380,12 +380,6 @@ export default class SlColorPicker extends LitElement {
|
|||
}
|
||||
}
|
||||
|
||||
handleDropdownShow(event: CustomEvent) {
|
||||
if (this.disabled) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
handleDropdownAfterHide() {
|
||||
this.showCopyFeedback = false;
|
||||
}
|
||||
|
@ -774,8 +768,8 @@ export default class SlColorPicker extends LitElement {
|
|||
class="color-dropdown"
|
||||
aria-disabled=${this.disabled ? 'true' : 'false'}
|
||||
.containing-element=${this}
|
||||
?disabled=${this.disabled}
|
||||
?hoist=${this.hoist}
|
||||
@sl-show=${this.handleDropdownShow}
|
||||
@sl-after-hide=${this.handleDropdownAfterHide}
|
||||
>
|
||||
<button
|
||||
|
|
|
@ -3,6 +3,7 @@ import { customElement, property, query } from 'lit/decorators';
|
|||
import { classMap } from 'lit-html/directives/class-map';
|
||||
import { animateTo, stopAnimations, shimKeyframesHeightAuto } from '../../internal/animate';
|
||||
import { event, EventEmitter, watch } from '../../internal/decorators';
|
||||
import { waitForEvent } from '../../internal/event';
|
||||
import { focusVisible } from '../../internal/focus-visible';
|
||||
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
|
||||
import styles from 'sass:./details.scss';
|
||||
|
@ -52,13 +53,13 @@ export default class SlDetails extends LitElement {
|
|||
/** Disables the details so it can't be toggled. */
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
/** Emitted when the details opens. Calling `event.preventDefault()` will prevent it from being opened. */
|
||||
/** Emitted when the details opens. */
|
||||
@event('sl-show') slShow: EventEmitter<void>;
|
||||
|
||||
/** Emitted after the details opens and all transitions are complete. */
|
||||
@event('sl-after-show') slAfterShow: EventEmitter<void>;
|
||||
|
||||
/** Emitted when the details closes. Calling `event.preventDefault()` will prevent it from being closed. */
|
||||
/** Emitted when the details closes. */
|
||||
@event('sl-hide') slHide: EventEmitter<void>;
|
||||
|
||||
/** Emitted after the details closes and all transitions are complete. */
|
||||
|
@ -80,51 +81,24 @@ export default class SlDetails extends LitElement {
|
|||
focusVisible.unobserve(this.details);
|
||||
}
|
||||
|
||||
/** Shows the alert. */
|
||||
/** Shows the details. */
|
||||
async show() {
|
||||
if (!this.hasInitialized || this.open || this.disabled) {
|
||||
if (this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slShow = this.slShow.emit();
|
||||
if (slShow.defaultPrevented) {
|
||||
this.open = false;
|
||||
return;
|
||||
}
|
||||
|
||||
await stopAnimations(this);
|
||||
this.body.hidden = false;
|
||||
this.open = true;
|
||||
|
||||
const { keyframes, options } = getAnimation(this, 'details.show');
|
||||
await animateTo(this.body, shimKeyframesHeightAuto(keyframes, this.body.scrollHeight), options);
|
||||
this.body.style.height = 'auto';
|
||||
|
||||
this.slAfterShow.emit();
|
||||
return waitForEvent(this, 'sl-after-show');
|
||||
}
|
||||
|
||||
/** Hides the alert */
|
||||
/** Hides the details */
|
||||
async hide() {
|
||||
// Prevent subsequent calls to the method, whether manually or triggered by the `open` watcher
|
||||
if (!this.hasInitialized || !this.open || this.disabled) {
|
||||
if (!this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slHide = this.slHide.emit();
|
||||
if (slHide.defaultPrevented) {
|
||||
this.open = true;
|
||||
return;
|
||||
}
|
||||
|
||||
await stopAnimations(this);
|
||||
this.open = false;
|
||||
|
||||
const { keyframes, options } = getAnimation(this, 'details.hide');
|
||||
await animateTo(this.body, shimKeyframesHeightAuto(keyframes, this.body.scrollHeight), options);
|
||||
this.body.hidden = true;
|
||||
this.body.style.height = 'auto';
|
||||
|
||||
this.slAfterHide.emit();
|
||||
return waitForEvent(this, 'sl-after-hide');
|
||||
}
|
||||
|
||||
handleSummaryClick() {
|
||||
|
@ -152,8 +126,36 @@ export default class SlDetails extends LitElement {
|
|||
}
|
||||
|
||||
@watch('open')
|
||||
handleOpenChange() {
|
||||
this.open ? this.show() : this.hide();
|
||||
async handleOpenChange() {
|
||||
if (!this.hasInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.open) {
|
||||
// Show
|
||||
this.slShow.emit();
|
||||
|
||||
await stopAnimations(this);
|
||||
this.body.hidden = false;
|
||||
|
||||
const { keyframes, options } = getAnimation(this, 'details.show');
|
||||
await animateTo(this.body, shimKeyframesHeightAuto(keyframes, this.body.scrollHeight), options);
|
||||
this.body.style.height = 'auto';
|
||||
|
||||
this.slAfterShow.emit();
|
||||
} else {
|
||||
// Hide
|
||||
this.slHide.emit();
|
||||
|
||||
await stopAnimations(this);
|
||||
|
||||
const { keyframes, options } = getAnimation(this, 'details.hide');
|
||||
await animateTo(this.body, shimKeyframesHeightAuto(keyframes, this.body.scrollHeight), options);
|
||||
this.body.hidden = true;
|
||||
this.body.style.height = 'auto';
|
||||
|
||||
this.slAfterHide.emit();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -4,6 +4,7 @@ import { classMap } from 'lit-html/directives/class-map';
|
|||
import { ifDefined } from 'lit-html/directives/if-defined';
|
||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
||||
import { event, EventEmitter, watch } from '../../internal/decorators';
|
||||
import { waitForEvent } from '../../internal/event';
|
||||
import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll';
|
||||
import { hasSlot } from '../../internal/slot';
|
||||
import { isPreventScrollSupported } from '../../internal/support';
|
||||
|
@ -74,13 +75,13 @@ export default class SlDialog extends LitElement {
|
|||
*/
|
||||
@property({ attribute: 'no-header', type: Boolean, reflect: true }) noHeader = false;
|
||||
|
||||
/** Emitted when the dialog opens. Calling `event.preventDefault()` will prevent it from being opened. */
|
||||
/** Emitted when the dialog opens. */
|
||||
@event('sl-show') slShow: EventEmitter<void>;
|
||||
|
||||
/** Emitted after the dialog opens and all transitions are complete. */
|
||||
@event('sl-after-show') slAfterShow: EventEmitter<void>;
|
||||
|
||||
/** Emitted when the dialog closes. Calling `event.preventDefault()` will prevent it from being closed. */
|
||||
/** Emitted when the dialog closes. */
|
||||
@event('sl-hide') slHide: EventEmitter<void>;
|
||||
|
||||
/** Emitted after the dialog closes and all transitions are complete. */
|
||||
|
@ -116,87 +117,24 @@ export default class SlDialog extends LitElement {
|
|||
unlockBodyScrolling(this);
|
||||
}
|
||||
|
||||
/** Shows the dialog */
|
||||
/** Shows the dialog. */
|
||||
async show() {
|
||||
if (!this.hasInitialized || this.open) {
|
||||
if (this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slShow = this.slShow.emit();
|
||||
if (slShow.defaultPrevented) {
|
||||
this.open = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.originalTrigger = document.activeElement as HTMLElement;
|
||||
this.open = true;
|
||||
this.modal.activate();
|
||||
|
||||
lockBodyScrolling(this);
|
||||
|
||||
await Promise.all([stopAnimations(this.dialog), stopAnimations(this.overlay)]);
|
||||
this.dialog.hidden = false;
|
||||
|
||||
// Browsers that support el.focus({ preventScroll }) can set initial focus immediately
|
||||
if (hasPreventScroll) {
|
||||
const slInitialFocus = this.slInitialFocus.emit();
|
||||
if (!slInitialFocus.defaultPrevented) {
|
||||
this.panel.focus({ preventScroll: 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();
|
||||
return waitForEvent(this, 'sl-after-show');
|
||||
}
|
||||
|
||||
/** Hides the dialog */
|
||||
async hide() {
|
||||
if (!this.hasInitialized || !this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slHide = this.slHide.emit();
|
||||
if (slHide.defaultPrevented) {
|
||||
this.open = true;
|
||||
if (!this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.open = false;
|
||||
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
|
||||
const trigger = this.originalTrigger;
|
||||
if (trigger && typeof trigger.focus === 'function') {
|
||||
setTimeout(() => trigger.focus());
|
||||
}
|
||||
|
||||
this.slAfterHide.emit();
|
||||
return waitForEvent(this, 'sl-after-hide');
|
||||
}
|
||||
|
||||
handleCloseClick() {
|
||||
|
@ -211,8 +149,71 @@ export default class SlDialog extends LitElement {
|
|||
}
|
||||
|
||||
@watch('open')
|
||||
handleOpenChange() {
|
||||
this.open ? this.show() : this.hide();
|
||||
async handleOpenChange() {
|
||||
if (!this.hasInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.open) {
|
||||
// Show
|
||||
this.slShow.emit();
|
||||
this.originalTrigger = document.activeElement as HTMLElement;
|
||||
this.modal.activate();
|
||||
|
||||
lockBodyScrolling(this);
|
||||
|
||||
await Promise.all([stopAnimations(this.dialog), stopAnimations(this.overlay)]);
|
||||
this.dialog.hidden = false;
|
||||
|
||||
// Browsers that support el.focus({ preventScroll }) can set initial focus immediately
|
||||
if (hasPreventScroll) {
|
||||
const slInitialFocus = this.slInitialFocus.emit();
|
||||
if (!slInitialFocus.defaultPrevented) {
|
||||
this.panel.focus({ preventScroll: 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();
|
||||
} else {
|
||||
// Hide
|
||||
this.slHide.emit();
|
||||
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
|
||||
const trigger = this.originalTrigger;
|
||||
if (trigger && typeof trigger.focus === 'function') {
|
||||
setTimeout(() => trigger.focus());
|
||||
}
|
||||
|
||||
this.slAfterHide.emit();
|
||||
}
|
||||
}
|
||||
|
||||
handleOverlayClick() {
|
||||
|
|
|
@ -4,6 +4,7 @@ import { classMap } from 'lit-html/directives/class-map';
|
|||
import { ifDefined } from 'lit-html/directives/if-defined';
|
||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
||||
import { event, EventEmitter, watch } from '../../internal/decorators';
|
||||
import { waitForEvent } from '../../internal/event';
|
||||
import { lockBodyScrolling, unlockBodyScrolling } from '../../internal/scroll';
|
||||
import { hasSlot } from '../../internal/slot';
|
||||
import { uppercaseFirstLetter } from '../../internal/string';
|
||||
|
@ -91,13 +92,13 @@ export default class SlDrawer extends LitElement {
|
|||
*/
|
||||
@property({ attribute: 'no-header', type: Boolean, reflect: true }) noHeader = false;
|
||||
|
||||
/** Emitted when the drawer opens. Calling `event.preventDefault()` will prevent it from being opened. */
|
||||
/** Emitted when the drawer opens. */
|
||||
@event('sl-show') slShow: EventEmitter<void>;
|
||||
|
||||
/** Emitted after the drawer opens and all transitions are complete. */
|
||||
@event('sl-after-show') slAfterShow: EventEmitter<void>;
|
||||
|
||||
/** Emitted when the drawer closes. Calling `event.preventDefault()` will prevent it from being closed. */
|
||||
/** Emitted when the drawer closes. */
|
||||
@event('sl-hide') slHide: EventEmitter<void>;
|
||||
|
||||
/** Emitted after the drawer closes and all transitions are complete. */
|
||||
|
@ -130,90 +131,24 @@ export default class SlDrawer extends LitElement {
|
|||
unlockBodyScrolling(this);
|
||||
}
|
||||
|
||||
/** Shows the drawer */
|
||||
/** Shows the drawer. */
|
||||
async show() {
|
||||
if (!this.hasInitialized || this.open) {
|
||||
if (this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slShow = this.slShow.emit();
|
||||
if (slShow.defaultPrevented) {
|
||||
this.open = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.originalTrigger = document.activeElement as HTMLElement;
|
||||
this.open = true;
|
||||
|
||||
// Lock body scrolling only if the drawer isn't contained
|
||||
if (!this.contained) {
|
||||
this.modal.activate();
|
||||
lockBodyScrolling(this);
|
||||
}
|
||||
|
||||
await Promise.all([stopAnimations(this.drawer), stopAnimations(this.overlay)]);
|
||||
this.drawer.hidden = false;
|
||||
|
||||
// Browsers that support el.focus({ preventScroll }) can set initial focus immediately
|
||||
if (hasPreventScroll) {
|
||||
const slInitialFocus = this.slInitialFocus.emit();
|
||||
if (!slInitialFocus.defaultPrevented) {
|
||||
this.panel.focus({ preventScroll: true });
|
||||
}
|
||||
}
|
||||
|
||||
const panelAnimation = getAnimation(this, `drawer.show${uppercaseFirstLetter(this.placement)}`);
|
||||
const overlayAnimation = getAnimation(this, 'drawer.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();
|
||||
return waitForEvent(this, 'sl-after-show');
|
||||
}
|
||||
|
||||
/** Hides the drawer */
|
||||
async hide() {
|
||||
if (!this.hasInitialized || !this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slHide = this.slHide.emit();
|
||||
if (slHide.defaultPrevented) {
|
||||
this.open = true;
|
||||
if (!this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.open = false;
|
||||
this.modal.deactivate();
|
||||
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');
|
||||
await Promise.all([
|
||||
animateTo(this.panel, panelAnimation.keyframes, panelAnimation.options),
|
||||
animateTo(this.overlay, overlayAnimation.keyframes, overlayAnimation.options)
|
||||
]);
|
||||
|
||||
this.drawer.hidden = true;
|
||||
|
||||
// Restore focus to the original trigger
|
||||
const trigger = this.originalTrigger;
|
||||
if (trigger && typeof trigger.focus === 'function') {
|
||||
setTimeout(() => trigger.focus());
|
||||
}
|
||||
|
||||
this.slAfterHide.emit();
|
||||
return waitForEvent(this, 'sl-after-hide');
|
||||
}
|
||||
|
||||
handleCloseClick() {
|
||||
|
@ -228,8 +163,74 @@ export default class SlDrawer extends LitElement {
|
|||
}
|
||||
|
||||
@watch('open')
|
||||
handleOpenChange() {
|
||||
this.open ? this.show() : this.hide();
|
||||
async handleOpenChange() {
|
||||
if (!this.hasInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.open) {
|
||||
// Show
|
||||
this.slShow.emit();
|
||||
this.originalTrigger = document.activeElement as HTMLElement;
|
||||
|
||||
// Lock body scrolling only if the drawer isn't contained
|
||||
if (!this.contained) {
|
||||
this.modal.activate();
|
||||
lockBodyScrolling(this);
|
||||
}
|
||||
|
||||
await Promise.all([stopAnimations(this.drawer), stopAnimations(this.overlay)]);
|
||||
this.drawer.hidden = false;
|
||||
|
||||
// Browsers that support el.focus({ preventScroll }) can set initial focus immediately
|
||||
if (hasPreventScroll) {
|
||||
const slInitialFocus = this.slInitialFocus.emit();
|
||||
if (!slInitialFocus.defaultPrevented) {
|
||||
this.panel.focus({ preventScroll: true });
|
||||
}
|
||||
}
|
||||
|
||||
const panelAnimation = getAnimation(this, `drawer.show${uppercaseFirstLetter(this.placement)}`);
|
||||
const overlayAnimation = getAnimation(this, 'drawer.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();
|
||||
} else {
|
||||
// Hide
|
||||
this.slHide.emit();
|
||||
this.modal.deactivate();
|
||||
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');
|
||||
await Promise.all([
|
||||
animateTo(this.panel, panelAnimation.keyframes, panelAnimation.options),
|
||||
animateTo(this.overlay, overlayAnimation.keyframes, overlayAnimation.options)
|
||||
]);
|
||||
|
||||
this.drawer.hidden = true;
|
||||
|
||||
// Restore focus to the original trigger
|
||||
const trigger = this.originalTrigger;
|
||||
if (trigger && typeof trigger.focus === 'function') {
|
||||
setTimeout(() => trigger.focus());
|
||||
}
|
||||
|
||||
this.slAfterHide.emit();
|
||||
}
|
||||
}
|
||||
|
||||
handleOverlayClick() {
|
||||
|
|
|
@ -4,11 +4,12 @@ import { classMap } from 'lit-html/directives/class-map';
|
|||
import { Instance as PopperInstance, createPopper } from '@popperjs/core/dist/esm';
|
||||
import { animateTo, stopAnimations } from '../../internal/animate';
|
||||
import { event, EventEmitter, watch } from '../../internal/decorators';
|
||||
import { waitForEvent } from '../../internal/event';
|
||||
import { scrollIntoView } from '../../internal/scroll';
|
||||
import { getNearestTabbableElement } from '../../internal/tabbable';
|
||||
import { setDefaultAnimation, getAnimation } from '../../utilities/animation-registry';
|
||||
import SlMenu from '../menu/menu';
|
||||
import SlMenuItem from '../menu-item/menu-item';
|
||||
import type SlMenu from '../menu/menu';
|
||||
import type SlMenuItem from '../menu-item/menu-item';
|
||||
import styles from 'sass:./dropdown.scss';
|
||||
|
||||
let id = 0;
|
||||
|
@ -60,6 +61,9 @@ export default class SlDropdown extends LitElement {
|
|||
| 'left-start'
|
||||
| 'left-end' = 'bottom-start';
|
||||
|
||||
/** Disables the dropdown so the panel will not open. */
|
||||
@property({ type: Boolean }) disabled = false;
|
||||
|
||||
/** Determines whether the dropdown should hide when a menu item is selected. */
|
||||
@property({ attribute: 'close-on-select', type: Boolean, reflect: true }) closeOnSelect = true;
|
||||
|
||||
|
@ -78,13 +82,13 @@ export default class SlDropdown extends LitElement {
|
|||
*/
|
||||
@property({ type: Boolean }) hoist = false;
|
||||
|
||||
/** Emitted when the dropdown opens. Calling `event.preventDefault()` will prevent it from being opened. */
|
||||
/** Emitted when the dropdown opens. */
|
||||
@event('sl-show') slShow: EventEmitter<void>;
|
||||
|
||||
/** Emitted after the dropdown opens and all animations are complete. */
|
||||
@event('sl-after-show') slAfterShow: EventEmitter<void>;
|
||||
|
||||
/** Emitted when the dropdown closes. Calling `event.preventDefault()` will prevent it from being closed. */
|
||||
/** Emitted when the dropdown closes. */
|
||||
@event('sl-hide') slHide: EventEmitter<void>;
|
||||
|
||||
/** Emitted after the dropdown closes and all animations are complete. */
|
||||
|
@ -329,60 +333,24 @@ export default class SlDropdown extends LitElement {
|
|||
}
|
||||
}
|
||||
|
||||
/** Shows the dropdown panel */
|
||||
/** Shows the dropdown panel. */
|
||||
async show() {
|
||||
// Prevent subsequent calls to the method, whether manually or triggered by the `open` watcher
|
||||
if (!this.hasInitialized || this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slShow = this.slShow.emit();
|
||||
if (slShow.defaultPrevented) {
|
||||
this.open = false;
|
||||
if (this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.open = true;
|
||||
|
||||
this.panel.addEventListener('sl-activate', this.handleMenuItemActivate);
|
||||
this.panel.addEventListener('sl-select', this.handlePanelSelect);
|
||||
document.addEventListener('keydown', this.handleDocumentKeyDown);
|
||||
document.addEventListener('mousedown', this.handleDocumentMouseDown);
|
||||
|
||||
await stopAnimations(this);
|
||||
this.panel.hidden = false;
|
||||
const { keyframes, options } = getAnimation(this, 'dropdown.show');
|
||||
await animateTo(this.panel, keyframes, options);
|
||||
|
||||
this.slAfterShow.emit();
|
||||
return waitForEvent(this, 'sl-after-show');
|
||||
}
|
||||
|
||||
/** Hides the dropdown panel */
|
||||
async hide() {
|
||||
// Prevent subsequent calls to the method, whether manually or triggered by the `open` watcher
|
||||
if (!this.hasInitialized || !this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slHide = this.slHide.emit();
|
||||
if (slHide.defaultPrevented) {
|
||||
this.open = true;
|
||||
if (!this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.open = false;
|
||||
|
||||
this.panel.removeEventListener('sl-activate', this.handleMenuItemActivate);
|
||||
this.panel.removeEventListener('sl-select', this.handlePanelSelect);
|
||||
document.addEventListener('keydown', this.handleDocumentKeyDown);
|
||||
document.removeEventListener('mousedown', this.handleDocumentMouseDown);
|
||||
|
||||
await stopAnimations(this);
|
||||
const { keyframes, options } = getAnimation(this, 'dropdown.hide');
|
||||
await animateTo(this.panel, keyframes, options);
|
||||
this.panel.hidden = true;
|
||||
|
||||
this.slAfterHide.emit();
|
||||
return waitForEvent(this, 'sl-after-hide');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -398,10 +366,42 @@ export default class SlDropdown extends LitElement {
|
|||
}
|
||||
|
||||
@watch('open')
|
||||
handleOpenChange() {
|
||||
this.open ? this.show() : this.hide();
|
||||
async handleOpenChange() {
|
||||
if (!this.hasInitialized || this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateAccessibleTrigger();
|
||||
|
||||
if (this.open) {
|
||||
// Show
|
||||
this.slShow.emit();
|
||||
this.panel.addEventListener('sl-activate', this.handleMenuItemActivate);
|
||||
this.panel.addEventListener('sl-select', this.handlePanelSelect);
|
||||
document.addEventListener('keydown', this.handleDocumentKeyDown);
|
||||
document.addEventListener('mousedown', this.handleDocumentMouseDown);
|
||||
|
||||
await stopAnimations(this);
|
||||
this.panel.hidden = false;
|
||||
const { keyframes, options } = getAnimation(this, 'dropdown.show');
|
||||
await animateTo(this.panel, keyframes, options);
|
||||
|
||||
this.slAfterShow.emit();
|
||||
} else {
|
||||
// Hide
|
||||
this.slHide.emit();
|
||||
this.panel.removeEventListener('sl-activate', this.handleMenuItemActivate);
|
||||
this.panel.removeEventListener('sl-select', this.handlePanelSelect);
|
||||
document.addEventListener('keydown', this.handleDocumentKeyDown);
|
||||
document.removeEventListener('mousedown', this.handleDocumentMouseDown);
|
||||
|
||||
await stopAnimations(this);
|
||||
const { keyframes, options } = getAnimation(this, 'dropdown.hide');
|
||||
await animateTo(this.panel, keyframes, options);
|
||||
this.panel.hidden = true;
|
||||
|
||||
this.slAfterHide.emit();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators';
|
||||
import { event, EventEmitter } from '../../internal/decorators';
|
||||
import SlButton from '../button/button';
|
||||
import SlCheckbox from '../checkbox/checkbox';
|
||||
import SlColorPicker from '../color-picker/color-picker';
|
||||
import SlInput from '../input/input';
|
||||
import SlRadio from '../radio/radio';
|
||||
import SlRange from '../range/range';
|
||||
import SlSelect from '../select/select';
|
||||
import SlSwitch from '../switch/switch';
|
||||
import SlTextarea from '../textarea/textarea';
|
||||
import type SlButton from '../button/button';
|
||||
import type SlCheckbox from '../checkbox/checkbox';
|
||||
import type SlColorPicker from '../color-picker/color-picker';
|
||||
import type SlInput from '../input/input';
|
||||
import type SlRadio from '../radio/radio';
|
||||
import type SlRange from '../range/range';
|
||||
import type SlSelect from '../select/select';
|
||||
import type SlSwitch from '../switch/switch';
|
||||
import type SlTextarea from '../textarea/textarea';
|
||||
import styles from 'sass:./form.scss';
|
||||
|
||||
interface FormControl {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import SlIcon from '../icon/icon';
|
||||
import defaultLibrary from './library.default';
|
||||
import systemLibrary from './library.system';
|
||||
import type SlIcon from '../icon/icon';
|
||||
|
||||
export type IconLibraryResolver = (name: string) => string;
|
||||
export type IconLibraryMutator = (svg: SVGElement) => void;
|
||||
|
|
|
@ -2,7 +2,7 @@ import { LitElement, html, unsafeCSS } from 'lit';
|
|||
import { customElement, query } from 'lit/decorators';
|
||||
import { event, EventEmitter } from '../../internal/decorators';
|
||||
import { getTextContent } from '../../internal/slot';
|
||||
import SlMenuItem from '../menu-item/menu-item';
|
||||
import type SlMenuItem from '../menu-item/menu-item';
|
||||
import styles from 'sass:./menu.scss';
|
||||
|
||||
/**
|
||||
|
@ -49,7 +49,7 @@ export default class SlMenu extends LitElement {
|
|||
|
||||
syncItems() {
|
||||
this.items = [...this.defaultSlot.assignedElements({ flatten: true })].filter(
|
||||
(el: any) => el instanceof SlMenuItem && !el.disabled
|
||||
(el: any) => el.tagName.toLowerCase() === 'sl-menu-item' && !el.disabled
|
||||
) as [SlMenuItem];
|
||||
}
|
||||
|
||||
|
|
|
@ -6,10 +6,10 @@ import { event, EventEmitter, watch } from '../../internal/decorators';
|
|||
import { getLabelledBy, renderFormControl } from '../../internal/form-control';
|
||||
import { getTextContent } from '../../internal/slot';
|
||||
import { hasSlot } from '../../internal/slot';
|
||||
import SlDropdown from '../dropdown/dropdown';
|
||||
import SlIconButton from '../icon-button/icon-button';
|
||||
import SlMenu from '../menu/menu';
|
||||
import SlMenuItem from '../menu-item/menu-item';
|
||||
import type SlDropdown from '../dropdown/dropdown';
|
||||
import type SlIconButton from '../icon-button/icon-button';
|
||||
import type SlMenu from '../menu/menu';
|
||||
import type SlMenuItem from '../menu-item/menu-item';
|
||||
import styles from 'sass:./select.scss';
|
||||
|
||||
let id = 0;
|
||||
|
@ -254,12 +254,7 @@ export default class SlSelect extends LitElement {
|
|||
this.syncItemsFromValue();
|
||||
}
|
||||
|
||||
handleMenuShow(event: CustomEvent) {
|
||||
if (this.disabled) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
handleMenuShow() {
|
||||
this.resizeMenu();
|
||||
this.resizeObserver.observe(this);
|
||||
this.isOpen = true;
|
||||
|
@ -404,6 +399,7 @@ export default class SlSelect extends LitElement {
|
|||
.hoist=${this.hoist}
|
||||
.closeOnSelect=${!this.multiple}
|
||||
.containingElement=${this}
|
||||
?disabled=${this.disabled}
|
||||
class=${classMap({
|
||||
select: true,
|
||||
'select--open': this.isOpen,
|
||||
|
|
|
@ -5,8 +5,8 @@ import { event, EventEmitter, watch } from '../../internal/decorators';
|
|||
import { focusVisible } from '../../internal/focus-visible';
|
||||
import { getOffset } from '../../internal/offset';
|
||||
import { scrollIntoView } from '../../internal/scroll';
|
||||
import SlTab from '../tab/tab';
|
||||
import SlTabPanel from '../tab-panel/tab-panel';
|
||||
import type SlTab from '../tab/tab';
|
||||
import type SlTabPanel from '../tab-panel/tab-panel';
|
||||
import styles from 'sass:./tab-group.scss';
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { LitElement, html, unsafeCSS } from 'lit';
|
||||
import { customElement, property, query } from 'lit/decorators';
|
||||
import { classMap } from 'lit-html/directives/class-map';
|
||||
import { animateTo, parseDuration, stopAnimations } from '../../internal/animate';
|
||||
import { Instance as PopperInstance, createPopper } from '@popperjs/core/dist/esm';
|
||||
import { animateTo, parseDuration, stopAnimations } from '../../internal/animate';
|
||||
import { event, EventEmitter, watch } from '../../internal/decorators';
|
||||
import { waitForEvent } from '../../internal/event';
|
||||
import { setDefaultAnimation, getAnimation } from '../../utilities/animation-registry';
|
||||
import styles from 'sass:./tooltip.scss';
|
||||
|
||||
|
@ -137,76 +138,22 @@ export default class SlTooltip extends LitElement {
|
|||
|
||||
/** Shows the tooltip. */
|
||||
async show() {
|
||||
// Prevent subsequent calls to the method, whether manually or triggered by the `open` watcher
|
||||
if (!this.hasInitialized || this.open || this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slShow = this.slShow.emit();
|
||||
if (slShow.defaultPrevented) {
|
||||
this.open = false;
|
||||
if (this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.open = true;
|
||||
|
||||
await stopAnimations(this.tooltip);
|
||||
|
||||
if (this.popover) {
|
||||
this.popover.destroy();
|
||||
}
|
||||
|
||||
this.popover = createPopper(this.target, this.positioner, {
|
||||
placement: this.placement,
|
||||
strategy: 'absolute',
|
||||
modifiers: [
|
||||
{
|
||||
name: 'flip',
|
||||
options: {
|
||||
boundary: 'viewport'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [this.skidding, this.distance]
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
this.tooltip.hidden = false;
|
||||
const { keyframes, options } = getAnimation(this, 'tooltip.show');
|
||||
await animateTo(this.tooltip, keyframes, options);
|
||||
|
||||
this.slAfterShow.emit();
|
||||
return waitForEvent(this, 'sl-after-show');
|
||||
}
|
||||
|
||||
/** Shows the tooltip. */
|
||||
/** Hides the tooltip */
|
||||
async hide() {
|
||||
// Prevent subsequent calls to the method, whether manually or triggered by the `open` watcher
|
||||
if (!this.hasInitialized || !this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slHide = this.slHide.emit();
|
||||
if (slHide.defaultPrevented) {
|
||||
this.open = true;
|
||||
if (!this.open) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.open = false;
|
||||
|
||||
await stopAnimations(this.tooltip);
|
||||
const { keyframes, options } = getAnimation(this, 'tooltip.hide');
|
||||
await animateTo(this.tooltip, keyframes, options);
|
||||
this.tooltip.hidden = true;
|
||||
|
||||
if (this.popover) {
|
||||
this.popover.destroy();
|
||||
}
|
||||
|
||||
this.slAfterHide.emit();
|
||||
return waitForEvent(this, 'sl-after-hide');
|
||||
}
|
||||
|
||||
getTarget() {
|
||||
|
@ -265,8 +212,61 @@ export default class SlTooltip extends LitElement {
|
|||
}
|
||||
|
||||
@watch('open')
|
||||
handleOpenChange() {
|
||||
this.open ? this.show() : this.hide();
|
||||
async handleOpenChange() {
|
||||
// Prevent subsequent calls to the method, whether manually or triggered by the `open` watcher
|
||||
if (!this.hasInitialized || this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.open) {
|
||||
// Show
|
||||
this.slShow.emit();
|
||||
|
||||
await stopAnimations(this.tooltip);
|
||||
|
||||
if (this.popover) {
|
||||
this.popover.destroy();
|
||||
}
|
||||
|
||||
this.popover = createPopper(this.target, this.positioner, {
|
||||
placement: this.placement,
|
||||
strategy: 'absolute',
|
||||
modifiers: [
|
||||
{
|
||||
name: 'flip',
|
||||
options: {
|
||||
boundary: 'viewport'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [this.skidding, this.distance]
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
this.tooltip.hidden = false;
|
||||
const { keyframes, options } = getAnimation(this, 'tooltip.show');
|
||||
await animateTo(this.tooltip, keyframes, options);
|
||||
|
||||
this.slAfterShow.emit();
|
||||
} else {
|
||||
// Hide
|
||||
this.slHide.emit();
|
||||
|
||||
await stopAnimations(this.tooltip);
|
||||
const { keyframes, options } = getAnimation(this, 'tooltip.hide');
|
||||
await animateTo(this.tooltip, keyframes, options);
|
||||
this.tooltip.hidden = true;
|
||||
|
||||
if (this.popover) {
|
||||
this.popover.destroy();
|
||||
}
|
||||
|
||||
this.slAfterHide.emit();
|
||||
}
|
||||
}
|
||||
|
||||
@watch('placement')
|
||||
|
|
|
@ -49,7 +49,7 @@ export class EventEmitter<T> {
|
|||
Object.assign(
|
||||
{
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
cancelable: false,
|
||||
composed: true,
|
||||
detail: {}
|
||||
},
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
//
|
||||
// Waits for a specific event to be emitted from an element. Ignores events that bubble up from child elements.
|
||||
//
|
||||
export function waitForEvent(el: HTMLElement, eventName: string) {
|
||||
return new Promise<void>(resolve => {
|
||||
function done(event: Event) {
|
||||
if (event.target === el) {
|
||||
el.removeEventListener(eventName, done);
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
|
||||
el.addEventListener(eventName, done);
|
||||
});
|
||||
}
|
Ładowanie…
Reference in New Issue