UNPKG

@esri/calcite-components

Version:

Web Components for Esri's Calcite Design System.

282 lines (281 loc) • 10.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 { c as customElement } from "../../chunks/runtime.js"; import { ref } from "lit-html/directives/ref.js"; import { html, nothing } from "lit"; import { LitElement, createEvent, safeClassMap } from "@arcgis/lumina"; import { g as getRoundRobinIndex } from "../../chunks/array.js"; import { b as focusElement, t as toAriaBoolean } from "../../chunks/dom.js"; import { g as guid } from "../../chunks/guid.js"; import { i as isActivationKey } from "../../chunks/key.js"; import { c as componentFocusable } from "../../chunks/component.js"; import { a as activeAttr, S as SLOTS, I as ICONS, C as CSS } from "../../chunks/resources2.js"; import { css } from "@lit/reactive-element/css-tag.js"; const styles = css`:host{box-sizing:border-box;display:flex;flex-direction:column;font-size:var(--calcite-font-size-1)}::slotted(calcite-action-group:not(:last-of-type)){border-block-end-width:var(--calcite-border-width-sm)}.default-trigger{position:relative;block-size:100%;flex:0 1 auto;align-self:stretch}slot[name=trigger]::slotted(calcite-action),calcite-action::slotted([slot=trigger]){position:relative;block-size:100%;flex:0 1 auto;align-self:stretch}.menu{display:flex;max-block-size:45vh;flex-direction:column;flex-wrap:nowrap;overflow-y:auto;overflow-x:hidden;outline:2px solid transparent;outline-offset:2px;gap:var(--calcite-action-menu-items-space, 0)}:host([hidden]){display:none}[hidden]{display:none}`; const SUPPORTED_MENU_NAV_KEYS = ["ArrowUp", "ArrowDown", "End", "Home"]; class ActionMenu extends LitElement { constructor() { super(...arguments); this.guid = `calcite-action-menu-${guid()}`; this.actionElements = []; this.menuButtonClick = () => { this.toggleOpen(); }; this.menuButtonId = `${this.guid}-menu-button`; this.menuButtonKeyDown = (event) => { const { key } = event; const { actionElements, activeMenuItemIndex, open } = this; if (!actionElements.length) { return; } if (isActivationKey(key)) { event.preventDefault(); if (!open) { this.toggleOpen(); return; } const action = actionElements[activeMenuItemIndex]; if (action) { action.click(); } else { this.toggleOpen(false); } } if (key === "Tab") { this.open = false; return; } if (key === "Escape") { this.toggleOpen(false); event.preventDefault(); return; } this.handleActionNavigation(event, key, actionElements); }; this.menuId = `${this.guid}-menu`; this._open = false; this.updateAction = (action, index) => { const { guid: guid2, activeMenuItemIndex } = this; const id = `${guid2}-action-${index}`; action.tabIndex = -1; action.setAttribute("role", "menuitem"); if (!action.id) { action.id = id; } action.toggleAttribute(activeAttr, index === activeMenuItemIndex); }; this.activeMenuItemIndex = -1; this.appearance = "solid"; this.expanded = false; this.overlayPositioning = "absolute"; this.placement = "auto"; this.scale = "m"; this.calciteActionMenuOpen = createEvent({ cancelable: false }); } static { this.properties = { activeMenuItemIndex: [16, {}, { state: true }], menuButtonEl: [16, {}, { state: true }], appearance: [3, {}, { reflect: true }], expanded: [7, {}, { reflect: true, type: Boolean }], flipPlacements: [0, {}, { attribute: false }], label: 1, open: [7, {}, { reflect: true, type: Boolean }], overlayPositioning: [3, {}, { reflect: true }], placement: [3, {}, { reflect: true }], scale: [3, {}, { reflect: true }] }; } static { this.styles = styles; } get open() { return this._open; } set open(open) { const oldOpen = this._open; if (open !== oldOpen) { this._open = open; this.openHandler(open); } } async setFocus() { await componentFocusable(this); return focusElement(this.menuButtonEl); } connectedCallback() { super.connectedCallback(); this.connectMenuButtonEl(); } willUpdate(changes) { if (changes.has("expanded") && (this.hasUpdated || this.expanded !== false)) { this.expandedHandler(); } if (changes.has("activeMenuItemIndex") && (this.hasUpdated || this.activeMenuItemIndex !== -1)) { this.updateActions(this.actionElements); } } disconnectedCallback() { super.disconnectedCallback(); this.disconnectMenuButtonEl(); } expandedHandler() { this.open = false; this.setTooltipReferenceElement(); } openHandler(open) { if (this.menuButtonEl) { this.menuButtonEl.active = open; } if (this.popoverEl) { this.popoverEl.open = open; } this.activeMenuItemIndex = this.open ? 0 : -1; this.calciteActionMenuOpen.emit(); this.setTooltipReferenceElement(); } connectMenuButtonEl() { const { menuButtonId, menuId, open, label } = this; const menuButtonEl = this.slottedMenuButtonEl || this.defaultMenuButtonEl; if (this.menuButtonEl === menuButtonEl) { return; } this.disconnectMenuButtonEl(); this.menuButtonEl = menuButtonEl; this.setTooltipReferenceElement(); if (!menuButtonEl) { return; } menuButtonEl.active = open; menuButtonEl.setAttribute("aria-controls", menuId); menuButtonEl.setAttribute("aria-expanded", toAriaBoolean(open)); menuButtonEl.setAttribute("aria-haspopup", "true"); if (!menuButtonEl.id) { menuButtonEl.id = menuButtonId; } if (!menuButtonEl.label) { menuButtonEl.label = label; } if (!menuButtonEl.text) { menuButtonEl.text = label; } menuButtonEl.addEventListener("click", this.menuButtonClick); menuButtonEl.addEventListener("keydown", this.menuButtonKeyDown); } disconnectMenuButtonEl() { const { menuButtonEl } = this; if (!menuButtonEl) { return; } menuButtonEl.removeEventListener("click", this.menuButtonClick); menuButtonEl.removeEventListener("keydown", this.menuButtonKeyDown); this.menuButtonEl = null; } setMenuButtonEl(event) { const actions = event.target.assignedElements({ flatten: true }).filter((el) => el?.matches("calcite-action")); this.slottedMenuButtonEl = actions[0]; this.connectMenuButtonEl(); } setDefaultMenuButtonEl(el) { this.defaultMenuButtonEl = el; if (el) { this.connectMenuButtonEl(); } } setPopoverEl(el) { if (!el) { return; } this.popoverEl = el; el.open = this.open; } handleCalciteActionClick() { this.open = false; this.setFocus(); } updateTooltip(event) { const tooltips = event.target.assignedElements({ flatten: true }).filter((el) => el?.matches("calcite-tooltip")); this.tooltipEl = tooltips[0]; this.setTooltipReferenceElement(); } setTooltipReferenceElement() { const { tooltipEl, expanded, menuButtonEl, open } = this; if (tooltipEl) { tooltipEl.referenceElement = !expanded && !open ? menuButtonEl : null; } } updateActions(actions) { actions?.forEach(this.updateAction); } handleDefaultSlotChange(event) { const actions = event.target.assignedElements({ flatten: true }).reduce((previousValue, currentValue) => { if (currentValue?.matches("calcite-action")) { previousValue.push(currentValue); return previousValue; } if (currentValue?.matches("calcite-action-group")) { return previousValue.concat(Array.from(currentValue.querySelectorAll("calcite-action"))); } return previousValue; }, []); this.actionElements = actions.filter((action) => !action.disabled && !action.hidden); } isValidKey(key, supportedKeys) { return !!supportedKeys.find((k) => k === key); } handleActionNavigation(event, key, actions) { if (!this.isValidKey(key, SUPPORTED_MENU_NAV_KEYS)) { return; } event.preventDefault(); if (!this.open) { this.toggleOpen(); if (key === "Home" || key === "ArrowDown") { this.activeMenuItemIndex = 0; } if (key === "End" || key === "ArrowUp") { this.activeMenuItemIndex = actions.length - 1; } return; } if (key === "Home") { this.activeMenuItemIndex = 0; } if (key === "End") { this.activeMenuItemIndex = actions.length - 1; } const currentIndex = this.activeMenuItemIndex; if (key === "ArrowUp") { this.activeMenuItemIndex = getRoundRobinIndex(Math.max(currentIndex - 1, -1), actions.length); } if (key === "ArrowDown") { this.activeMenuItemIndex = getRoundRobinIndex(currentIndex + 1, actions.length); } } toggleOpen(value = !this.open) { this.open = value; } handlePopoverOpen(event) { event.stopPropagation(); this.open = true; this.setFocus(); } handlePopoverClose(event) { event.stopPropagation(); this.open = false; } renderMenuButton() { const { appearance, label, scale, expanded } = this; const menuButtonSlot = html`<slot name=${SLOTS.trigger} @slotchange=${this.setMenuButtonEl}><calcite-action .appearance=${appearance} class=${safeClassMap(CSS.defaultTrigger)} .icon=${ICONS.menu} .scale=${scale} .text=${label} .textEnabled=${expanded} ${ref(this.setDefaultMenuButtonEl)}></calcite-action></slot>`; return menuButtonSlot; } renderMenuItems() { const { actionElements, activeMenuItemIndex, menuId, menuButtonEl, label, placement, overlayPositioning, flipPlacements } = this; const activeAction = actionElements[activeMenuItemIndex]; const activeDescendantId = activeAction?.id || null; return html`<calcite-popover auto-close .flipPlacements=${flipPlacements} focus-trap-disabled .label=${label} offset-distance=0 @calcitePopoverClose=${this.handlePopoverClose} @calcitePopoverOpen=${this.handlePopoverOpen} .overlayPositioning=${overlayPositioning} .placement=${placement} pointer-disabled .referenceElement=${menuButtonEl} trigger-disabled ${ref(this.setPopoverEl)}><div aria-activedescendant=${activeDescendantId ?? nothing} aria-labelledby=${menuButtonEl?.id ?? nothing} class=${safeClassMap(CSS.menu)} id=${menuId ?? nothing} @click=${this.handleCalciteActionClick} role=menu tabindex=-1><slot @slotchange=${this.handleDefaultSlotChange}></slot></div></calcite-popover>`; } render() { return html`${this.renderMenuButton()}${this.renderMenuItems()}<slot name=${SLOTS.tooltip} @slotchange=${this.updateTooltip}></slot>`; } } customElement("calcite-action-menu", ActionMenu); export { ActionMenu };