UNPKG

@trimble-oss/moduswebcomponents

Version:

Modus Web Components is a modern, accessible UI library built with Stencil JS that provides reusable web components following Trimble's Modus design system. This updated version focuses on improved flexibility, enhanced theming options, comprehensive cust

331 lines (330 loc) 12.2 kB
import { h, Host, } from "@stencil/core"; import { convertPropsToClasses } from "./modus-wc-menu.tailwind"; import { handleShadowDOMStyles } from "../base-component"; import { inheritAriaAttributes } from "../utils"; /** * A customizable menu component used to display a list of li elements vertically or horizontally. * * The component supports a `<slot>` for injecting custom li elements inside the ul element. */ export class ModusWcMenu { constructor() { this.inheritedAttributes = {}; this.selectedItems = []; /** Custom CSS class to apply to the ul element. */ this.customClass = ''; /** The orientation of the menu. */ this.orientation = 'vertical'; /** The selection mode of the menu. */ this.selectionMode = 'single'; /** The size of the menu. */ this.size = 'md'; this.getMenuRole = () => this.orientation === 'horizontal' ? 'menubar' : 'menu'; } onSelectionModeChange() { this.selectedItems = []; } componentWillLoad() { handleShadowDOMStyles(this.el); if (!this.el.ariaLabel) { this.el.ariaLabel = 'Menu'; } this.inheritedAttributes = inheritAriaAttributes(this.el); } getClasses() { // For submenus, only add the dropdown class if (this.isSubMenu) { const classList = ['modus-wc-menu-dropdown']; if (this.customClass) classList.push(this.customClass); return classList.join(' '); } // For regular menus, add all the standard classes const classList = ['modus-wc-menu modus-wc-w-full']; const propClasses = convertPropsToClasses({ bordered: this.bordered, orientation: this.orientation, size: this.size, }); // The order CSS classes are added matters to CSS specificity if (propClasses) classList.push(propClasses); if (this.customClass) classList.push(this.customClass); return classList.join(' '); } getMenuItems() { return Array.from(this.el.querySelectorAll('modus-wc-menu-item')).filter((item) => item.closest('modus-wc-menu') === this.el); } handleItemSelect(e) { if (this.selectionMode !== 'multiple') return; const item = e.target; const isCurrentlySelected = this.selectedItems.includes(item); if (e.detail.selected && !isCurrentlySelected) { this.selectedItems = [...this.selectedItems, item]; } else if (!e.detail.selected && isCurrentlySelected) { this.selectedItems = this.selectedItems.filter((i) => i !== item); } this.menuSelectionChange.emit({ selectedItems: this.selectedItems }); } handleKeyDown(e) { if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') return; e.preventDefault(); const items = this.getMenuItems(); const focusableItems = items.filter((item) => !item.disabled); if (focusableItems.length === 0) return; const activeEl = document.activeElement; const currentMenuItem = activeEl === null || activeEl === void 0 ? void 0 : activeEl.closest('modus-wc-menu-item'); const currentIndex = focusableItems.indexOf(currentMenuItem); let nextIndex; if (e.key === 'ArrowDown') { nextIndex = currentIndex < focusableItems.length - 1 ? currentIndex + 1 : 0; } else { nextIndex = currentIndex > 0 ? currentIndex - 1 : focusableItems.length - 1; } const nextLi = focusableItems[nextIndex].querySelector('li'); if (nextLi) { nextLi.focus(); } } handleFocusout(e) { if (!this.el.contains(e.relatedTarget)) { this.menuFocusout.emit(e); if (this.isSubMenu) { e.stopPropagation(); } } } render() { return (h(Host, { key: '19854fb1e7960407db704bb954ff66bb355ab042', class: this.isSubMenu ? 'modus-wc-menu-submenu' : undefined }, h("ul", Object.assign({ key: '0752b609c2ed4a897c8a6e8ecc73718cff4c8d09', "aria-orientation": this.orientation, class: this.getClasses(), role: this.getMenuRole() }, this.inheritedAttributes), h("slot", { key: '4957e81fa8566c990a5f06464d2d26dee3af5ba7' })))); } static get is() { return "modus-wc-menu"; } static get originalStyleUrls() { return { "$": ["modus-wc-menu.scss"] }; } static get styleUrls() { return { "$": ["modus-wc-menu.css"] }; } static get properties() { return { "bordered": { "type": "boolean", "attribute": "bordered", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean | undefined", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Indicates that the menu should have a border." }, "getter": false, "setter": false, "reflect": false }, "customClass": { "type": "string", "attribute": "custom-class", "mutable": false, "complexType": { "original": "string", "resolved": "string | undefined", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Custom CSS class to apply to the ul element." }, "getter": false, "setter": false, "reflect": false, "defaultValue": "''" }, "orientation": { "type": "string", "attribute": "orientation", "mutable": false, "complexType": { "original": "Orientation", "resolved": "\"horizontal\" | \"vertical\" | undefined", "references": { "Orientation": { "location": "import", "path": "../types", "id": "src/components/types.ts::Orientation" } } }, "required": false, "optional": true, "docs": { "tags": [], "text": "The orientation of the menu." }, "getter": false, "setter": false, "reflect": false, "defaultValue": "'vertical'" }, "selectionMode": { "type": "string", "attribute": "selection-mode", "mutable": false, "complexType": { "original": "SelectionMode", "resolved": "\"multiple\" | \"single\" | undefined", "references": { "SelectionMode": { "location": "import", "path": "../types", "id": "src/components/types.ts::SelectionMode" } } }, "required": false, "optional": true, "docs": { "tags": [], "text": "The selection mode of the menu." }, "getter": false, "setter": false, "reflect": true, "defaultValue": "'single'" }, "size": { "type": "string", "attribute": "size", "mutable": false, "complexType": { "original": "ModusSize", "resolved": "\"lg\" | \"md\" | \"sm\" | undefined", "references": { "ModusSize": { "location": "import", "path": "../types", "id": "src/components/types.ts::ModusSize" } } }, "required": false, "optional": true, "docs": { "tags": [], "text": "The size of the menu." }, "getter": false, "setter": false, "reflect": false, "defaultValue": "'md'" }, "isSubMenu": { "type": "boolean", "attribute": "is-sub-menu", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean | undefined", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Indicates that this menu is a submenu (dropdown)." }, "getter": false, "setter": false, "reflect": false } }; } static get events() { return [{ "method": "menuFocusout", "name": "menuFocusout", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "Event emitted when the menu loses focus." }, "complexType": { "original": "FocusEvent", "resolved": "FocusEvent", "references": { "FocusEvent": { "location": "global", "id": "global::FocusEvent" } } } }, { "method": "menuSelectionChange", "name": "menuSelectionChange", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "Event emitted when the selection changes in multiple selection mode. Emits the array of currently selected menu item elements." }, "complexType": { "original": "{\n selectedItems: HTMLElement[];\n }", "resolved": "{ selectedItems: HTMLElement[]; }", "references": { "HTMLElement": { "location": "global", "id": "global::HTMLElement" } } } }]; } static get elementRef() { return "el"; } static get watchers() { return [{ "propName": "selectionMode", "methodName": "onSelectionModeChange" }]; } static get listeners() { return [{ "name": "itemSelect", "method": "handleItemSelect", "target": undefined, "capture": false, "passive": false }, { "name": "keydown", "method": "handleKeyDown", "target": undefined, "capture": false, "passive": false }, { "name": "focusout", "method": "handleFocusout", "target": undefined, "capture": false, "passive": false }]; } }