2022-08-17 15:37:37 +00:00
|
|
|
import { html } from 'lit';
|
2021-05-27 21:00:43 +00:00
|
|
|
import { customElement, property, query } from 'lit/decorators.js';
|
2021-09-29 12:40:26 +00:00
|
|
|
import { classMap } from 'lit/directives/class-map.js';
|
2022-03-24 12:01:09 +00:00
|
|
|
import { animateTo, stopAnimations } from '../../internal/animate';
|
2022-09-16 20:21:40 +00:00
|
|
|
import { waitForEvent } from '../../internal/event';
|
2022-03-24 12:01:09 +00:00
|
|
|
import { scrollIntoView } from '../../internal/scroll';
|
2022-08-17 15:37:37 +00:00
|
|
|
import ShoelaceElement from '../../internal/shoelace-element';
|
2022-03-24 12:01:09 +00:00
|
|
|
import { getTabbableBoundary } from '../../internal/tabbable';
|
|
|
|
import { watch } from '../../internal/watch';
|
|
|
|
import { getAnimation, setDefaultAnimation } from '../../utilities/animation-registry';
|
2022-06-09 22:14:38 +00:00
|
|
|
import { LocalizeController } from '../../utilities/localize';
|
2022-08-09 19:28:01 +00:00
|
|
|
import '../popup/popup';
|
2021-07-10 00:45:44 +00:00
|
|
|
import styles from './dropdown.styles';
|
2022-08-08 14:15:41 +00:00
|
|
|
import type SlButton from '../button/button';
|
|
|
|
import type SlIconButton from '../icon-button/icon-button';
|
|
|
|
import type SlMenuItem from '../menu-item/menu-item';
|
|
|
|
import type SlMenu from '../menu/menu';
|
2022-08-09 19:28:01 +00:00
|
|
|
import type SlPopup from '../popup/popup';
|
2022-07-19 12:27:39 +00:00
|
|
|
import type { CSSResultGroup } from 'lit';
|
2020-07-15 21:30:37 +00:00
|
|
|
|
|
|
|
/**
|
2022-10-21 13:56:35 +00:00
|
|
|
* @summary Dropdowns expose additional content that "drops down" in a panel.
|
|
|
|
*
|
2020-07-17 10:09:10 +00:00
|
|
|
* @since 2.0
|
2020-07-15 21:30:37 +00:00
|
|
|
* @status stable
|
|
|
|
*
|
2022-08-09 19:28:01 +00:00
|
|
|
* @dependency sl-popup
|
|
|
|
*
|
2022-12-06 16:18:14 +00:00
|
|
|
* @slot - The dropdown's main content.
|
2021-06-25 20:25:46 +00:00
|
|
|
* @slot trigger - The dropdown's trigger, usually a `<sl-button>` element.
|
2020-07-15 21:30:37 +00:00
|
|
|
*
|
2021-06-25 20:25:46 +00:00
|
|
|
* @event sl-show - Emitted when the dropdown opens.
|
|
|
|
* @event sl-after-show - Emitted after the dropdown opens and all animations are complete.
|
|
|
|
* @event sl-hide - Emitted when the dropdown closes.
|
2021-07-01 21:23:16 +00:00
|
|
|
* @event sl-after-hide - Emitted after the dropdown closes and all animations are complete.
|
2021-05-26 12:42:33 +00:00
|
|
|
*
|
2022-12-06 16:18:14 +00:00
|
|
|
* @csspart base - The component's base wrapper.
|
2021-06-25 20:25:46 +00:00
|
|
|
* @csspart trigger - The container that wraps the trigger.
|
|
|
|
* @csspart panel - The panel that gets shown when the dropdown is open.
|
2021-06-24 22:24:54 +00:00
|
|
|
*
|
2021-06-25 20:25:46 +00:00
|
|
|
* @animation dropdown.show - The animation to use when showing the dropdown.
|
|
|
|
* @animation dropdown.hide - The animation to use when hiding the dropdown.
|
2020-07-15 21:30:37 +00:00
|
|
|
*/
|
2021-03-18 13:04:23 +00:00
|
|
|
@customElement('sl-dropdown')
|
2022-08-17 15:37:37 +00:00
|
|
|
export default class SlDropdown extends ShoelaceElement {
|
2022-07-19 12:27:39 +00:00
|
|
|
static styles: CSSResultGroup = styles;
|
2021-03-06 17:01:39 +00:00
|
|
|
|
2022-08-09 19:28:01 +00:00
|
|
|
@query('.dropdown') popup: SlPopup;
|
2022-12-02 22:03:59 +00:00
|
|
|
@query('.dropdown__trigger') trigger: HTMLSlotElement;
|
|
|
|
@query('.dropdown__panel') panel: HTMLSlotElement;
|
2021-02-26 14:09:13 +00:00
|
|
|
|
2022-06-09 22:14:38 +00:00
|
|
|
private readonly localize = new LocalizeController(this);
|
2020-07-15 21:30:37 +00:00
|
|
|
|
2022-12-06 16:18:14 +00:00
|
|
|
/**
|
|
|
|
* Indicates whether or not the dropdown is open. You can toggle this attribute to show and hide the dropdown, or you
|
|
|
|
* can use the `show()` and `hide()` methods and this attribute will reflect the dropdown's open state.
|
|
|
|
*/
|
2021-07-01 00:04:46 +00:00
|
|
|
@property({ type: Boolean, reflect: true }) open = false;
|
2020-07-15 21:30:37 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The preferred placement of the dropdown panel. Note that the actual placement may vary as needed to keep the panel
|
|
|
|
* inside of the viewport.
|
|
|
|
*/
|
2022-03-03 20:48:20 +00:00
|
|
|
@property({ reflect: true }) placement:
|
2020-07-15 21:30:37 +00:00
|
|
|
| 'top'
|
|
|
|
| 'top-start'
|
|
|
|
| 'top-end'
|
|
|
|
| 'bottom'
|
|
|
|
| 'bottom-start'
|
|
|
|
| 'bottom-end'
|
|
|
|
| 'right'
|
|
|
|
| 'right-start'
|
|
|
|
| 'right-end'
|
|
|
|
| 'left'
|
|
|
|
| 'left-start'
|
|
|
|
| 'left-end' = 'bottom-start';
|
|
|
|
|
2021-05-27 20:29:10 +00:00
|
|
|
/** Disables the dropdown so the panel will not open. */
|
2022-05-02 20:33:30 +00:00
|
|
|
@property({ type: Boolean, reflect: true }) disabled = false;
|
2021-05-27 20:29:10 +00:00
|
|
|
|
2021-07-01 21:23:16 +00:00
|
|
|
/**
|
|
|
|
* By default, the dropdown is closed when an item is selected. This attribute will keep it open instead. Useful for
|
2022-12-06 16:18:14 +00:00
|
|
|
* dropdowns that allow for multiple interactions.
|
2021-07-01 21:23:16 +00:00
|
|
|
*/
|
|
|
|
@property({ attribute: 'stay-open-on-select', type: Boolean, reflect: true }) stayOpenOnSelect = false;
|
2020-07-15 21:30:37 +00:00
|
|
|
|
2022-12-06 16:18:14 +00:00
|
|
|
/**
|
|
|
|
* The dropdown will close when the user interacts outside of this element (e.g. clicking). Useful for composing other
|
|
|
|
* components that use a dropdown internally.
|
|
|
|
*/
|
2022-01-16 05:47:14 +00:00
|
|
|
@property({ attribute: false }) containingElement?: HTMLElement;
|
2020-07-15 21:30:37 +00:00
|
|
|
|
|
|
|
/** The distance in pixels from which to offset the panel away from its trigger. */
|
2021-09-24 12:31:54 +00:00
|
|
|
@property({ type: Number }) distance = 0;
|
2020-07-15 21:30:37 +00:00
|
|
|
|
|
|
|
/** The distance in pixels from which to offset the panel along its trigger. */
|
2021-07-01 00:04:46 +00:00
|
|
|
@property({ type: Number }) skidding = 0;
|
2020-07-15 21:30:37 +00:00
|
|
|
|
2020-08-25 21:07:28 +00:00
|
|
|
/**
|
|
|
|
* Enable this option to prevent the panel from being clipped when the component is placed inside a container with
|
2022-12-06 16:18:14 +00:00
|
|
|
* `overflow: auto|scroll`. Hoisting uses a fixed positioning strategy that works in many, but not all, scenarios.
|
2020-08-25 21:07:28 +00:00
|
|
|
*/
|
2021-07-01 00:04:46 +00:00
|
|
|
@property({ type: Boolean }) hoist = false;
|
2021-03-06 17:01:39 +00:00
|
|
|
|
|
|
|
connectedCallback() {
|
|
|
|
super.connectedCallback();
|
2023-01-03 15:19:25 +00:00
|
|
|
this.handleMenuItemActivate = this.handleMenuItemActivate.bind(this);
|
|
|
|
this.handlePanelSelect = this.handlePanelSelect.bind(this);
|
|
|
|
this.handleKeyDown = this.handleKeyDown.bind(this);
|
|
|
|
this.handleDocumentKeyDown = this.handleDocumentKeyDown.bind(this);
|
|
|
|
this.handleDocumentMouseDown = this.handleDocumentMouseDown.bind(this);
|
2021-02-26 14:09:13 +00:00
|
|
|
|
2022-01-19 14:37:07 +00:00
|
|
|
if (!this.containingElement) {
|
2021-02-26 14:09:13 +00:00
|
|
|
this.containingElement = this;
|
|
|
|
}
|
2021-05-26 11:51:57 +00:00
|
|
|
}
|
2020-07-15 21:30:37 +00:00
|
|
|
|
2022-08-09 20:01:13 +00:00
|
|
|
firstUpdated() {
|
2021-05-26 12:42:33 +00:00
|
|
|
this.panel.hidden = !this.open;
|
2022-03-03 20:48:20 +00:00
|
|
|
|
|
|
|
// If the dropdown is visible on init, update its position
|
|
|
|
if (this.open) {
|
2022-08-09 20:01:13 +00:00
|
|
|
this.addOpenListeners();
|
|
|
|
this.popup.active = true;
|
2022-03-03 20:48:20 +00:00
|
|
|
}
|
2020-07-15 21:30:37 +00:00
|
|
|
}
|
|
|
|
|
2021-03-06 17:01:39 +00:00
|
|
|
disconnectedCallback() {
|
|
|
|
super.disconnectedCallback();
|
2022-04-08 12:52:20 +00:00
|
|
|
this.removeOpenListeners();
|
2022-01-17 04:44:10 +00:00
|
|
|
this.hide();
|
2020-07-15 21:30:37 +00:00
|
|
|
}
|
|
|
|
|
2020-07-24 13:28:36 +00:00
|
|
|
focusOnTrigger() {
|
2022-12-02 22:03:59 +00:00
|
|
|
const trigger = this.trigger.assignedElements({ flatten: true })[0] as HTMLElement | undefined;
|
2022-01-16 05:47:14 +00:00
|
|
|
if (typeof trigger?.focus === 'function') {
|
2021-03-29 11:16:40 +00:00
|
|
|
trigger.focus();
|
2020-07-24 13:28:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-15 21:30:37 +00:00
|
|
|
getMenu() {
|
2022-12-02 22:03:59 +00:00
|
|
|
return this.panel.assignedElements({ flatten: true }).find(el => el.tagName.toLowerCase() === 'sl-menu') as
|
2022-01-16 05:47:14 +00:00
|
|
|
| SlMenu
|
|
|
|
| undefined;
|
2020-07-15 21:30:37 +00:00
|
|
|
}
|
|
|
|
|
2023-01-03 15:19:25 +00:00
|
|
|
handleKeyDown(event: KeyboardEvent) {
|
2022-11-09 14:50:58 +00:00
|
|
|
// Close when escape is pressed inside an open dropdown. We need to listen on the panel itself and stop propagation
|
|
|
|
// in case any ancestors are also listening for this key.
|
|
|
|
if (this.open && event.key === 'Escape') {
|
|
|
|
event.stopPropagation();
|
2022-01-17 04:44:10 +00:00
|
|
|
this.hide();
|
2020-07-24 13:28:36 +00:00
|
|
|
this.focusOnTrigger();
|
2020-07-15 21:30:37 +00:00
|
|
|
}
|
2023-01-03 15:19:25 +00:00
|
|
|
}
|
2020-07-15 21:30:37 +00:00
|
|
|
|
2023-01-03 15:19:25 +00:00
|
|
|
handleDocumentKeyDown(event: KeyboardEvent) {
|
2020-09-03 21:44:52 +00:00
|
|
|
// Handle tabbing
|
2020-07-15 21:30:37 +00:00
|
|
|
if (event.key === 'Tab') {
|
2020-10-22 17:42:52 +00:00
|
|
|
// Tabbing within an open menu should close the dropdown and refocus the trigger
|
|
|
|
if (this.open && document.activeElement?.tagName.toLowerCase() === 'sl-menu-item') {
|
|
|
|
event.preventDefault();
|
2022-01-17 04:44:10 +00:00
|
|
|
this.hide();
|
2020-10-22 17:42:52 +00:00
|
|
|
this.focusOnTrigger();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Tabbing outside of the containing element closes the panel
|
|
|
|
//
|
|
|
|
// If the dropdown is used within a shadow DOM, we need to obtain the activeElement within that shadowRoot,
|
|
|
|
// otherwise `document.activeElement` will only return the name of the parent shadow DOM element.
|
2020-07-15 21:30:37 +00:00
|
|
|
setTimeout(() => {
|
2020-09-28 12:16:56 +00:00
|
|
|
const activeElement =
|
2022-01-16 05:47:14 +00:00
|
|
|
this.containingElement?.getRootNode() instanceof ShadowRoot
|
2021-02-26 14:09:13 +00:00
|
|
|
? document.activeElement?.shadowRoot?.activeElement
|
2020-09-28 12:16:56 +00:00
|
|
|
: document.activeElement;
|
|
|
|
|
2022-01-16 05:47:14 +00:00
|
|
|
if (
|
2022-01-19 14:37:07 +00:00
|
|
|
!this.containingElement ||
|
2022-01-16 05:47:14 +00:00
|
|
|
activeElement?.closest(this.containingElement.tagName.toLowerCase()) !== this.containingElement
|
|
|
|
) {
|
2022-01-17 04:44:10 +00:00
|
|
|
this.hide();
|
2020-07-15 21:30:37 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2023-01-03 15:19:25 +00:00
|
|
|
}
|
2020-07-15 21:30:37 +00:00
|
|
|
|
2023-01-03 15:19:25 +00:00
|
|
|
handleDocumentMouseDown(event: MouseEvent) {
|
2020-09-02 20:59:20 +00:00
|
|
|
// Close when clicking outside of the containing element
|
2022-01-16 05:47:14 +00:00
|
|
|
const path = event.composedPath();
|
2022-01-19 14:37:07 +00:00
|
|
|
if (this.containingElement && !path.includes(this.containingElement)) {
|
2022-01-17 04:44:10 +00:00
|
|
|
this.hide();
|
2020-07-15 21:30:37 +00:00
|
|
|
}
|
2023-01-03 15:19:25 +00:00
|
|
|
}
|
2020-07-15 21:30:37 +00:00
|
|
|
|
2023-01-03 15:19:25 +00:00
|
|
|
handleMenuItemActivate(event: CustomEvent) {
|
2021-02-26 14:09:13 +00:00
|
|
|
const item = event.target as SlMenuItem;
|
2020-08-08 19:45:32 +00:00
|
|
|
scrollIntoView(item, this.panel);
|
2023-01-03 15:19:25 +00:00
|
|
|
}
|
2020-08-08 19:45:32 +00:00
|
|
|
|
2023-01-03 15:19:25 +00:00
|
|
|
handlePanelSelect(event: CustomEvent) {
|
2020-07-15 21:30:37 +00:00
|
|
|
const target = event.target as HTMLElement;
|
|
|
|
|
|
|
|
// Hide the dropdown when a menu item is selected
|
2021-07-01 21:23:16 +00:00
|
|
|
if (!this.stayOpenOnSelect && target.tagName.toLowerCase() === 'sl-menu') {
|
2022-01-17 04:44:10 +00:00
|
|
|
this.hide();
|
2020-07-24 13:28:36 +00:00
|
|
|
this.focusOnTrigger();
|
2020-07-15 21:30:37 +00:00
|
|
|
}
|
2023-01-03 15:19:25 +00:00
|
|
|
}
|
2020-07-15 21:30:37 +00:00
|
|
|
|
2020-09-03 21:44:52 +00:00
|
|
|
handleTriggerClick() {
|
2022-01-16 05:47:14 +00:00
|
|
|
if (this.open) {
|
2022-01-17 04:44:10 +00:00
|
|
|
this.hide();
|
2022-01-16 05:47:14 +00:00
|
|
|
} else {
|
2022-01-17 04:44:10 +00:00
|
|
|
this.show();
|
2022-01-16 05:47:14 +00:00
|
|
|
}
|
2020-09-03 21:44:52 +00:00
|
|
|
}
|
|
|
|
|
2020-07-15 21:30:37 +00:00
|
|
|
handleTriggerKeyDown(event: KeyboardEvent) {
|
2020-09-03 21:44:52 +00:00
|
|
|
// Close when escape or tab is pressed
|
2022-11-23 15:38:04 +00:00
|
|
|
if (event.key === 'Escape' && this.open) {
|
|
|
|
event.stopPropagation();
|
2020-09-03 21:44:52 +00:00
|
|
|
this.focusOnTrigger();
|
2022-01-17 04:44:10 +00:00
|
|
|
this.hide();
|
2020-09-03 21:44:52 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// When spacebar/enter is pressed, show the panel but don't focus on the menu. This let's the user press the same
|
|
|
|
// key again to hide the menu in case they don't want to make a selection.
|
|
|
|
if ([' ', 'Enter'].includes(event.key)) {
|
2020-07-15 21:30:37 +00:00
|
|
|
event.preventDefault();
|
2022-01-16 05:47:14 +00:00
|
|
|
this.handleTriggerClick();
|
2020-09-03 21:44:52 +00:00
|
|
|
return;
|
2020-07-15 21:30:37 +00:00
|
|
|
}
|
2020-07-24 14:41:52 +00:00
|
|
|
|
2022-04-05 19:08:27 +00:00
|
|
|
const menu = this.getMenu();
|
2020-09-03 21:44:52 +00:00
|
|
|
|
2022-04-05 19:08:27 +00:00
|
|
|
if (menu) {
|
|
|
|
const menuItems = menu.defaultSlot.assignedElements({ flatten: true }) as SlMenuItem[];
|
|
|
|
const firstMenuItem = menuItems[0];
|
|
|
|
const lastMenuItem = menuItems[menuItems.length - 1];
|
|
|
|
|
|
|
|
// When up/down is pressed, we make the assumption that the user is familiar with the menu and plans to make a
|
|
|
|
// selection. Rather than toggle the panel, we focus on the menu (if one exists) and activate the first item for
|
|
|
|
// faster navigation.
|
|
|
|
if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) {
|
|
|
|
event.preventDefault();
|
2020-09-03 21:44:52 +00:00
|
|
|
|
2022-04-05 19:08:27 +00:00
|
|
|
// Show the menu if it's not already open
|
|
|
|
if (!this.open) {
|
|
|
|
this.show();
|
2022-02-12 18:37:55 +00:00
|
|
|
}
|
2020-10-22 17:42:52 +00:00
|
|
|
|
2022-04-05 19:08:27 +00:00
|
|
|
if (menuItems.length > 0) {
|
|
|
|
// Focus on the first/last menu item after showing
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
if (event.key === 'ArrowDown' || event.key === 'Home') {
|
|
|
|
menu.setCurrentItem(firstMenuItem);
|
|
|
|
firstMenuItem.focus();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (event.key === 'ArrowUp' || event.key === 'End') {
|
|
|
|
menu.setCurrentItem(lastMenuItem);
|
|
|
|
lastMenuItem.focus();
|
|
|
|
}
|
|
|
|
});
|
2022-02-12 18:37:55 +00:00
|
|
|
}
|
2022-04-05 19:08:27 +00:00
|
|
|
}
|
2020-07-24 14:41:52 +00:00
|
|
|
}
|
2020-07-15 21:30:37 +00:00
|
|
|
}
|
|
|
|
|
2020-09-11 14:55:24 +00:00
|
|
|
handleTriggerKeyUp(event: KeyboardEvent) {
|
|
|
|
// Prevent space from triggering a click event in Firefox
|
|
|
|
if (event.key === ' ') {
|
|
|
|
event.preventDefault();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-16 21:04:35 +00:00
|
|
|
handleTriggerSlotChange() {
|
|
|
|
this.updateAccessibleTrigger();
|
|
|
|
}
|
|
|
|
|
|
|
|
//
|
|
|
|
// Slotted triggers can be arbitrary content, but we need to link them to the dropdown panel with `aria-haspopup` and
|
|
|
|
// `aria-expanded`. These must be applied to the "accessible trigger" (the tabbable portion of the trigger element
|
|
|
|
// that gets slotted in) so screen readers will understand them. The accessible trigger could be the slotted element,
|
|
|
|
// a child of the slotted element, or an element in the slotted element's shadow root.
|
|
|
|
//
|
|
|
|
// For example, the accessible trigger of an <sl-button> is a <button> located inside its shadow root.
|
|
|
|
//
|
|
|
|
// To determine this, we assume the first tabbable element in the trigger slot is the "accessible trigger."
|
|
|
|
//
|
|
|
|
updateAccessibleTrigger() {
|
2022-12-02 22:03:59 +00:00
|
|
|
const assignedElements = this.trigger.assignedElements({ flatten: true }) as HTMLElement[];
|
2022-01-16 05:47:14 +00:00
|
|
|
const accessibleTrigger = assignedElements.find(el => getTabbableBoundary(el).start);
|
2022-03-16 21:40:31 +00:00
|
|
|
let target: HTMLElement;
|
2022-01-16 05:47:14 +00:00
|
|
|
|
2022-01-19 14:37:07 +00:00
|
|
|
if (accessibleTrigger) {
|
2022-03-16 21:40:31 +00:00
|
|
|
switch (accessibleTrigger.tagName.toLowerCase()) {
|
|
|
|
// Shoelace buttons have to update the internal button so it's announced correctly by screen readers
|
|
|
|
case 'sl-button':
|
|
|
|
case 'sl-icon-button':
|
|
|
|
target = (accessibleTrigger as SlButton | SlIconButton).button;
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
target = accessibleTrigger;
|
|
|
|
}
|
|
|
|
|
|
|
|
target.setAttribute('aria-haspopup', 'true');
|
|
|
|
target.setAttribute('aria-expanded', this.open ? 'true' : 'false');
|
2020-10-16 21:04:35 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-27 20:29:10 +00:00
|
|
|
/** Shows the dropdown panel. */
|
2021-05-26 12:42:33 +00:00
|
|
|
async show() {
|
2021-05-27 20:29:10 +00:00
|
|
|
if (this.open) {
|
2022-01-16 05:47:14 +00:00
|
|
|
return undefined;
|
2021-02-26 14:09:13 +00:00
|
|
|
}
|
|
|
|
|
2021-05-26 12:42:33 +00:00
|
|
|
this.open = true;
|
2021-05-27 20:29:10 +00:00
|
|
|
return waitForEvent(this, 'sl-after-show');
|
2021-02-26 14:09:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/** Hides the dropdown panel */
|
2021-05-26 12:42:33 +00:00
|
|
|
async hide() {
|
2021-05-27 20:29:10 +00:00
|
|
|
if (!this.open) {
|
2022-01-16 05:47:14 +00:00
|
|
|
return undefined;
|
2021-02-26 14:09:13 +00:00
|
|
|
}
|
|
|
|
|
2021-05-26 12:42:33 +00:00
|
|
|
this.open = false;
|
2021-05-27 20:29:10 +00:00
|
|
|
return waitForEvent(this, 'sl-after-hide');
|
2021-02-26 14:09:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Instructs the dropdown menu to reposition. Useful when the position or size of the trigger changes when the menu
|
|
|
|
* is activated.
|
|
|
|
*/
|
|
|
|
reposition() {
|
2022-08-09 19:28:01 +00:00
|
|
|
this.popup.reposition();
|
2021-02-26 14:09:13 +00:00
|
|
|
}
|
|
|
|
|
2022-04-08 12:52:20 +00:00
|
|
|
addOpenListeners() {
|
|
|
|
this.panel.addEventListener('sl-activate', this.handleMenuItemActivate);
|
|
|
|
this.panel.addEventListener('sl-select', this.handlePanelSelect);
|
2022-11-09 14:50:58 +00:00
|
|
|
this.panel.addEventListener('keydown', this.handleKeyDown);
|
2022-04-08 12:52:20 +00:00
|
|
|
document.addEventListener('keydown', this.handleDocumentKeyDown);
|
|
|
|
document.addEventListener('mousedown', this.handleDocumentMouseDown);
|
|
|
|
}
|
|
|
|
|
|
|
|
removeOpenListeners() {
|
2022-10-27 13:28:35 +00:00
|
|
|
if (this.panel) {
|
|
|
|
this.panel.removeEventListener('sl-activate', this.handleMenuItemActivate);
|
|
|
|
this.panel.removeEventListener('sl-select', this.handlePanelSelect);
|
2022-11-09 14:50:58 +00:00
|
|
|
this.panel.removeEventListener('keydown', this.handleKeyDown);
|
2022-10-27 13:28:35 +00:00
|
|
|
}
|
2022-04-08 12:52:20 +00:00
|
|
|
document.removeEventListener('keydown', this.handleDocumentKeyDown);
|
|
|
|
document.removeEventListener('mousedown', this.handleDocumentMouseDown);
|
|
|
|
}
|
|
|
|
|
2021-06-15 13:26:35 +00:00
|
|
|
@watch('open', { waitUntilFirstUpdate: true })
|
2021-05-27 20:29:10 +00:00
|
|
|
async handleOpenChange() {
|
2021-06-15 13:26:35 +00:00
|
|
|
if (this.disabled) {
|
2022-02-19 16:56:16 +00:00
|
|
|
this.open = false;
|
2021-05-27 20:29:10 +00:00
|
|
|
return;
|
|
|
|
}
|
2021-02-26 14:09:13 +00:00
|
|
|
|
2021-03-06 20:09:12 +00:00
|
|
|
this.updateAccessibleTrigger();
|
2021-05-27 20:29:10 +00:00
|
|
|
|
|
|
|
if (this.open) {
|
|
|
|
// Show
|
2022-09-16 20:21:40 +00:00
|
|
|
this.emit('sl-show');
|
2022-04-08 12:52:20 +00:00
|
|
|
this.addOpenListeners();
|
2021-05-27 20:29:10 +00:00
|
|
|
|
|
|
|
await stopAnimations(this);
|
|
|
|
this.panel.hidden = false;
|
2022-08-09 19:28:01 +00:00
|
|
|
this.popup.active = true;
|
2022-06-09 22:14:38 +00:00
|
|
|
const { keyframes, options } = getAnimation(this, 'dropdown.show', { dir: this.localize.dir() });
|
2022-08-09 19:28:01 +00:00
|
|
|
await animateTo(this.popup.popup, keyframes, options);
|
2021-05-27 20:29:10 +00:00
|
|
|
|
2022-09-16 20:21:40 +00:00
|
|
|
this.emit('sl-after-show');
|
2021-05-27 20:29:10 +00:00
|
|
|
} else {
|
|
|
|
// Hide
|
2022-09-16 20:21:40 +00:00
|
|
|
this.emit('sl-hide');
|
2022-04-08 12:52:20 +00:00
|
|
|
this.removeOpenListeners();
|
2021-05-27 20:29:10 +00:00
|
|
|
|
|
|
|
await stopAnimations(this);
|
2022-06-09 22:14:38 +00:00
|
|
|
const { keyframes, options } = getAnimation(this, 'dropdown.hide', { dir: this.localize.dir() });
|
2022-08-09 19:28:01 +00:00
|
|
|
await animateTo(this.popup.popup, keyframes, options);
|
2021-05-27 20:29:10 +00:00
|
|
|
this.panel.hidden = true;
|
2022-08-09 19:28:01 +00:00
|
|
|
this.popup.active = false;
|
2021-05-27 20:29:10 +00:00
|
|
|
|
2022-09-16 20:21:40 +00:00
|
|
|
this.emit('sl-after-hide');
|
2021-05-27 20:29:10 +00:00
|
|
|
}
|
2021-02-26 14:09:13 +00:00
|
|
|
}
|
|
|
|
|
2020-07-15 21:30:37 +00:00
|
|
|
render() {
|
2021-02-26 14:09:13 +00:00
|
|
|
return html`
|
2022-08-09 19:28:01 +00:00
|
|
|
<sl-popup
|
2020-07-15 21:30:37 +00:00
|
|
|
part="base"
|
2021-12-30 17:14:39 +00:00
|
|
|
id="dropdown"
|
2022-08-09 19:28:01 +00:00
|
|
|
placement=${this.placement}
|
|
|
|
distance=${this.distance}
|
|
|
|
skidding=${this.skidding}
|
|
|
|
strategy=${this.hoist ? 'fixed' : 'absolute'}
|
|
|
|
flip
|
|
|
|
shift
|
2022-08-19 18:21:30 +00:00
|
|
|
auto-size="vertical"
|
2022-08-10 15:01:13 +00:00
|
|
|
auto-size-padding="10"
|
2021-02-26 14:09:13 +00:00
|
|
|
class=${classMap({
|
2020-07-15 21:30:37 +00:00
|
|
|
dropdown: true,
|
|
|
|
'dropdown--open': this.open
|
2021-02-26 14:09:13 +00:00
|
|
|
})}
|
2020-07-15 21:30:37 +00:00
|
|
|
>
|
2022-12-02 22:03:59 +00:00
|
|
|
<slot
|
|
|
|
name="trigger"
|
2022-08-09 19:28:01 +00:00
|
|
|
slot="anchor"
|
2020-07-15 21:30:37 +00:00
|
|
|
part="trigger"
|
|
|
|
class="dropdown__trigger"
|
2021-03-06 17:01:39 +00:00
|
|
|
@click=${this.handleTriggerClick}
|
|
|
|
@keydown=${this.handleTriggerKeyDown}
|
|
|
|
@keyup=${this.handleTriggerKeyUp}
|
2022-12-02 22:03:59 +00:00
|
|
|
@slotchange=${this.handleTriggerSlotChange}
|
|
|
|
></slot>
|
2020-07-15 21:30:37 +00:00
|
|
|
|
2022-12-02 22:03:59 +00:00
|
|
|
<slot
|
2022-08-09 19:28:01 +00:00
|
|
|
part="panel"
|
|
|
|
class="dropdown__panel"
|
|
|
|
aria-hidden=${this.open ? 'false' : 'true'}
|
|
|
|
aria-labelledby="dropdown"
|
2022-12-02 22:03:59 +00:00
|
|
|
></slot>
|
2022-08-09 19:28:01 +00:00
|
|
|
</sl-popup>
|
2021-02-26 14:09:13 +00:00
|
|
|
`;
|
2020-07-15 21:30:37 +00:00
|
|
|
}
|
|
|
|
}
|
2021-03-12 14:07:38 +00:00
|
|
|
|
2021-05-26 12:42:33 +00:00
|
|
|
setDefaultAnimation('dropdown.show', {
|
|
|
|
keyframes: [
|
2022-12-01 20:38:59 +00:00
|
|
|
{ opacity: 0, scale: 0.9 },
|
|
|
|
{ opacity: 1, scale: 1 }
|
2021-05-26 12:42:33 +00:00
|
|
|
],
|
2021-11-12 14:43:02 +00:00
|
|
|
options: { duration: 100, easing: 'ease' }
|
2021-05-26 12:42:33 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
setDefaultAnimation('dropdown.hide', {
|
|
|
|
keyframes: [
|
2022-12-01 20:38:59 +00:00
|
|
|
{ opacity: 1, scale: 1 },
|
|
|
|
{ opacity: 0, scale: 0.9 }
|
2021-05-26 12:42:33 +00:00
|
|
|
],
|
2021-11-12 14:43:02 +00:00
|
|
|
options: { duration: 100, easing: 'ease' }
|
2021-05-26 12:42:33 +00:00
|
|
|
});
|
|
|
|
|
2021-03-12 14:09:08 +00:00
|
|
|
declare global {
|
|
|
|
interface HTMLElementTagNameMap {
|
|
|
|
'sl-dropdown': SlDropdown;
|
|
|
|
}
|
|
|
|
}
|