fix show/hide logic

pull/463/head
Cory LaViska 2021-05-27 16:29:10 -04:00
rodzic 8d8b77ca07
commit 10f045fe6e
15 zmienionych plików z 385 dodań i 365 usunięć

Wyświetl plik

@ -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

Wyświetl plik

@ -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')

Wyświetl plik

@ -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

Wyświetl plik

@ -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() {

Wyświetl plik

@ -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() {

Wyświetl plik

@ -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() {

Wyświetl plik

@ -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() {

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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;

Wyświetl plik

@ -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];
}

Wyświetl plik

@ -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,

Wyświetl plik

@ -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';
/**

Wyświetl plik

@ -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')

Wyświetl plik

@ -49,7 +49,7 @@ export class EventEmitter<T> {
Object.assign(
{
bubbles: true,
cancelable: true,
cancelable: false,
composed: true,
detail: {}
},

Wyświetl plik

@ -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);
});
}