UNPKG

@esri/calcite-components

Version:

Web Components for Esri's Calcite Design System.

372 lines (371 loc) • 15.5 kB
/*! All material copyright ESRI, All Rights Reserved, unless otherwise specified. See https://github.com/Esri/calcite-design-system/blob/dev/LICENSE.md for details. v3.2.1 */ import { c as customElement } from "../../chunks/runtime.js"; import { ref } from "lit-html/directives/ref.js"; import { html } from "lit"; import { LitElement, createEvent, safeClassMap } from "@arcgis/lumina"; import { h as focusFirstTabbable, d as focusElementInGroup, b as focusElement } from "../../chunks/dom.js"; import { b 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 { u as updateHostInteraction, I as InteractiveContainer } from "../../chunks/interactive.js"; import { i as isActivationKey } from "../../chunks/key.js"; import { c as componentFocusable } from "../../chunks/component.js"; import { c as createObserver } from "../../chunks/observers.js"; import { o as onToggleOpenCloseComponent } from "../../chunks/openCloseComponent.js"; import { g as getDimensionClass } from "../../chunks/dynamicClasses.js"; import { css } from "@lit/reactive-element/css-tag.js"; const SLOTS = { dropdownTrigger: "trigger" }; const CSS = { content: "calcite-dropdown-content", wrapper: "calcite-dropdown-wrapper" }; 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}.calcite-dropdown-wrapper{--calcite-floating-ui-z-index: var(--calcite-z-index-dropdown);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)}.calcite-dropdown-wrapper .calcite-floating-ui-anim{position:relative;transition:var(--calcite-floating-ui-transition);transition-property:inset,left,opacity;opacity:0;box-shadow:0 0 16px #00000029;z-index:var(--calcite-z-index);border-radius:.25rem}.calcite-dropdown-wrapper[data-placement^=bottom] .calcite-floating-ui-anim{inset-block-start:-5px}.calcite-dropdown-wrapper[data-placement^=top] .calcite-floating-ui-anim{inset-block-start:5px}.calcite-dropdown-wrapper[data-placement^=left] .calcite-floating-ui-anim{left:5px}.calcite-dropdown-wrapper[data-placement^=right] .calcite-floating-ui-anim{left:-5px}.calcite-dropdown-wrapper[data-placement] .calcite-floating-ui-anim--active{opacity:1;inset-block-start:0;left:0}.calcite-dropdown-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))}.calcite-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]) .calcite-dropdown-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}`; class Dropdown extends LitElement { constructor() { super(); this.focusLastDropdownItem = false; this.groups = []; this.guid = `calcite-dropdown-${guid()}`; this.items = []; this.mutationObserver = createObserver("mutation", () => this.updateItems()); this.transitionProp = "opacity"; this.resizeObserver = createObserver("resize", (entries) => this.resizeObserverCallback(entries)); 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.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 }], 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() { await componentFocusable(this); focusFirstTabbable(this.referenceEl); } 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(); } } updated() { updateHostInteraction(this); } loaded() { this.updateSelectedItems(); connectFloatingUI(this); } disconnectedCallback() { super.disconnectedCallback(); this.mutationObserver?.disconnect(); this.resizeObserver?.disconnect(); disconnectFloatingUI(this); } openHandler() { onToggleOpenCloseComponent(this); if (this.disabled) { return; } 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 || event.detail.requestedDropdownGroup.selectionMode === "none") { this.closeCalciteDropdown(); } event.stopPropagation(); } setFilteredPlacements() { const { el, flipPlacements } = this; this.filteredFlipPlacements = flipPlacements ? filterValidFlipPlacements(flipPlacements, el) : null; } updateTriggers(event) { this.triggers = event.target.assignedElements({ flatten: true }); this.reposition(true); } 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((entry) => { const { target } = entry; if (!this.hasUpdated) { return; } if (target === this.referenceEl) { this.setDropdownWidth(); } else if (target === this.scrollerEl) { this.setMaxScrollerHeight(); } }); } setDropdownWidth() { const { referenceEl, scrollerEl } = this; const referenceElWidth = referenceEl?.clientWidth; scrollerEl.style.minWidth = `${referenceElWidth}px`; } setMaxScrollerHeight() { const maxScrollerHeight = this.getMaxScrollerHeight(); this.scrollerEl.style.maxBlockSize = maxScrollerHeight > 0 ? `${maxScrollerHeight}px` : ""; this.reposition(true); } setScrollerAndTransitionEl(el) { if (!el) { return; } this.resizeObserver?.observe(el); this.scrollerEl = el; this.transitionEl = el; } onBeforeOpen() { this.calciteDropdownBeforeOpen.emit(); } async onOpen() { this.focusOnFirstActiveOrDefaultItem(); this.calciteDropdownOpen.emit(); } onBeforeClose() { this.calciteDropdownBeforeClose.emit(); } onClose() { this.calciteDropdownClose.emit(); hideFloatingUI(this); } setReferenceEl(el) { this.referenceEl = el; connectFloatingUI(this); if (el) { this.resizeObserver?.observe(el); } } 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); } getMaxScrollerHeight() { const { maxItems, items } = this; return items.length >= maxItems && maxItems > 0 ? this.getYDistance(this.scrollerEl, items[maxItems - 1]) : 0; } getYDistance(parent, child) { const parentRect = parent.getBoundingClientRect(); const childRect = child.getBoundingClientRect(); return childRect.bottom - parentRect.top; } closeCalciteDropdown(focusTrigger = true) { this.open = false; if (focusTrigger) { focusElement(this.triggers[0]); } } focusOnFirstActiveOrDefaultItem() { const selectedItem = this.getTraversableItems().find((item) => item.selected); const target = selectedItem || (this.focusLastDropdownItem ? this.items[this.items.length - 1] : this.items[0]); this.focusLastDropdownItem = false; if (!target) { return; } focusElement(target); } toggleDropdown() { this.open = !this.open; } updateTabIndexOfItems(target) { this.items.forEach((item) => { item.tabIndex = target !== item ? -1 : 0; }); } render() { const { open, guid: guid2 } = this; return InteractiveContainer({ disabled: this.disabled, children: html`<div class="calcite-trigger-container" .id=${`${guid2}-menubutton`} @click=${this.toggleDropdown} @keydown=${this.keyDownHandler} ${ref(this.setReferenceEl)}><slot aria-controls=${`${guid2}-menu`} .ariaExpanded=${open} aria-haspopup=menu name=${SLOTS.dropdownTrigger} @slotchange=${this.updateTriggers}></slot></div><div .ariaHidden=${!open} class=${safeClassMap({ [CSS.wrapper]: true, [getDimensionClass("width", this.width, this.widthScale)]: !!(this.width || this.widthScale) })} ${ref(this.setFloatingEl)}><div aria-labelledby=${`${guid2}-menubutton`} class=${safeClassMap({ [CSS.content]: true, [FloatingCSS.animation]: true, [FloatingCSS.animationActive]: open })} .id=${`${guid2}-menu`} role=menu ${ref(this.setScrollerAndTransitionEl)}><slot @slotchange=${this.updateGroups}></slot></div></div>` }); } } customElement("calcite-dropdown", Dropdown); export { Dropdown };