UNPKG

@shoelace-style/shoelace

Version:

A forward-thinking library of web components.

380 lines (376 loc) 11.4 kB
import { dropdown_styles_default } from "./chunk.I7IBIORS.js"; import { getTabbableBoundary } from "./chunk.LXDTFLWU.js"; import { SlPopup } from "./chunk.UDZLGDWZ.js"; import { getAnimation, setDefaultAnimation } from "./chunk.EJDHS3MU.js"; import { waitForEvent } from "./chunk.B4BZKR24.js"; import { animateTo, stopAnimations } from "./chunk.RV6ECJES.js"; import { LocalizeController } from "./chunk.NH3SRVOC.js"; import { o } from "./chunk.2URMUHDY.js"; import { e } from "./chunk.UZVKBFXH.js"; import { watch } from "./chunk.SJGTYGCD.js"; import { component_styles_default } from "./chunk.K23QWHWK.js"; import { ShoelaceElement, e as e2, n } from "./chunk.QAHCGRIS.js"; import { x } from "./chunk.CXZZ2LVK.js"; import { __decorateClass } from "./chunk.625AWUY7.js"; // src/components/dropdown/dropdown.component.ts var SlDropdown = class extends ShoelaceElement { constructor() { super(...arguments); this.localize = new LocalizeController(this); this.open = false; this.placement = "bottom-start"; this.disabled = false; this.stayOpenOnSelect = false; this.distance = 0; this.skidding = 0; this.hoist = false; this.sync = void 0; this.handleKeyDown = (event) => { if (this.open && event.key === "Escape") { event.stopPropagation(); this.hide(); this.focusOnTrigger(); } }; this.handleDocumentKeyDown = (event) => { var _a; if (event.key === "Escape" && this.open && !this.closeWatcher) { event.stopPropagation(); this.focusOnTrigger(); this.hide(); return; } if (event.key === "Tab") { if (this.open && ((_a = document.activeElement) == null ? void 0 : _a.tagName.toLowerCase()) === "sl-menu-item") { event.preventDefault(); this.hide(); this.focusOnTrigger(); return; } setTimeout(() => { var _a2, _b, _c; const activeElement = ((_a2 = this.containingElement) == null ? void 0 : _a2.getRootNode()) instanceof ShadowRoot ? (_c = (_b = document.activeElement) == null ? void 0 : _b.shadowRoot) == null ? void 0 : _c.activeElement : document.activeElement; if (!this.containingElement || (activeElement == null ? void 0 : activeElement.closest(this.containingElement.tagName.toLowerCase())) !== this.containingElement) { this.hide(); } }); } }; this.handleDocumentMouseDown = (event) => { const path = event.composedPath(); if (this.containingElement && !path.includes(this.containingElement)) { this.hide(); } }; this.handlePanelSelect = (event) => { const target = event.target; if (!this.stayOpenOnSelect && target.tagName.toLowerCase() === "sl-menu") { this.hide(); this.focusOnTrigger(); } }; } connectedCallback() { super.connectedCallback(); if (!this.containingElement) { this.containingElement = this; } } firstUpdated() { this.panel.hidden = !this.open; if (this.open) { this.addOpenListeners(); this.popup.active = true; } } disconnectedCallback() { super.disconnectedCallback(); this.removeOpenListeners(); this.hide(); } focusOnTrigger() { const trigger = this.trigger.assignedElements({ flatten: true })[0]; if (typeof (trigger == null ? void 0 : trigger.focus) === "function") { trigger.focus(); } } getMenu() { return this.panel.assignedElements({ flatten: true }).find((el) => el.tagName.toLowerCase() === "sl-menu"); } handleTriggerClick() { if (this.open) { this.hide(); } else { this.show(); this.focusOnTrigger(); } } async handleTriggerKeyDown(event) { if ([" ", "Enter"].includes(event.key)) { event.preventDefault(); this.handleTriggerClick(); return; } const menu = this.getMenu(); if (menu) { const menuItems = menu.getAllItems(); const firstMenuItem = menuItems[0]; const lastMenuItem = menuItems[menuItems.length - 1]; if (["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) { event.preventDefault(); if (!this.open) { this.show(); await this.updateComplete; } if (menuItems.length > 0) { this.updateComplete.then(() => { if (event.key === "ArrowDown" || event.key === "Home") { menu.setCurrentItem(firstMenuItem); firstMenuItem.focus(); } if (event.key === "ArrowUp" || event.key === "End") { menu.setCurrentItem(lastMenuItem); lastMenuItem.focus(); } }); } } } } handleTriggerKeyUp(event) { if (event.key === " ") { event.preventDefault(); } } 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() { const assignedElements = this.trigger.assignedElements({ flatten: true }); const accessibleTrigger = assignedElements.find((el) => getTabbableBoundary(el).start); let target; if (accessibleTrigger) { switch (accessibleTrigger.tagName.toLowerCase()) { case "sl-button": case "sl-icon-button": target = accessibleTrigger.button; break; default: target = accessibleTrigger; } target.setAttribute("aria-haspopup", "true"); target.setAttribute("aria-expanded", this.open ? "true" : "false"); } } /** Shows the dropdown panel. */ async show() { if (this.open) { return void 0; } this.open = true; return waitForEvent(this, "sl-after-show"); } /** Hides the dropdown panel */ async hide() { if (!this.open) { return void 0; } this.open = false; return waitForEvent(this, "sl-after-hide"); } /** * Instructs the dropdown menu to reposition. Useful when the position or size of the trigger changes when the menu * is activated. */ reposition() { this.popup.reposition(); } addOpenListeners() { var _a; this.panel.addEventListener("sl-select", this.handlePanelSelect); if ("CloseWatcher" in window) { (_a = this.closeWatcher) == null ? void 0 : _a.destroy(); this.closeWatcher = new CloseWatcher(); this.closeWatcher.onclose = () => { this.hide(); this.focusOnTrigger(); }; } else { this.panel.addEventListener("keydown", this.handleKeyDown); } document.addEventListener("keydown", this.handleDocumentKeyDown); document.addEventListener("mousedown", this.handleDocumentMouseDown); } removeOpenListeners() { var _a; if (this.panel) { this.panel.removeEventListener("sl-select", this.handlePanelSelect); this.panel.removeEventListener("keydown", this.handleKeyDown); } document.removeEventListener("keydown", this.handleDocumentKeyDown); document.removeEventListener("mousedown", this.handleDocumentMouseDown); (_a = this.closeWatcher) == null ? void 0 : _a.destroy(); } async handleOpenChange() { if (this.disabled) { this.open = false; return; } this.updateAccessibleTrigger(); if (this.open) { this.emit("sl-show"); this.addOpenListeners(); await stopAnimations(this); this.panel.hidden = false; this.popup.active = true; const { keyframes, options } = getAnimation(this, "dropdown.show", { dir: this.localize.dir() }); await animateTo(this.popup.popup, keyframes, options); this.emit("sl-after-show"); } else { this.emit("sl-hide"); this.removeOpenListeners(); await stopAnimations(this); const { keyframes, options } = getAnimation(this, "dropdown.hide", { dir: this.localize.dir() }); await animateTo(this.popup.popup, keyframes, options); this.panel.hidden = true; this.popup.active = false; this.emit("sl-after-hide"); } } render() { return x` <sl-popup part="base" exportparts="popup:base__popup" id="dropdown" placement=${this.placement} distance=${this.distance} skidding=${this.skidding} strategy=${this.hoist ? "fixed" : "absolute"} flip shift auto-size="vertical" auto-size-padding="10" sync=${o(this.sync ? this.sync : void 0)} class=${e({ dropdown: true, "dropdown--open": this.open })} > <slot name="trigger" slot="anchor" part="trigger" class="dropdown__trigger" @click=${this.handleTriggerClick} @keydown=${this.handleTriggerKeyDown} @keyup=${this.handleTriggerKeyUp} @slotchange=${this.handleTriggerSlotChange} ></slot> <div aria-hidden=${this.open ? "false" : "true"} aria-labelledby="dropdown"> <slot part="panel" class="dropdown__panel"></slot> </div> </sl-popup> `; } }; SlDropdown.styles = [component_styles_default, dropdown_styles_default]; SlDropdown.dependencies = { "sl-popup": SlPopup }; __decorateClass([ e2(".dropdown") ], SlDropdown.prototype, "popup", 2); __decorateClass([ e2(".dropdown__trigger") ], SlDropdown.prototype, "trigger", 2); __decorateClass([ e2(".dropdown__panel") ], SlDropdown.prototype, "panel", 2); __decorateClass([ n({ type: Boolean, reflect: true }) ], SlDropdown.prototype, "open", 2); __decorateClass([ n({ reflect: true }) ], SlDropdown.prototype, "placement", 2); __decorateClass([ n({ type: Boolean, reflect: true }) ], SlDropdown.prototype, "disabled", 2); __decorateClass([ n({ attribute: "stay-open-on-select", type: Boolean, reflect: true }) ], SlDropdown.prototype, "stayOpenOnSelect", 2); __decorateClass([ n({ attribute: false }) ], SlDropdown.prototype, "containingElement", 2); __decorateClass([ n({ type: Number }) ], SlDropdown.prototype, "distance", 2); __decorateClass([ n({ type: Number }) ], SlDropdown.prototype, "skidding", 2); __decorateClass([ n({ type: Boolean }) ], SlDropdown.prototype, "hoist", 2); __decorateClass([ n({ reflect: true }) ], SlDropdown.prototype, "sync", 2); __decorateClass([ watch("open", { waitUntilFirstUpdate: true }) ], SlDropdown.prototype, "handleOpenChange", 1); setDefaultAnimation("dropdown.show", { keyframes: [ { opacity: 0, scale: 0.9 }, { opacity: 1, scale: 1 } ], options: { duration: 100, easing: "ease" } }); setDefaultAnimation("dropdown.hide", { keyframes: [ { opacity: 1, scale: 1 }, { opacity: 0, scale: 0.9 } ], options: { duration: 100, easing: "ease" } }); export { SlDropdown };