UNPKG

@esri/calcite-components

Version:

Web Components for Esri's Calcite Design System.

382 lines (381 loc) • 16.3 kB
/* COPYRIGHT Esri - https://js.arcgis.com/5.0/LICENSE.txt */ import { c as customElement } from "../../chunks/runtime.js"; import { ref } from "lit/directives/ref.js"; import { css, nothing, html } from "lit"; import { LitElement, createEvent, safeClassMap } from "@arcgis/lumina"; import { queryAssignedElements } from "lit/decorators.js"; import { f as focusElementInGroup, j as focusElement, v as nextFrame } from "../../chunks/dom.js"; import { d as defaultMenuPlacement, r as reposition, c as connectFloatingUI, a as disconnectFloatingUI, f as filterValidFlipPlacements, h as hideFloatingUI, F as FloatingCSS } from "../../chunks/floating-ui.js"; import { g as guid } from "../../chunks/guid.js"; import { i as isActivationKey } from "../../chunks/key.js"; import { c as createObserver, u as updateRefObserver } from "../../chunks/observers.js"; import { t as toggleOpenClose } from "../../chunks/openCloseComponent.js"; import { g as getDimensionClass } from "../../chunks/dynamicClasses.js"; import { u as useSetFocus } from "../../chunks/useSetFocus.js"; import { u as useInteractive } from "../../chunks/useInteractive.js"; import { u as useTopLayer } from "../../chunks/useTopLayer.js"; const SLOTS = { trigger: "trigger" }; const CSS = { content: "content", wrapper: "wrapper", triggerContainer: "trigger-container" }; const idPrefix = "calcite-dropdown"; const IDS = { menuButton: (id) => `${idPrefix}-${id}-menubutton`, menu: (id) => `${idPrefix}-${id}-menu` }; const styles = css`:host([disabled]){cursor:default;-webkit-user-select:none;user-select:none;opacity:var(--calcite-opacity-disabled)}:host([disabled]) *,:host([disabled]) ::slotted(*){pointer-events:none}:host{display:inline-block}.wrapper{inline-size:max-content;display:none;max-inline-size:100vw;max-block-size:100vh;inset-block-start:0;left:0;z-index:var(--calcite-floating-ui-z-index)}@starting-style{.wrapper{opacity:0;inset-block-start:0;left:0}}:host([top-layer-disabled]) .wrapper{--calcite-floating-ui-z-index: var(--calcite-z-index-dropdown)}.wrapper[popover]{padding:0;margin:0;border:none;background-color:transparent;overflow:visible;display:none}.wrapper:popover-open{display:block}.wrapper .calcite-floating-ui-anim{position:relative;transition-duration:var(--calcite-floating-ui-transition);transition-property:inset-block-start,left,opacity,display;transition-behavior:allow-discrete;opacity:0;box-shadow:0 0 16px #00000029;z-index:var(--calcite-z-index);border-radius:.25rem}.wrapper[data-placement^=bottom] .calcite-floating-ui-anim{inset-block-start:-5px}.wrapper[data-placement^=top] .calcite-floating-ui-anim{inset-block-start:5px}.wrapper[data-placement^=left] .calcite-floating-ui-anim{left:5px}.wrapper[data-placement^=right] .calcite-floating-ui-anim{left:-5px}.wrapper[data-placement] .calcite-floating-ui-anim--active{opacity:1;inset-block-start:0;left:0}@starting-style{.wrapper[data-placement] .calcite-floating-ui-anim--active{opacity:0}}.content{max-height:45vh;width:auto;overflow-y:auto;overflow-x:hidden;inline-size:var(--calcite-dropdown-width, var(--calcite-internal-dropdown-width));background-color:var(--calcite-dropdown-background-color, var(--calcite-color-foreground-1))}.trigger-container{position:relative;display:flex;height:100%;flex:1 1 auto;word-wrap:break-word;word-break:break-word}.width-s{--calcite-internal-dropdown-width: 12rem}.width-m{--calcite-internal-dropdown-width: 14rem}.width-l{--calcite-internal-dropdown-width: 16rem}@media(forced-colors:active){:host([open]) .wrapper{border:var(--calcite-border-width-sm) solid canvasText}}:host([hidden]){display:none}[hidden]{display:none}:host([disabled]) ::slotted([calcite-hydrated][disabled]),:host([disabled]) [calcite-hydrated][disabled]{opacity:1}.interaction-container{display:contents}`; var __defProp = Object.defineProperty; var __decorateClass = (decorators, target, key, kind) => { var result = void 0; for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = decorator(target, key, result) || result; if (result) __defProp(target, key, result); return result; }; class Dropdown extends LitElement { constructor() { super(); this.focusLastDropdownItem = false; this.groups = []; this.guid = guid(); this.items = []; this.mutationObserver = createObserver("mutation", () => this.updateItems()); this.transitionProp = "opacity"; this.resizeObserver = createObserver("resize", (entries) => this.resizeObserverCallback(entries)); this.focusSetter = useSetFocus()(this); this.interactiveContainer = useInteractive(this); this.topLayer = useTopLayer({ target: () => this.floatingEl })(this); this.closeOnSelectDisabled = false; this.disabled = false; this.maxItems = 0; this.offsetDistance = 0; this.offsetSkidding = 0; this.open = false; this.overlayPositioning = "absolute"; this.placement = defaultMenuPlacement; this.scale = "m"; this.selectedItems = []; this.topLayerDisabled = false; this.type = "click"; this.calciteDropdownBeforeClose = createEvent({ cancelable: false }); this.calciteDropdownBeforeOpen = createEvent({ cancelable: false }); this.calciteDropdownClose = createEvent({ cancelable: false }); this.calciteDropdownOpen = createEvent({ cancelable: false }); this.calciteDropdownSelect = createEvent({ cancelable: false }); this.listenOn(window, "click", this.closeCalciteDropdownOnClick); this.listen("calciteInternalDropdownCloseRequest", this.closeCalciteDropdownOnEvent); this.listenOn(window, "calciteDropdownOpen", this.closeCalciteDropdownOnOpenEvent); this.listen("pointerenter", this.pointerEnterHandler); this.listen("pointerleave", this.pointerLeaveHandler); this.listen("calciteInternalDropdownItemKeyEvent", this.calciteInternalDropdownItemKeyEvent); this.listen("calciteInternalDropdownItemSelect", this.handleItemSelect); } static { this.properties = { closeOnSelectDisabled: [7, {}, { reflect: true, type: Boolean }], disabled: [7, {}, { reflect: true, type: Boolean }], flipPlacements: [0, {}, { attribute: false }], maxItems: [11, {}, { reflect: true, type: Number }], offsetDistance: [11, {}, { type: Number, reflect: true }], offsetSkidding: [11, {}, { reflect: true, type: Number }], open: [7, {}, { reflect: true, type: Boolean }], overlayPositioning: [3, {}, { reflect: true }], placement: [3, {}, { reflect: true }], scale: [3, {}, { reflect: true }], selectedItems: [0, {}, { attribute: false }], topLayerDisabled: [7, {}, { reflect: true, type: Boolean }], type: [3, {}, { reflect: true }], widthScale: [3, {}, { reflect: true }], width: [3, {}, { reflect: true }] }; } static { this.shadowRootOptions = { mode: "open", delegatesFocus: true }; } static { this.styles = styles; } async reposition(delayed = false) { const { filteredFlipPlacements, floatingEl, offsetDistance, offsetSkidding, overlayPositioning, placement, referenceEl } = this; return reposition(this, { floatingEl, referenceEl, offsetDistance, offsetSkidding, overlayPositioning, placement, flipPlacements: filteredFlipPlacements, type: "menu" }, delayed); } async setFocus(options) { return this.focusSetter(() => this.referenceEl, options); } connectedCallback() { super.connectedCallback(); this.mutationObserver?.observe(this.el, { childList: true, subtree: true }); this.setFilteredPlacements(); this.updateItems(); connectFloatingUI(this); } willUpdate(changes) { if (changes.has("open") && (this.hasUpdated || this.open !== false)) { this.openHandler(); } if (changes.has("disabled") && (this.hasUpdated || this.disabled !== false)) { this.handleDisabledChange(this.disabled); } if (changes.has("flipPlacements")) { this.flipPlacementsHandler(); } if (changes.has("maxItems") && this.hasUpdated) { this.setMaxScrollerHeight(); } if (this.hasUpdated && (changes.has("offsetDistance") && this.offsetDistance !== 0 || changes.has("offsetSkidding") && this.offsetSkidding !== 0 || changes.has("overlayPositioning") && this.overlayPositioning !== "absolute" || changes.has("placement") && this.placement !== defaultMenuPlacement)) { this.reposition(true); } if (changes.has("scale") && (this.hasUpdated || this.scale !== "m")) { this.handlePropsChange(); } } loaded() { this.updateSelectedItems(); connectFloatingUI(this); } disconnectedCallback() { super.disconnectedCallback(); this.mutationObserver?.disconnect(); this.resizeObserver?.disconnect(); disconnectFloatingUI(this); } openHandler() { if (this.disabled) { return; } toggleOpenClose(this); this.reposition(true); } handleDisabledChange(value) { if (!value) { this.open = false; } } flipPlacementsHandler() { this.setFilteredPlacements(); this.reposition(true); } handlePropsChange() { this.updateItems(); this.updateGroupProps(); } closeCalciteDropdownOnClick(event) { if (this.disabled || !this.open || event.composedPath().includes(this.el)) { return; } this.closeCalciteDropdown(false); } closeCalciteDropdownOnEvent(event) { this.closeCalciteDropdown(); event.stopPropagation(); } closeCalciteDropdownOnOpenEvent(event) { if (event.composedPath().includes(this.el)) { return; } this.open = false; } pointerEnterHandler() { if (this.disabled || this.type !== "hover") { return; } this.toggleDropdown(); } pointerLeaveHandler() { if (this.disabled || this.type !== "hover") { return; } this.closeCalciteDropdown(); } getTraversableItems() { return this.items.filter((item) => !item.disabled && !item.hidden); } calciteInternalDropdownItemKeyEvent(event) { const { keyboardEvent } = event.detail; const target = keyboardEvent.target; const traversableItems = this.getTraversableItems(); switch (keyboardEvent.key) { case "Tab": this.open = false; this.updateTabIndexOfItems(target); break; case "ArrowDown": focusElementInGroup(traversableItems, target, "next"); break; case "ArrowUp": focusElementInGroup(traversableItems, target, "previous"); break; case "Home": focusElementInGroup(traversableItems, target, "first"); break; case "End": focusElementInGroup(traversableItems, target, "last"); break; } event.stopPropagation(); } handleItemSelect(event) { this.updateSelectedItems(); event.stopPropagation(); this.calciteDropdownSelect.emit(); if (!this.closeOnSelectDisabled) { this.closeCalciteDropdown(); } } setFilteredPlacements() { const { el, flipPlacements } = this; this.filteredFlipPlacements = flipPlacements ? filterValidFlipPlacements(flipPlacements, el) : null; } updateItems() { this.items = this.groups.map((group) => Array.from(group?.querySelectorAll("calcite-dropdown-item"))).reduce((previousValue, currentValue) => [...previousValue, ...currentValue], []); this.updateSelectedItems(); this.reposition(true); this.items.forEach((item) => item.scale = this.scale); } updateGroups(event) { const groups = event.target.assignedElements({ flatten: true }).filter((el) => el?.matches("calcite-dropdown-group")); this.groups = groups; this.updateItems(); this.updateGroupProps(); } updateGroupProps() { this.groups.forEach((group, index) => { group.scale = this.scale; group.position = index; }); } resizeObserverCallback(entries) { entries.forEach(({ target }) => { if (target === this.referenceEl) { this.setDropdownWidth(); } else if (target === this.scrollerEl) { this.setMaxScrollerHeight(); } }); } setDropdownWidth() { const { referenceEl, scrollerEl } = this; if (!scrollerEl || !referenceEl) { return; } scrollerEl.style.minWidth = `${referenceEl.clientWidth}px`; } setMaxScrollerHeight() { const { maxItems, items, scrollerEl } = this; if (!scrollerEl) { return; } const maxScrollerHeight = items.length >= maxItems && maxItems > 0 ? this.getYDistanceFromScroller(items.at(maxItems - 1)) : 0; scrollerEl.style.maxBlockSize = maxScrollerHeight > 0 ? `${maxScrollerHeight}px` : ""; this.reposition(true); } setScrollerAndTransitionEl(el) { updateRefObserver(this.resizeObserver, this.scrollerEl, el); this.scrollerEl = el; this.transitionEl = el; } onBeforeOpen() { this.focusOnFirstActiveOrDefaultItem(); this.calciteDropdownBeforeOpen.emit(); this.topLayer.show(); } onOpen() { this.calciteDropdownOpen.emit(); } onBeforeClose() { this.calciteDropdownBeforeClose.emit(); } onClose() { this.calciteDropdownClose.emit(); hideFloatingUI(this); this.topLayer.hide(); } setReferenceEl(el) { updateRefObserver(this.resizeObserver, this.referenceEl, el); this.referenceEl = el; connectFloatingUI(this); } setFloatingEl(el) { this.floatingEl = el; connectFloatingUI(this); } keyDownHandler(event) { if (!event.composedPath().includes(this.referenceEl)) { return; } const { defaultPrevented, key } = event; if (defaultPrevented) { return; } if (key === "Escape") { this.closeCalciteDropdown(); event.preventDefault(); return; } if (this.open && event.shiftKey && key === "Tab") { this.closeCalciteDropdown(); event.preventDefault(); return; } if (isActivationKey(key)) { this.toggleDropdown(); event.preventDefault(); } else if (key === "ArrowDown" || key === "ArrowUp") { event.preventDefault(); this.focusLastDropdownItem = key === "ArrowUp"; this.open = true; } } updateSelectedItems() { this.selectedItems = this.items.filter((item) => item.selected); } getYDistanceFromScroller(last) { const style = last.getBoundingClientRect(); return last.offsetTop + style.height; } closeCalciteDropdown(focusTrigger = true) { this.open = false; if (focusTrigger) { focusElement(this.triggerEls[0]); } } async focusOnFirstActiveOrDefaultItem() { const selectedItem = this.getTraversableItems().find((item) => item.selected); const target = selectedItem || (this.focusLastDropdownItem ? this.items.at(-1) : this.items[0]); this.focusLastDropdownItem = false; if (!target) { return; } await this.updateComplete; await nextFrame(); await nextFrame(); await focusElement(target); target.scrollIntoView({ block: "nearest" }); } toggleDropdown() { this.open = !this.open; } updateTabIndexOfItems(target) { this.items.forEach((item) => { item.tabIndex = target !== item ? -1 : 0; }); } render() { const { open, guid: guid2 } = this; return this.interactiveContainer({ disabled: this.disabled, children: html`<div class=${safeClassMap(CSS.triggerContainer)} id=${IDS.menuButton(guid2) ?? nothing} @click=${this.toggleDropdown} @keydown=${this.keyDownHandler} ${ref(this.setReferenceEl)}><slot aria-controls=${IDS.menu(guid2) ?? nothing} .ariaExpanded=${open} aria-haspopup=menu name=${SLOTS.trigger}></slot></div><div .ariaHidden=${!open} class=${safeClassMap({ [CSS.wrapper]: true, [getDimensionClass("width", this.width, this.widthScale)]: !!(this.width || this.widthScale) })} popover=manual ${ref(this.setFloatingEl)}><div aria-labelledby=${IDS.menuButton(guid2) ?? nothing} class=${safeClassMap({ [CSS.content]: true, [FloatingCSS.animation]: true, [FloatingCSS.animationActive]: open })} id=${IDS.menu(guid2) ?? nothing} role=menu ${ref(this.setScrollerAndTransitionEl)}><slot @slotchange=${this.updateGroups}></slot></div></div>` }); } } __decorateClass([ queryAssignedElements({ slot: SLOTS.trigger }) ], Dropdown.prototype, "triggerEls"); customElement("calcite-dropdown", Dropdown); export { Dropdown };