UNPKG

@esri/calcite-components

Version:

Web Components for Esri's Calcite Design System.

611 lines (610 loc) • 25.8 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 { D as DEBOUNCE, c as customElement } from "../../chunks/runtime.js"; import { ref } from "lit-html/directives/ref.js"; import { keyed } from "lit-html/directives/keyed.js"; import { debounce } from "lodash-es"; import { html } from "lit"; import { LitElement, createEvent, safeClassMap } from "@arcgis/lumina"; import { a as slotChangeHasAssignedElement, y as getRootNode } from "../../chunks/dom.js"; import { u as updateHostInteraction, I as InteractiveContainer } from "../../chunks/interactive.js"; import { c as createObserver } from "../../chunks/observers.js"; import { b as listItemSelector, l as listSelector, u as updateListItemChildren, c as listItemGroupSelector, i as isListItem, e as expandedAncestors } from "../../chunks/utils.js"; import { d as disconnectSortableComponent, c as connectSortableComponent } from "../../chunks/sortableComponent.js"; import { S as SLOTS$1 } from "../../chunks/resources.js"; import { c as componentFocusable } from "../../chunks/component.js"; import { n as numberStringFormatter } from "../../chunks/locale.js"; import { g as guid } from "../../chunks/guid.js"; import { u as useT9n } from "../../chunks/useT9n.js"; import { css } from "@lit/reactive-element/css-tag.js"; const CSS = { container: "container", table: "table", scrim: "scrim", stack: "stack", tableContainer: "table-container", sticky: "sticky-pos", assistiveText: "assistive-text" }; const SLOTS = { filterNoResults: "filter-no-results", filterActionsStart: "filter-actions-start", filterActionsEnd: "filter-actions-end" }; 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:block}:host([disabled]) ::slotted([calcite-hydrated][disabled]),:host([disabled]) [calcite-hydrated][disabled]{opacity:1}.interaction-container{display:contents}.container{position:relative;background-color:var(--calcite-list-background-color, var(--calcite-color-foreground-1))}.table-container{box-sizing:border-box;display:flex;inline-size:100%;flex-direction:column;background-color:transparent}.table-container *{box-sizing:border-box}.table{inline-size:100%}.stack{--calcite-stack-padding-inline: 0;--calcite-stack-padding-block: 0}.sticky-pos{position:sticky;inset-block-start:0px;z-index:var(--calcite-z-index-sticky);background-color:var(--calcite-list-background-color, var(--calcite-color-foreground-1))}.assistive-text{position:absolute;inline-size:1px;block-size:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}:host([hidden]){display:none}[hidden]{display:none}`; const parentSelector = `${listItemGroupSelector}, ${listItemSelector}`; class List extends LitElement { constructor() { super(); this.dragSelector = listItemSelector; this.focusableItems = []; this.handleSelector = "calcite-sort-handle"; this.listItems = []; this.listItemGroups = []; this.mutationObserver = createObserver("mutation", () => { this.willPerformFilter = true; this.updateListItems(); }); this.updateListItems = debounce(() => { this.updateGroupItems(); const { selectionAppearance, selectionMode, interactionMode, dragEnabled, el, filterEl, moveToItems, displayMode, scale } = this; const items = Array.from(this.el.querySelectorAll(listItemSelector)); items.forEach((item) => { item.scale = scale; item.selectionAppearance = selectionAppearance; item.selectionMode = selectionMode; item.interactionMode = interactionMode; if (item.closest(listSelector) === el) { item.moveToItems = moveToItems.filter((moveToItem) => moveToItem.element !== el && !item.contains(moveToItem.element)); item.dragHandle = dragEnabled; item.displayMode = displayMode; } }); if (this.parentListEl) { this.setUpSorting(); return; } this.listItems = items; if (this.filterEnabled && this.willPerformFilter) { this.willPerformFilter = false; this.dataForFilter = this.getItemData(); if (filterEl) { filterEl.items = this.dataForFilter; this.filterAndUpdateData(); } } this.visibleItems = this.listItems.filter((item) => !item.closed && !item.hidden); this.updateFilteredItems(); this.borderItems(); this.focusableItems = this.filteredItems.filter((item) => !item.disabled); this.setActiveListItem(); this.updateSelectedItems(); this.setUpSorting(); }, DEBOUNCE.nextTick); this.visibleItems = []; this.willFilterEmit = false; this.willPerformFilter = false; this.messages = useT9n({ blocking: true }); this.dataForFilter = []; this.hasFilterActionsEnd = false; this.hasFilterActionsStart = false; this.hasFilterNoResults = false; this.moveToItems = []; this.disabled = false; this.dragEnabled = false; this.filterEnabled = false; this.filteredData = []; this.filteredItems = []; this.interactionMode = "interactive"; this.loading = false; this.displayMode = "flat"; this.scale = "m"; this.selectedItems = []; this.selectionAppearance = "icon"; this.selectionMode = "none"; this.calciteInternalListDefaultSlotChange = createEvent({ cancelable: false }); this.calciteListChange = createEvent({ cancelable: false }); this.calciteListDragEnd = createEvent({ cancelable: false }); this.calciteListDragStart = createEvent({ cancelable: false }); this.calciteListFilter = createEvent({ cancelable: false }); this.calciteListMoveHalt = createEvent({ cancelable: false }); this.calciteListOrderChange = createEvent({ cancelable: false }); this.listen("calciteInternalListItemToggle", this.handleCalciteListItemToggle); this.listen("calciteInternalFocusPreviousItem", this.handleCalciteInternalFocusPreviousItem); this.listen("calciteInternalListItemActive", this.handleCalciteInternalListItemActive); this.listen("calciteListItemSelect", this.handleCalciteListItemSelect); this.listen("calciteInternalAssistiveTextChange", this.handleCalciteInternalAssistiveTextChange); this.listen("calciteSortHandleReorder", this.handleSortReorder); this.listen("calciteSortHandleMove", this.handleSortMove); this.listen("calciteInternalListItemSelect", this.handleCalciteInternalListItemSelect); this.listen("calciteInternalListItemSelectMultiple", this.handleCalciteInternalListItemSelectMultiple); this.listen("calciteInternalListItemChange", this.handleCalciteInternalListItemChange); this.listen("calciteInternalListItemGroupDefaultSlotChange", this.handleCalciteInternalListItemGroupDefaultSlotChange); } static { this.properties = { assistiveText: [16, {}, { state: true }], dataForFilter: [16, {}, { state: true }], hasFilterActionsEnd: [16, {}, { state: true }], hasFilterActionsStart: [16, {}, { state: true }], hasFilterNoResults: [16, {}, { state: true }], moveToItems: [16, {}, { state: true }], hasActiveFilter: [16, {}, { state: true }], showNoResultsContainer: [16, {}, { state: true }], canPull: [0, {}, { attribute: false }], canPut: [0, {}, { attribute: false }], disabled: [7, {}, { reflect: true, type: Boolean }], dragEnabled: [7, {}, { reflect: true, type: Boolean }], filterEnabled: [7, {}, { reflect: true, type: Boolean }], filterPredicate: [0, {}, { attribute: false }], filterLabel: [3, {}, { reflect: true }], filterPlaceholder: [3, {}, { reflect: true }], filterProps: [0, {}, { attribute: false }], filterText: [3, {}, { reflect: true }], filteredData: [0, {}, { attribute: false }], filteredItems: [0, {}, { attribute: false }], group: [3, {}, { reflect: true }], interactionMode: [3, {}, { reflect: true }], label: 1, loading: [7, {}, { reflect: true, type: Boolean }], messageOverrides: [0, {}, { attribute: false }], displayMode: [3, {}, { reflect: true }], numberingSystem: 1, scale: [3, {}, { reflect: true }], selectedItems: [0, {}, { attribute: false }], selectionAppearance: [3, {}, { reflect: true }], selectionMode: [3, {}, { reflect: true }] }; } static { this.styles = styles; } get hasActiveFilter() { return this.filterEnabled && this.filterText && this.filteredItems.length !== this.visibleItems.length; } get showNoResultsContainer() { return this.filterEnabled && this.filterText && this.hasFilterNoResults && this.visibleItems.length && !this.filteredItems.length; } putFailed(dragDetail) { this.calciteListMoveHalt.emit(dragDetail); } async setFocus() { await componentFocusable(this); if (this.filterEnabled) { return this.filterEl?.setFocus(); } return this.focusableItems.find((listItem) => listItem.active)?.setFocus(); } connectedCallback() { super.connectedCallback(); this.connectObserver(); this.willPerformFilter = true; this.updateListItems(); this.setUpSorting(); this.setParentList(); this.setListItemGroups(); } async load() { this.handleInteractionModeWarning(); } willUpdate(changes) { if (changes.has("filterText") || changes.has("filterProps") || changes.has("filterPredicate")) { this.performFilter(); } if (changes.has("filterEnabled") && (this.hasUpdated || this.filterEnabled !== false) || changes.has("group") || changes.has("dragEnabled") && (this.hasUpdated || this.dragEnabled !== false) || changes.has("selectionMode") && (this.hasUpdated || this.selectionMode !== "none") || changes.has("selectionAppearance") && (this.hasUpdated || this.selectionAppearance !== "icon") || changes.has("displayMode") && this.hasUpdated || changes.has("scale") && this.hasUpdated || changes.has("filterPredicate") && this.hasUpdated) { this.handleListItemChange(); } } updated() { updateHostInteraction(this); } disconnectedCallback() { super.disconnectedCallback(); this.disconnectObserver(); disconnectSortableComponent(this); } handleListItemChange() { this.willPerformFilter = true; this.updateListItems(); } handleCalciteListItemToggle(event) { if (this.parentListEl) { return; } event.stopPropagation(); this.borderItems(); } handleCalciteInternalFocusPreviousItem(event) { if (this.parentListEl) { return; } event.stopPropagation(); const { focusableItems } = this; const currentIndex = focusableItems.findIndex((listItem) => listItem.active); const prevIndex = currentIndex - 1; if (focusableItems[prevIndex]) { this.focusRow(focusableItems[prevIndex]); } } handleCalciteInternalListItemActive(event) { if (this.parentListEl) { return; } event.stopPropagation(); const target = event.target; const { listItems } = this; listItems.forEach((listItem) => { listItem.active = listItem === target; }); } handleCalciteListItemSelect() { if (this.parentListEl) { return; } this.updateSelectedItems(true); } handleCalciteInternalAssistiveTextChange(event) { this.assistiveText = event.detail.message; event.stopPropagation(); } handleSortReorder(event) { if (this.parentListEl || event.defaultPrevented) { return; } event.preventDefault(); this.handleReorder(event); } handleSortMove(event) { if (this.parentListEl || event.defaultPrevented) { return; } event.preventDefault(); this.handleMove(event); } handleCalciteInternalListItemSelect(event) { if (this.parentListEl) { return; } event.stopPropagation(); const target = event.target; const { listItems, selectionMode } = this; if (target.selected && (selectionMode === "single" || selectionMode === "single-persist")) { listItems.forEach((listItem) => listItem.selected = listItem === target); } this.updateSelectedItems(); } handleCalciteInternalListItemSelectMultiple(event) { if (this.parentListEl) { return; } event.stopPropagation(); const { target, detail } = event; const { focusableItems, lastSelectedInfo } = this; const selectedItem = target; if (detail.selectMultiple && !!lastSelectedInfo) { const currentIndex = focusableItems.indexOf(selectedItem); const lastSelectedIndex = focusableItems.indexOf(lastSelectedInfo.selectedItem); const startIndex = Math.min(lastSelectedIndex, currentIndex); const endIndex = Math.max(lastSelectedIndex, currentIndex); focusableItems.slice(startIndex, endIndex + 1).forEach((item) => item.selected = lastSelectedInfo.selected); } else { this.lastSelectedInfo = { selectedItem, selected: selectedItem.selected }; } } handleCalciteInternalListItemChange(event) { if (this.parentListEl) { return; } event.stopPropagation(); this.updateListItems(); } handleCalciteInternalListItemGroupDefaultSlotChange(event) { event.stopPropagation(); } connectObserver() { this.mutationObserver?.observe(this.el, { childList: true, subtree: true }); } disconnectObserver() { this.mutationObserver?.disconnect(); } setUpSorting() { const { dragEnabled, defaultSlotEl } = this; if (!dragEnabled) { return; } if (defaultSlotEl) { updateListItemChildren(defaultSlotEl); } connectSortableComponent(this); } onGlobalDragStart() { this.disconnectObserver(); } onGlobalDragEnd() { this.connectObserver(); } onDragEnd(detail) { this.calciteListDragEnd.emit(detail); } onDragStart(detail) { detail.dragEl.sortHandleOpen = false; this.calciteListDragStart.emit(detail); } onDragSort(detail) { this.setParentList(); this.updateListItems(); this.calciteListOrderChange.emit(detail); } setParentList() { this.parentListEl = this.el.parentElement?.closest(listSelector); } handleDefaultSlotChange() { if (this.parentListEl) { this.calciteInternalListDefaultSlotChange.emit(); } } setListItemGroups() { this.listItemGroups = Array.from(this.el.querySelectorAll(listItemGroupSelector)); } handleFilterActionsStartSlotChange(event) { this.hasFilterActionsStart = slotChangeHasAssignedElement(event); } handleFilterActionsEndSlotChange(event) { this.hasFilterActionsEnd = slotChangeHasAssignedElement(event); } handleFilterNoResultsSlotChange(event) { this.hasFilterNoResults = slotChangeHasAssignedElement(event); } setActiveListItem() { const { focusableItems } = this; if (!focusableItems.some((item) => item.active)) { if (focusableItems[0]) { focusableItems[0].active = true; } } } async updateSelectedItems(emit = false) { await this.updateComplete; this.selectedItems = this.visibleItems.filter((item) => item.selected); if (emit) { this.calciteListChange.emit(); } } filterElements({ el, filteredItems, visibleParents }) { const filterHidden = !visibleParents.has(el) && !filteredItems.includes(el); el.filterHidden = filterHidden; const closestParent = el.parentElement.closest(parentSelector); if (!closestParent) { return; } if (!filterHidden) { visibleParents.add(closestParent); } this.filterElements({ el: closestParent, filteredItems, visibleParents }); } allParentListItemsExpanded(item) { const parentItem = item.parentElement?.closest(listItemSelector); if (!parentItem) { return true; } else if (!parentItem.expanded) { return false; } return this.allParentListItemsExpanded(parentItem); } borderItems() { const visibleItems = this.visibleItems.filter((item) => !item.filterHidden && this.allParentListItemsExpanded(item)); visibleItems.forEach((item) => item.bordered = item !== visibleItems[visibleItems.length - 1]); } updateFilteredItems() { const { visibleItems, filteredData, filterText, filterPredicate } = this; const lastDescendantItems = visibleItems?.filter((listItem) => visibleItems.every((li) => li === listItem || !listItem.contains(li))); const filteredItems = filterPredicate ? visibleItems.filter(filterPredicate) : !filterText ? visibleItems || [] : filteredData.map((item) => item.el); const visibleParents = /* @__PURE__ */ new WeakSet(); lastDescendantItems.forEach((listItem) => this.filterElements({ el: listItem, filteredItems, visibleParents })); this.filteredItems = filteredItems; if (this.willFilterEmit) { this.willFilterEmit = false; this.calciteListFilter.emit(); } } updateFilteredData() { const { filterEl } = this; if (!filterEl) { return; } if (filterEl.filteredItems) { this.filteredData = filterEl.filteredItems; } this.updateListItems(); } async filterAndUpdateData() { await this.filterEl?.filter(this.filterText); this.updateFilteredData(); } get effectiveFilterProps() { if (!this.filterProps) { return ["description", "label", "metadata", "heading"]; } return this.filterProps.filter((prop) => prop !== "el"); } performFilter() { const { filterEl, filterText, effectiveFilterProps } = this; if (!filterEl) { return; } filterEl.value = filterText; filterEl.filterProps = effectiveFilterProps; this.filterAndUpdateData(); } setDefaultSlotEl(el) { this.defaultSlotEl = el; } setFilterEl(el) { this.filterEl = el; this.performFilter(); } handleFilterChange(event) { event.stopPropagation(); const { value } = event.currentTarget; this.filterText = value; this.willFilterEmit = true; this.updateFilteredData(); } getItemData() { return this.listItems.map((item) => ({ label: item.label, description: item.description, metadata: item.metadata, heading: this.getGroupHeading(item), el: item })); } getGroupHeading(item) { const heading = this.listItemGroups.filter((group) => group.contains(item)).map((group) => group.heading); return heading; } updateGroupItems() { const { el, group, scale } = this; const rootNode = getRootNode(el); const lists = group ? Array.from(rootNode.querySelectorAll(`${listSelector}[group="${group}"]`)).filter((list) => !list.disabled && list.dragEnabled) : []; this.moveToItems = lists.map((element) => ({ element, label: element.label ?? element.id, id: guid() })); const groupItems = Array.from(this.el.querySelectorAll(listItemGroupSelector)); groupItems.forEach((item) => { item.scale = scale; }); } focusRow(focusEl) { const { focusableItems } = this; if (!focusEl) { return; } focusableItems.forEach((listItem) => listItem.active = listItem === focusEl); focusEl.setFocus(); } isNavigable(listItem) { const parentListItemEl = listItem.parentElement?.closest(listItemSelector); if (!parentListItemEl) { return true; } return parentListItemEl.expanded && this.isNavigable(parentListItemEl); } handleListKeydown(event) { if (event.defaultPrevented || !!this.parentListEl) { return; } const { key } = event; const navigableItems = this.focusableItems.filter((listItem) => this.isNavigable(listItem)); const currentIndex = navigableItems.findIndex((listItem) => listItem.active); if (key === "ArrowDown") { event.preventDefault(); const nextIndex = event.target === this.filterEl ? 0 : currentIndex + 1; if (navigableItems[nextIndex]) { this.focusRow(navigableItems[nextIndex]); } } else if (key === "ArrowUp") { event.preventDefault(); if (currentIndex === 0 && this.filterEnabled) { this.filterEl?.setFocus(); return; } const prevIndex = currentIndex - 1; if (navigableItems[prevIndex]) { this.focusRow(navigableItems[prevIndex]); } } else if (key === "Home") { event.preventDefault(); const homeItem = navigableItems[0]; if (homeItem) { this.focusRow(homeItem); } } else if (key === "End") { event.preventDefault(); const endItem = navigableItems[navigableItems.length - 1]; if (endItem) { this.focusRow(endItem); } } } handleInteractionModeWarning() { if (this.interactionMode === "static" && this.selectionMode !== "none" && this.selectionAppearance === "border") { console.warn(`selection-appearance="border" requires interaction-mode="interactive"`); } } handleMove(event) { const { moveTo } = event.detail; const dragEl = event.target; const fromEl = dragEl?.parentElement; const toEl = moveTo.element; const fromElItems = Array.from(fromEl.children).filter(isListItem); const oldIndex = fromElItems.indexOf(dragEl); const newIndex = 0; if (!fromEl) { return; } if (fromEl.canPull?.({ toEl, fromEl, dragEl, newIndex, oldIndex }) === false) { this.calciteListMoveHalt.emit({ toEl, fromEl, dragEl, oldIndex, newIndex }); return; } if (toEl.canPut?.({ toEl, fromEl, dragEl, newIndex, oldIndex }) === false) { toEl.putFailed({ toEl, fromEl, dragEl, oldIndex, newIndex }); return; } dragEl.sortHandleOpen = false; this.disconnectObserver(); toEl.prepend(dragEl); expandedAncestors(dragEl); this.updateListItems(); this.connectObserver(); this.calciteListOrderChange.emit({ dragEl, fromEl, toEl, newIndex, oldIndex }); } handleReorder(event) { const { reorder } = event.detail; const dragEl = event.target; const parentEl = dragEl?.parentElement; if (!parentEl) { return; } dragEl.sortHandleOpen = false; const sameParentItems = Array.from(parentEl.children).filter(isListItem); const lastIndex = sameParentItems.length - 1; const oldIndex = sameParentItems.indexOf(dragEl); let newIndex = oldIndex; switch (reorder) { case "top": newIndex = 0; break; case "bottom": newIndex = lastIndex; break; case "up": newIndex = oldIndex === 0 ? 0 : oldIndex - 1; break; case "down": newIndex = oldIndex === lastIndex ? lastIndex : oldIndex + 1; break; } this.disconnectObserver(); const referenceEl = reorder === "up" || reorder === "top" ? sameParentItems[newIndex] : sameParentItems[newIndex].nextSibling; parentEl.insertBefore(dragEl, referenceEl); this.updateListItems(); this.connectObserver(); this.calciteListOrderChange.emit({ dragEl, fromEl: parentEl, toEl: parentEl, newIndex, oldIndex }); } render() { const { loading, label, disabled, dataForFilter, filterEnabled, filterPlaceholder, filterText, filterLabel, hasFilterActionsStart, hasFilterActionsEnd, effectiveFilterProps } = this; return InteractiveContainer({ disabled: this.disabled, children: html`<div class=${safeClassMap(CSS.container)}>${this.dragEnabled ? html`<span aria-live=assertive class=${safeClassMap(CSS.assistiveText)}>${this.assistiveText}</span>` : null}${this.renderItemAriaLive()}${loading ? html`<calcite-scrim class=${safeClassMap(CSS.scrim)} .loading=${loading}></calcite-scrim>` : null}<div .ariaBusy=${loading} .ariaLabel=${label || ""} class=${safeClassMap(CSS.table)} @keydown=${this.handleListKeydown} role=treegrid>${filterEnabled || hasFilterActionsStart || hasFilterActionsEnd ? html`<div class=${safeClassMap(CSS.sticky)} role=rowgroup><div role=row><div role=columnheader><calcite-stack class=${safeClassMap(CSS.stack)}><slot name=${SLOTS.filterActionsStart} @slotchange=${this.handleFilterActionsStartSlotChange} slot=${SLOTS$1.actionsStart}></slot><calcite-filter .ariaLabel=${filterPlaceholder} .disabled=${disabled} .filterProps=${effectiveFilterProps} .items=${dataForFilter} .label=${filterLabel} @calciteFilterChange=${this.handleFilterChange} .placeholder=${filterPlaceholder} .scale=${this.scale} .value=${filterText} ${ref(this.setFilterEl)}></calcite-filter><slot name=${SLOTS.filterActionsEnd} @slotchange=${this.handleFilterActionsEndSlotChange} slot=${SLOTS$1.actionsEnd}></slot></calcite-stack></div></div></div>` : null}<div class=${safeClassMap(CSS.tableContainer)} role=rowgroup><slot @slotchange=${this.handleDefaultSlotChange} ${ref(this.setDefaultSlotEl)}></slot></div></div><div aria-live=polite data-test-id=no-results-container .hidden=${!this.showNoResultsContainer}><slot name=${SLOTS.filterNoResults} @slotchange=${this.handleFilterNoResultsSlotChange}></slot></div></div>` }); } renderItemAriaLive() { const { messages, filteredItems, parentListEl, messages: { _lang: effectiveLocale }, numberingSystem } = this; numberStringFormatter.numberFormatOptions = { locale: effectiveLocale, numberingSystem }; return !parentListEl ? html`<div aria-live=polite class=${safeClassMap(CSS.assistiveText)}>${this.hasActiveFilter ? keyed("aria-filter-enabled", html`<div>${messages.filterEnabled}</div>`) : null}${keyed("aria-item-count", html`<div>${messages.total.replace("{count}", numberStringFormatter.localize(filteredItems.length.toString()))}</div>`)}${filteredItems.length ? keyed("aria-item-list", html`<ol>${filteredItems.map((item) => html`<li>${item.label}</li>`)}</ol>`) : null}</div>` : null; } } customElement("calcite-list", List); export { List };