@esri/calcite-components
Version:
Web Components for Esri's Calcite Design System.
372 lines (371 loc) • 15.5 kB
JavaScript
/*! 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} (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`} =${this.toggleDropdown} =${this.keyDownHandler} ${ref(this.setReferenceEl)}><slot aria-controls=${`${guid2}-menu`} .ariaExpanded=${open} aria-haspopup=menu name=${SLOTS.dropdownTrigger} =${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 =${this.updateGroups}></slot></div></div>` });
}
}
customElement("calcite-dropdown", Dropdown);
export {
Dropdown
};