UNPKG

@esri/calcite-components

Version:

Web Components for Esri's Calcite Design System.

460 lines (459 loc) • 19.3 kB
/* COPYRIGHT Esri - https://js.arcgis.com/5.1/LICENSE.txt */ import { c as customElement } from "../../chunks/runtime.js"; import { ref } from "lit/directives/ref.js"; import { css, html } from "lit"; import { LitElement, createEvent, safeClassMap } from "@arcgis/lumina"; import { useDirection } from "@arcgis/lumina/controllers"; import { u 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 { 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"; import { u as useReferenceElement, r as referenceElementManager } from "../../chunks/manager.js"; const SLOTS = { trigger: "trigger" }; const CSS = { content: "content", wrapper: "wrapper", triggerContainer: "trigger-container" }; 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}`; const manager = referenceElementManager({ click: true, hover: true }); class Dropdown extends LitElement { constructor() { super(); this.referenceElementController = useReferenceElement({ manager })(this); this.direction = useDirection(); this.focusLastDropdownItem = false; this.activeItemIndex = -1; this.groups = []; this.items = []; this.mutationObserver = createObserver("mutation", () => this.updateItems()); this.transitionProp = "opacity"; this.resizeObserver = createObserver("resize", (entries) => this.resizeObserverCallback(entries)); this.onReferenceElementKeyDown = (event) => this.keyDownHandler(event); 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.listenOn(window, "calciteDropdownOpen", this.closeCalciteDropdownOnOpenEvent); this.listen("pointerenter", this.pointerEnterHandler); this.listen("pointerleave", this.pointerLeaveHandler); this.listen("calciteInternalDropdownItemSelect", this.handleItemSelect); } static { this.properties = { activeDescendantElement: [16, {}, { state: true }], referenceEl: [16, {}, { state: true }], 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 }], referenceElement: 1, 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; } get referenceElementType() { return this.referenceElement ? this.type : null; } async reposition(delayed = false) { const { filteredFlipPlacements, floatingEl, offsetDistance, offsetSkidding, overlayPositioning, placement, referenceEl } = this; return reposition(this, { direction: this.direction, floatingEl, referenceEl, offsetDistance, offsetSkidding, overlayPositioning, placement, flipPlacements: filteredFlipPlacements, type: "menu" }, delayed); } async setFocus(options) { return this.focusSetter(() => this.referenceEl instanceof HTMLElement ? this.referenceEl : this.floatingEl, 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(); } if (changes.has("referenceElement") && !this.referenceElement && this.open) { this.topLayer.hide(); } } updated(changes) { if (changes.has("referenceEl") && this.referenceElementType) { connectFloatingUI(this); } } 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.referenceElementType || this.disabled || !this.open || event.composedPath().includes(this.el)) { return; } this.closeCalciteDropdown(); } closeCalciteDropdownOnOpenEvent(event) { if (this.referenceElementType || event.composedPath().includes(this.el)) { return; } this.closeCalciteDropdown(); } pointerEnterHandler() { if (this.referenceElementType || this.disabled || this.type !== "hover") { return; } this.open = true; } pointerLeaveHandler() { if (this.referenceElementType || this.disabled || this.type !== "hover") { return; } this.closeCalciteDropdown(); } getTraversableItems() { return this.items.filter((item) => !item.disabled && !item.hidden); } async handleItemSelect(event) { this.updateSelectedItems(); this.syncActiveItemFromTraversableItems(); event.stopPropagation(); this.calciteDropdownSelect.emit(); await this.setFocus(); 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.syncActiveItemFromTraversableItems(); 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 instanceof HTMLElement)) { 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.setInitialActiveItem(); 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) { const previousReferenceEl = this.referenceEl instanceof HTMLElement ? this.referenceEl : null; const nextReferenceEl = el instanceof HTMLElement ? el : null; updateRefObserver(this.resizeObserver, previousReferenceEl, nextReferenceEl); this.referenceEl = el; connectFloatingUI(this); } setFloatingEl(el) { this.floatingEl = el; connectFloatingUI(this); } keyDownHandler(event) { if (!(this.referenceEl instanceof HTMLElement) || !event.composedPath().includes(this.referenceEl)) { return; } const { defaultPrevented, key } = event; if (defaultPrevented) { return; } if (this.open && key === "Escape") { this.closeCalciteDropdown(); event.preventDefault(); return; } if (!this.open && isActivationKey(key)) { this.open = true; event.preventDefault(); return; } if (!this.open && (key === "ArrowDown" || key === "ArrowUp")) { event.preventDefault(); this.focusLastDropdownItem = key === "ArrowUp"; this.open = true; return; } if (!this.open) { return; } if (key === "Tab") { this.closeCalciteDropdown(); return; } if (key === "ArrowDown") { event.preventDefault(); this.navigateActiveItem("next"); return; } if (key === "ArrowUp") { event.preventDefault(); this.navigateActiveItem("previous"); return; } if (key === "Home") { event.preventDefault(); this.navigateActiveItem("first"); return; } if (key === "End") { event.preventDefault(); this.navigateActiveItem("last"); return; } if (isActivationKey(key)) { event.preventDefault(); this.activateActiveItem(); } } updateSelectedItems() { this.selectedItems = this.items.filter((item) => item.selected); } getYDistanceFromScroller(last) { const style = last.getBoundingClientRect(); return last.offsetTop + style.height; } closeCalciteDropdown() { this.open = false; this.setActiveItemByIndex(-1); } async setInitialActiveItem() { const traversableItems = this.getTraversableItems(); const target = this.focusLastDropdownItem ? traversableItems.at(-1) : traversableItems[0]; this.focusLastDropdownItem = false; if (!target) { this.setActiveItemByIndex(-1); return; } const targetIndex = traversableItems.findIndex((item) => item === target); this.setActiveItemByIndex(targetIndex); await this.scrollActiveItemIntoView(target); } syncActiveItemFromTraversableItems() { const traversableItems = this.getTraversableItems(); if (!traversableItems.length) { this.setActiveItemByIndex(-1); return; } if (this.activeItemIndex < 0 || this.activeItemIndex >= traversableItems.length) { this.setActiveItemByIndex(0); return; } this.updateActiveDescendantElement(traversableItems[this.activeItemIndex]); } setActiveItemByIndex(index) { this.activeItemIndex = index; const traversableItems = this.getTraversableItems(); const activeItem = index >= 0 ? traversableItems[index] : null; this.updateActiveDescendantElement(activeItem); } updateActiveDescendantElement(activeItem) { this.items.forEach((item) => { item.activeDescendant = item === activeItem; }); this.activeDescendantElement = activeItem ?? null; } navigateActiveItem(direction) { const traversableItems = this.getTraversableItems(); if (!traversableItems.length) { return; } const totalItems = traversableItems.length; let index = this.activeItemIndex; if (index < 0 || index >= totalItems) { index = direction === "previous" || direction === "last" ? totalItems - 1 : 0; } else if (direction === "next") { index = (index + 1) % totalItems; } else if (direction === "previous") { index = (index - 1 + totalItems) % totalItems; } else if (direction === "first") { index = 0; } else if (direction === "last") { index = totalItems - 1; } const activeItem = traversableItems[index]; this.setActiveItemByIndex(index); void this.scrollActiveItemIntoView(activeItem); } async scrollActiveItemIntoView(target) { if (!target) { return; } await this.updateComplete; await nextFrame(); await nextFrame(); target.scrollIntoView({ block: "nearest" }); } activateActiveItem() { const traversableItems = this.getTraversableItems(); const activeItem = traversableItems[this.activeItemIndex] || traversableItems[0]; if (!activeItem) { return; } this.setActiveItemByIndex(traversableItems.findIndex((item) => item === activeItem)); activeItem.activateItem(); } openHoverDropdown() { if (this.open || this.disabled || this.type !== "hover") { return; } this.open = true; } closeHoverDropdown(event) { if (!this.open || this.disabled || this.type !== "hover") { return; } const relatedTarget = event.relatedTarget; if (relatedTarget && (this.el.contains(relatedTarget) || this.referenceEl != null && this.referenceEl instanceof HTMLElement && this.referenceEl.contains(relatedTarget))) { return; } this.closeCalciteDropdown(); } toggleClickDropdown() { if (this.disabled || this.type !== "click") { return; } this.open = !this.open; } render() { const { open } = this; return this.interactiveContainer({ disabled: this.disabled, children: html`${!this.referenceElementType ? html`<div class=${safeClassMap(CSS.triggerContainer)} @click=${this.toggleClickDropdown} @focusin=${this.openHoverDropdown} @focusout=${this.closeHoverDropdown} @keydown=${this.keyDownHandler} ${ref(this.setReferenceEl)}><slot .ariaActiveDescendantElement=${this.activeDescendantElement ?? null} .ariaControlsElements=${this.scrollerEl ? [this.scrollerEl] : void 0} .ariaExpanded=${open} aria-haspopup=menu name=${SLOTS.trigger}></slot></div>` : null}<div class=${safeClassMap({ [CSS.wrapper]: true, [getDimensionClass("width", this.width, this.widthScale)]: !!(this.width || this.widthScale) })} .inert=${!open} popover=manual ${ref(this.setFloatingEl)}><div .ariaLabelledByElements=${this.referenceEl instanceof HTMLElement ? [this.referenceEl] : void 0} class=${safeClassMap({ [CSS.content]: true, [FloatingCSS.animation]: true, [FloatingCSS.animationActive]: open })} role=menu ${ref(this.setScrollerAndTransitionEl)}><slot @slotchange=${this.updateGroups}></slot></div></div>` }); } } customElement("calcite-dropdown", Dropdown); export { Dropdown };