@esri/calcite-components
Version:
Web Components for Esri's Calcite Design System.
301 lines (300 loc) • 12.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, html, nothing } from "lit";
import { LitElement, createEvent, safeClassMap } from "@arcgis/lumina";
import { g as getRoundRobinIndex } from "../../chunks/array.js";
import { t as toAriaBoolean } from "../../chunks/dom.js";
import { g as guid } from "../../chunks/guid.js";
import { i as isActivationKey } from "../../chunks/key.js";
import { i as isAction } from "../../chunks/resources.js";
import { u as useSetFocus } from "../../chunks/useSetFocus.js";
import { I as IDS, S as SLOTS, a as ICONS, C as CSS } from "../../chunks/resources2.js";
const styles = css`:host([scale=s]){--calcite-internal-action-menu-gap: var(--calcite-action-menu-items-space, var(--calcite-spacing-xxs));--calcite-internal-action-menu-padding: var(--calcite-spacing-xxs)}:host([scale=m]){--calcite-internal-action-menu-gap: var(--calcite-action-menu-items-space, var(--calcite-spacing-sm));--calcite-internal-action-menu-padding: var(--calcite-spacing-sm)}:host([scale=l]){--calcite-internal-action-menu-gap: var(--calcite-action-menu-items-space, var(--calcite-spacing-sm-plus));--calcite-internal-action-menu-padding: var(--calcite-spacing-sm-plus)}:host{box-sizing:border-box;display:flex;flex-direction:column;font-size:var(--calcite-font-size-relative-lg)}::slotted(calcite-action-group:not(:last-of-type)){border-block-end-width:var(--calcite-border-width-sm);padding-block-end:var(--calcite-internal-action-menu-padding)}.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-internal-action-menu-gap);padding:var(--calcite-internal-action-menu-padding)}: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 = guid();
this.actionElements = [];
this.menuButtonClick = () => {
this.toggleOpen();
};
this.menuButtonId = IDS.button(this.guid);
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 = IDS.menu(this.guid);
this._open = false;
this.updateAction = (action, index) => {
const { guid: guid2, activeMenuItemIndex } = this;
const id = IDS.action(guid2, index);
action.tabIndex = -1;
action.setAttribute("role", "menuitem");
if (!action.id) {
action.id = id;
}
action.activeDescendant = index === activeMenuItemIndex;
};
this.focusSetter = useSetFocus()(this);
this.mouseDownHandler = (event) => {
if (!event.composedPath().some(isAction)) {
return;
}
this.activeMenuItemIndex = this.actionElements?.findIndex((action) => action === event.target);
};
this.activeMenuItemIndex = -1;
this.appearance = "solid";
this.expanded = false;
this.overlayPositioning = "absolute";
this.placement = "auto";
this.topLayerDisabled = false;
this.scale = "m";
this.calciteActionMenuCollapse = createEvent({ cancelable: false });
this.calciteActionMenuExpand = createEvent({ cancelable: false });
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 }], topLayerDisabled: [7, {}, { reflect: true, type: Boolean }], 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(options) {
return this.focusSetter(() => this.menuButtonEl, options);
}
connectedCallback() {
super.connectedCallback();
this.connectMenuButtonEl();
this.listen("mousedown", this.mouseDownHandler);
}
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);
}
if (changes.has("expanded") && this.hasUpdated) {
if (this.expanded) {
this.calciteActionMenuExpand.emit();
} else {
this.calciteActionMenuCollapse.emit();
}
}
}
disconnectedCallback() {
super.disconnectedCallback();
this.disconnectMenuButtonEl();
}
expandedHandler() {
this.open = false;
this.setTooltipReferenceElement();
}
openHandler(open) {
if (this.menuButtonEl) {
this.menuButtonEl.active = open;
this.menuButtonEl.aria = {
expanded: 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;
this.connectMenuButtonEl();
}
setPopoverEl(el) {
if (!el) {
return;
}
this.popoverEl = el;
el.open = this.open;
}
handleCalciteActionClick(event) {
if (this.actionElements?.some((action) => event.composedPath().includes(action))) {
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);
}
async 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;
}, []);
await this.componentOnReady();
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} =${this.setMenuButtonEl}><calcite-action .appearance=${appearance} .aria=${{ expanded }} 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 =${this.handlePopoverClose} =${this.handlePopoverOpen} .overlayPositioning=${overlayPositioning} .placement=${placement} pointer-disabled .referenceElement=${menuButtonEl} .scale=${this.scale} .topLayerDisabled=${this.topLayerDisabled} trigger-disabled ${ref(this.setPopoverEl)}><div aria-activedescendant=${activeDescendantId ?? nothing} aria-labelledby=${menuButtonEl?.id ?? nothing} class=${safeClassMap(CSS.menu)} id=${menuId ?? nothing} =${this.handleCalciteActionClick} role=menu tabindex=-1><slot =${this.handleDefaultSlotChange}></slot></div></calcite-popover>`;
}
render() {
return html`${this.renderMenuButton()}${this.renderMenuItems()}<slot name=${SLOTS.tooltip} =${this.updateTooltip}></slot>`;
}
}
customElement("calcite-action-menu", ActionMenu);
export {
ActionMenu
};