@esri/calcite-components
Version:
Web Components for Esri's Calcite Design System.
382 lines (381 loc) • 16.3 kB
JavaScript
/* 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)}-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}-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}{: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} =${this.toggleDropdown} =${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 =${this.updateGroups}></slot></div></div>` });
}
}
__decorateClass([
queryAssignedElements({ slot: SLOTS.trigger })
], Dropdown.prototype, "triggerEls");
customElement("calcite-dropdown", Dropdown);
export {
Dropdown
};