@esri/calcite-components
Version:
Web Components for Esri's Calcite Design System.
282 lines (281 loc) • 10.8 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, 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} =${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 =${this.handlePopoverClose} =${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} =${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
};