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

576 lines (575 loc) 22.6 kB
import { h, Host, } from "@stencil/core"; import { convertPropsToClasses } from "./modus-wc-tree-item.tailwind"; import { handleShadowDOMStyles } from "../base-component"; import { inheritAriaAttributes } from "../utils"; /** * A customizable tree item component used to display the item portion of a tree menu. * * This component supports `start` and `end` slots for custom content at the beginning and end of the item. */ export class ModusWcTreeItem { constructor() { this.inheritedAttributes = {}; /** Custom CSS class to apply to the li element. */ this.customClass = ''; /** The text rendered in the tree item. */ this.label = ''; /** The size of the tree item. */ this.size = 'md'; /** The position of the tooltip relative to the tree item. */ this.tooltipPosition = 'auto'; /** The unique identifying value of the tree item. */ this.value = ''; /** Internal state to track if submenu is expanded */ this.isExpanded = false; this.handleItemClick = (event) => { var _a; if (this.isInteractiveSlotClick(event, 'end')) return; if (this.isInteractiveSlotClick(event, 'start')) return; this.handleItemSelect(); // Blurring the li prevents the focus ring from sticking after mouse click. (_a = this.el.querySelector('li')) === null || _a === void 0 ? void 0 : _a.blur(); }; this.handleItemSelect = () => { if (this.hasSubmenu) { if (this.blockExpand) { this.itemSelect.emit({ value: this.value }); return; } const sideNav = this.el.closest('modus-wc-side-navigation'); if (sideNav && !sideNav.expanded) { this.itemSelect.emit({ value: this.value }); return; } const submenu = this.el.querySelector('.modus-wc-menu-dropdown'); const liElement = this.el.querySelector('li'); if (submenu && liElement) { submenu.classList.toggle('modus-wc-menu-dropdown-show'); this.isExpanded = submenu.classList.contains('modus-wc-menu-dropdown-show'); if (this.isExpanded) { liElement.classList.add('modus-wc-menu-item-expanded'); liElement.classList.add('modus-wc-menu-dropdown-show'); } else { liElement.classList.remove('modus-wc-menu-item-expanded'); liElement.classList.remove('modus-wc-menu-dropdown-show'); } } } else if (this.resolveSelectionMode() === 'multiple' || this.checkbox) { this.selected = !this.selected; } else { if (this.resolveSelectionMode() === 'single') { this.deselectSiblings(); } this.selected = true; } this.itemSelect.emit({ value: this.value, selected: this.selected }); }; } componentWillLoad() { handleShadowDOMStyles(this.el); this.inheritedAttributes = inheritAriaAttributes(this.el); this._selectionMode = this.resolveSelectionMode(); } componentDidLoad() { if (typeof MutationObserver === 'undefined') return; const parentTreeMenu = this.el.closest('modus-wc-tree-menu'); if (parentTreeMenu) { this.parentTreeMenuObserver = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.attributeName === 'selection-mode') { this.handleSelectionModeChange(); break; } } }); this.parentTreeMenuObserver.observe(parentTreeMenu, { attributes: true }); } } disconnectedCallback() { var _a; (_a = this.parentTreeMenuObserver) === null || _a === void 0 ? void 0 : _a.disconnect(); } handleSelectionModeChange() { this._selectionMode = this.resolveSelectionMode(); this.selected = false; } handleKeyDown(e) { if (this.disabled) return; if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.handleItemSelect(); } } /** * Public method to collapse the submenu if it's expanded */ async collapseSubmenu() { if (this.hasSubmenu && this.isExpanded) { const submenu = this.el.querySelector('.modus-wc-menu-dropdown'); const liElement = this.el.querySelector('li'); if (submenu && liElement) { submenu.classList.remove('modus-wc-menu-dropdown-show'); liElement.classList.remove('modus-wc-menu-item-expanded'); liElement.classList.remove('modus-wc-menu-dropdown-show'); this.isExpanded = false; } } return Promise.resolve(); } getRootTreeMenu() { var _a, _b; let treeMenu = this.el.closest('modus-wc-tree-menu'); while (treeMenu) { const parent = (_b = (_a = treeMenu.parentElement) === null || _a === void 0 ? void 0 : _a.closest('modus-wc-tree-menu')) !== null && _b !== void 0 ? _b : null; if (!parent) break; treeMenu = parent; } return treeMenu; } deselectSiblings() { const rootTreeMenu = this.getRootTreeMenu(); if (!rootTreeMenu) return; const allItems = rootTreeMenu.querySelectorAll('modus-wc-tree-item'); allItems.forEach((item) => { if (item !== this.el) { item.selected = false; } }); } resolveSelectionMode() { var _a; return (_a = this.el.closest('modus-wc-tree-menu')) === null || _a === void 0 ? void 0 : _a.selectionMode; } getClasses() { const classList = ['modus-wc-menu-item']; const isActive = !this.hasSubmenu && !!this.selected; const propClasses = convertPropsToClasses({ active: isActive, bordered: this.bordered, disabled: this.disabled, focused: this.focused, size: this.size, }); if (propClasses) classList.push(propClasses); if (this.customClass) classList.push(this.customClass); return classList.join(' '); } getInteractiveClasses() { const classList = ['modus-wc-menu-item-interactive']; if (this.hasSubmenu) classList.push('modus-wc-menu-dropdown-toggle'); return classList.join(' '); } getRole(mode) { if (mode === 'multiple') return 'menuitemcheckbox'; if (mode === 'single') return 'menuitemradio'; return 'menuitem'; } hasCheckbox(mode) { return this.checkbox || mode === 'multiple'; } getAriaChecked(mode) { if (!this.hasCheckbox(mode)) return undefined; return this.selected ? 'true' : 'false'; } getAriaSelected(mode) { if (this.hasCheckbox(mode)) return undefined; if (mode === 'single') return this.selected ? 'true' : 'false'; return undefined; } isSlotClick(event, slotName) { const slotted = Array.from(this.el.querySelectorAll(`[slot="${slotName}"]`)); if (slotted.length === 0) return false; // Use the innermost event target so this works regardless of how // Stencil's slot relocation affects composedPath ancestry order. const innerTarget = event.composedPath()[0]; if (!innerTarget) return false; return slotted.some((el) => el === innerTarget || el.contains(innerTarget)); } isInteractiveSlotClick(event, slotName) { if (!this.isSlotClick(event, slotName)) return false; if (slotName === 'end') return true; const interactiveSelector = 'button, a, input, select, textarea, modus-wc-button, modus-wc-checkbox'; return event.composedPath().some((node) => { if (!(node instanceof HTMLElement)) return false; return (node.matches(interactiveSelector) || !!node.closest(interactiveSelector)); }); } render() { const mode = this._selectionMode; return (h(Host, { key: 'e449da3b3e622c0563053ea430d154504af7bc94' }, h("li", Object.assign({ key: '69a6c98f67cc075172f75edda01a6a38a3073f0e', "aria-checked": this.getAriaChecked(mode), "aria-disabled": this.disabled, "aria-expanded": this.hasSubmenu ? String(this.isExpanded) : undefined, "aria-selected": this.getAriaSelected(mode), class: this.getClasses(), role: this.getRole(mode), tabIndex: this.disabled ? -1 : 0 }, this.inheritedAttributes), h("div", { key: 'bb1ad08a4c96902d11d4a87f5929b811e72e0781', class: this.getInteractiveClasses(), onClick: this.handleItemClick, role: "presentation", tabIndex: -1 }, h("div", { key: '64a245f9c429c17f0074705b7dab106ca864bf4e', class: "modus-wc-menu-item-content" }, (this.checkbox || mode === 'multiple') && (h("modus-wc-checkbox", { key: '2fd4bce5ada577a49e429c4916b1a8304fec475f', "aria-label": "Checkbox", disabled: this.disabled, size: this.size, value: !!this.selected })), h("slot", { key: '3cd2f808b5c605f5da8b49332b550f654d03ef24', name: "start" }), h("div", { key: '43a01c089eaa192d263d31ed4878f5be7340a323', class: "modus-wc-menu-item-labels" }, this.tooltipContent ? (h("modus-wc-tooltip", { content: this.tooltipContent, position: this.tooltipPosition, customClass: "modus-wc-menu-item-tooltip" }, h("div", null, this.label))) : (h("div", null, this.label)), this.subLabel && (h("div", { key: 'a8e29414ded68dd80314bcfa3714325bc570e2b4', class: "modus-wc-menu-item-sublabel" }, this.subLabel))), h("slot", { key: '6e3d2f5ff446499d2835af24bf8e5f457eec5dc3', name: "end" }))), h("slot", { key: '9f94e45e38a165aab731b18374c720d176a0a1de' })))); } static get is() { return "modus-wc-tree-item"; } static get originalStyleUrls() { return { "$": ["modus-wc-tree-item.scss"] }; } static get styleUrls() { return { "$": ["modus-wc-tree-item.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": "" }, "getter": false, "setter": false, "reflect": false }, "checkbox": { "type": "boolean", "attribute": "checkbox", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean | undefined", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "If true, renders a checkbox at the start of the tree item." }, "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 li element." }, "getter": false, "setter": false, "reflect": false, "defaultValue": "''" }, "disabled": { "type": "boolean", "attribute": "disabled", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean | undefined", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "The disabled state of the tree item." }, "getter": false, "setter": false, "reflect": false }, "label": { "type": "string", "attribute": "label", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "The text rendered in the tree item." }, "getter": false, "setter": false, "reflect": false, "defaultValue": "''" }, "selected": { "type": "boolean", "attribute": "selected", "mutable": true, "complexType": { "original": "boolean", "resolved": "boolean | undefined", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "The selected state of the tree item." }, "getter": false, "setter": false, "reflect": false }, "focused": { "type": "boolean", "attribute": "focused", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean | undefined", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "The focused state of the tree item." }, "getter": false, "setter": false, "reflect": false }, "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 tree item." }, "getter": false, "setter": false, "reflect": false, "defaultValue": "'md'" }, "subLabel": { "type": "string", "attribute": "sub-label", "mutable": false, "complexType": { "original": "string", "resolved": "string | undefined", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "The text rendered beneath the label." }, "getter": false, "setter": false, "reflect": false }, "tooltipContent": { "type": "string", "attribute": "tooltip-content", "mutable": false, "complexType": { "original": "string", "resolved": "string | undefined", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "The tooltip text to display when hovering over the tree item." }, "getter": false, "setter": false, "reflect": false }, "tooltipPosition": { "type": "string", "attribute": "tooltip-position", "mutable": false, "complexType": { "original": "'auto' | 'top' | 'right' | 'bottom' | 'left'", "resolved": "\"auto\" | \"bottom\" | \"left\" | \"right\" | \"top\" | undefined", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "The position of the tooltip relative to the tree item." }, "getter": false, "setter": false, "reflect": false, "defaultValue": "'auto'" }, "value": { "type": "string", "attribute": "value", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "The unique identifying value of the tree item." }, "getter": false, "setter": false, "reflect": false, "defaultValue": "''" }, "hasSubmenu": { "type": "boolean", "attribute": "has-submenu", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean | undefined", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Whether this tree item has a collapsible submenu. When true, the item will show a caret and handle toggle behavior." }, "getter": false, "setter": false, "reflect": false }, "blockExpand": { "type": "boolean", "attribute": "block-expand", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean | undefined", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "When true, prevents the built-in inline submenu toggle on click. The item will only emit `itemSelect` so the consumer can handle the expansion externally (e.g. show a flyout panel). Only has an effect when `hasSubmenu` is also true." }, "getter": false, "setter": false, "reflect": false } }; } static get states() { return { "isExpanded": {}, "_selectionMode": {} }; } static get events() { return [{ "method": "itemSelect", "name": "itemSelect", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "Event emitted when a tree item is selected." }, "complexType": { "original": "{\n value: string;\n selected?: boolean;\n }", "resolved": "{ value: string; selected?: boolean | undefined; }", "references": {} } }]; } static get methods() { return { "collapseSubmenu": { "complexType": { "signature": "() => Promise<void>", "parameters": [], "references": { "Promise": { "location": "global", "id": "global::Promise" }, "HTMLElement": { "location": "global", "id": "global::HTMLElement" } }, "return": "Promise<void>" }, "docs": { "text": "Public method to collapse the submenu if it's expanded", "tags": [] } } }; } static get elementRef() { return "el"; } static get listeners() { return [{ "name": "keydown", "method": "handleKeyDown", "target": undefined, "capture": false, "passive": false }]; } }