@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
526 lines (525 loc) • 20.3 kB
JavaScript
import { h, Host, } from "@stencil/core";
import { convertPropsToClasses } from "./modus-wc-menu-item.tailwind";
import { handleShadowDOMStyles } from "../base-component";
import { inheritAriaAttributes } from "../utils";
/**
* A customizable menu item component used to display the item portion of a menu.
*
* This component supports a 'start-icon' `<slot>` that allows for custom icons to be placed at the beginning of the item.
*/
export class ModusWcMenuItem {
constructor() {
this.inheritedAttributes = {};
/** Custom CSS class to apply to the li element. */
this.customClass = '';
/** The text rendered in the menu item. */
this.label = '';
/** The size of the menu item. */
this.size = 'md';
/** The position of the tooltip relative to the menu item. */
this.tooltipPosition = 'auto';
/** The unique identifying value of the menu item. */
this.value = '';
/** Internal state to track if submenu is expanded */
this.isExpanded = false;
this.handleItemSelect = () => {
if (this.hasSubmenu) {
// Check if side nav is expanded (if this menu is inside a side nav)
const sideNav = this.el.closest('modus-wc-side-navigation');
if (sideNav &&
!sideNav.expanded) {
// Don't allow submenu expansion when side nav is collapsed
// Still emit the event for consistency
this.itemSelect.emit({ value: this.value });
return;
}
// The submenu should be inside this menu-item element (slotted content)
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');
const buttonElement = liElement.querySelector('button');
// Update internal expanded state and add/remove class
this.isExpanded = submenu.classList.contains('modus-wc-menu-dropdown-show');
if (this.isExpanded) {
liElement.classList.add('modus-wc-menu-item-expanded');
if (buttonElement) {
buttonElement.classList.add('modus-wc-menu-dropdown-show');
}
}
else {
liElement.classList.remove('modus-wc-menu-item-expanded');
if (buttonElement) {
buttonElement.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 parentMenu = this.el.closest('modus-wc-menu');
if (parentMenu) {
this.parentMenuObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.attributeName === 'selection-mode') {
this.handleSelectionModeChange();
break;
}
}
});
this.parentMenuObserver.observe(parentMenu, { attributes: true });
}
}
disconnectedCallback() {
var _a;
(_a = this.parentMenuObserver) === 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');
this.isExpanded = false;
}
}
return Promise.resolve();
}
getRootMenu() {
var _a, _b;
let menu = this.el.closest('modus-wc-menu');
while (menu) {
const parent = (_b = (_a = menu.parentElement) === null || _a === void 0 ? void 0 : _a.closest('modus-wc-menu')) !== null && _b !== void 0 ? _b : null;
if (!parent)
break;
menu = parent;
}
return menu;
}
deselectSiblings() {
const rootMenu = this.getRootMenu();
if (!rootMenu)
return;
const allItems = rootMenu.querySelectorAll('modus-wc-menu-item');
allItems.forEach((item) => {
if (item !== this.el) {
item.selected = false;
}
});
}
resolveSelectionMode() {
var _a;
return (_a = this.el.closest('modus-wc-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(' ');
}
getButtonClasses() {
return this.hasSubmenu ? 'modus-wc-menu-dropdown-toggle' : '';
}
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;
}
render() {
const mode = this._selectionMode;
return (h(Host, { key: '08962b2c66e30cdc6222a2e4f02966c085be3f97' }, h("li", Object.assign({ key: 'be6621a5e9a1a6c0a8a68c09d9df355bbfb5e491', "aria-checked": this.getAriaChecked(mode), "aria-selected": this.getAriaSelected(mode), "aria-disabled": this.disabled, class: this.getClasses(), role: this.getRole(mode), tabIndex: this.disabled ? -1 : 0 }, this.inheritedAttributes), h("button", { key: '16730f36cf2c30cc8edd9b294ebf2fbf780c8f52', class: this.getButtonClasses(), disabled: this.disabled, onClick: this.handleItemSelect, tabIndex: -1, type: "button" }, h("div", { key: '1a85d975363a5831584b2cf09aff08ec862580fb', class: "modus-wc-menu-item-content" }, (this.checkbox || mode === 'multiple') && (h("modus-wc-checkbox", { key: '45b72935af96ab83d638a96bda0ff5eb3667f0fe', "aria-label": "Checkbox", disabled: this.disabled, size: this.size, value: !!this.selected })), h("slot", { key: '3d6a429564ed102acf8eaa068d42aecee654ddf1', name: "start-icon" }), h("div", { key: 'e824b9bc909b8289d5c80a0d530e1c8aa423b228', 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: 'ae2b477265c3acc4e2d34cd0157110b47eadfc80', class: "modus-wc-menu-item-sublabel" }, this.subLabel))))), h("slot", { key: 'cfd85f94459545f39ac4b14844ee3f8aa50d93bf' }))));
}
static get is() { return "modus-wc-menu-item"; }
static get originalStyleUrls() {
return {
"$": ["modus-wc-menu-item.scss"]
};
}
static get styleUrls() {
return {
"$": ["modus-wc-menu-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 menu 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 menu 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 menu 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 menu 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 menu 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 menu 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 menu 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 menu 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 menu 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 menu item has a collapsible submenu. When true, the item will show a caret and handle toggle behavior."
},
"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 menu 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
}];
}
}