UNPKG

@esri/calcite-components

Version:

Web Components for Esri's Calcite Design System.

665 lines (664 loc) • 20.9 kB
/*! * All material copyright ESRI, All Rights Reserved, unless otherwise specified. * See https://github.com/Esri/calcite-components/blob/master/LICENSE.md for details. * v1.5.0-next.4 */ import { Fragment, h, Host } from "@stencil/core"; import { getElementDir, slotChangeGetAssignedElements } from "../../utils/dom"; import { componentLoaded, setComponentLoaded, setUpLoadableComponent } from "../../utils/loadable"; import { CSS } from "./resources"; import { CSS_UTILITY } from "../../utils/resources"; import { connectMessages, disconnectMessages, setUpMessages, updateMessages } from "../../utils/t9n"; import { connectLocalized, disconnectLocalized } from "../../utils/locale"; /** * @slot submenu-item - A slot for adding `calcite-menu-item`s in a submenu. */ export class CalciteMenuItem { constructor() { this.clickHandler = (event) => { if ((this.href && event.target === this.dropdownActionEl) || (!this.href && this.hasSubmenu)) { this.open = !this.open; } this.selectMenuItem(event); }; this.handleMenuItemSlotChange = (event) => { this.submenuItems = slotChangeGetAssignedElements(event); this.submenuItems.forEach((item) => { if (!item.topLevelMenuLayout) { item.topLevelMenuLayout = this.topLevelMenuLayout; } }); this.hasSubmenu = this.submenuItems.length > 0; }; this.keyDownHandler = async (event) => { // opening and closing of submenu is handled here. Any other functionality is bubbled to parent menu. if (event.key === " " || event.key === "Enter") { this.selectMenuItem(event); if (this.hasSubmenu && (!this.href || (this.href && event.target === this.dropdownActionEl))) { this.open = !this.open; } event.preventDefault(); } else if (event.key === "Escape") { if (this.open) { this.open = false; return; } this.calciteInternalMenuItemKeyEvent.emit({ event }); event.preventDefault(); } else if (event.key === "ArrowDown" || event.key === "ArrowUp") { event.preventDefault(); if ((event.target === this.dropdownActionEl || !this.href) && this.hasSubmenu && !this.open && this.layout === "horizontal") { this.open = true; return; } this.calciteInternalMenuItemKeyEvent.emit({ event, children: this.submenuItems, isSubmenuOpen: this.open && this.hasSubmenu }); } else if (event.key === "ArrowLeft") { event.preventDefault(); this.calciteInternalMenuItemKeyEvent.emit({ event, children: this.submenuItems, isSubmenuOpen: true }); } else if (event.key === "ArrowRight") { event.preventDefault(); if ((event.target === this.dropdownActionEl || !this.href) && this.hasSubmenu && !this.open && this.layout === "vertical") { this.open = true; return; } this.calciteInternalMenuItemKeyEvent.emit({ event, children: this.submenuItems, isSubmenuOpen: this.open && this.hasSubmenu }); } }; this.active = undefined; this.breadcrumb = undefined; this.href = undefined; this.iconEnd = undefined; this.iconFlipRtl = undefined; this.iconStart = undefined; this.isTopLevelItem = false; this.label = undefined; this.layout = undefined; this.messageOverrides = undefined; this.messages = undefined; this.open = false; this.rel = undefined; this.target = undefined; this.text = undefined; this.topLevelMenuLayout = undefined; this.defaultMessages = undefined; this.effectiveLocale = ""; this.hasSubmenu = false; this.submenuItems = undefined; } onMessagesChange() { /* wired up by t9n util */ } effectiveLocaleChange() { updateMessages(this, this.effectiveLocale); } //-------------------------------------------------------------------------- // // Public Methods // //-------------------------------------------------------------------------- /** Sets focus on the component. */ async setFocus() { await componentLoaded(this); this.anchorEl.focus(); } //-------------------------------------------------------------------------- // // Event Listeners // //-------------------------------------------------------------------------- handleClickOut(event) { if (this.topLevelMenuLayout !== "vertical" && this.hasSubmenu && this.open && !event.composedPath().includes(this.el)) { this.open = false; } } handleFocusOut(event) { if (this.topLevelMenuLayout !== "vertical" && !this.el.contains(event.relatedTarget)) { this.open = false; } } //-------------------------------------------------------------------------- // // Lifecycle // //-------------------------------------------------------------------------- connectedCallback() { connectLocalized(this); connectMessages(this); } async componentWillLoad() { setUpLoadableComponent(this); await setUpMessages(this); } componentDidLoad() { setComponentLoaded(this); } disconnectedCallback() { disconnectLocalized(this); disconnectMessages(this); } // -------------------------------------------------------------------------- // // Private Methods // // -------------------------------------------------------------------------- blurHandler() { this.isFocused = false; } focusHandler(event) { const target = event.target; this.isFocused = true; if (target.open && !this.open) { target.open = false; } } selectMenuItem(event) { if (event.target !== this.dropdownActionEl) { this.calciteMenuItemSelect.emit(); } } //-------------------------------------------------------------------------- // // Render Methods // //-------------------------------------------------------------------------- renderIconStart() { return (h("calcite-icon", { class: `${CSS.icon} ${CSS.iconStart}`, flipRtl: this.iconFlipRtl === "start" || this.iconFlipRtl === "both", icon: this.iconStart, key: CSS.iconStart, scale: "s" })); } renderIconEnd() { return (h("calcite-icon", { class: `${CSS.icon} ${CSS.iconEnd}`, flipRtl: this.iconFlipRtl === "end" || this.iconFlipRtl === "both", icon: this.iconEnd, key: CSS.iconEnd, scale: "s" })); } renderBreadcrumbIcon(dir) { return (h("calcite-icon", { class: `${CSS.icon} ${CSS.iconBreadcrumb}`, icon: dir === "rtl" ? "chevron-left" : "chevron-right", key: CSS.iconBreadcrumb, scale: "s" })); } renderDropdownIcon(dir) { const dirChevron = dir === "rtl" ? "chevron-left" : "chevron-right"; return (h("calcite-icon", { class: `${CSS.icon} ${CSS.iconDropdown}`, icon: this.topLevelMenuLayout === "vertical" || this.isTopLevelItem ? this.open ? "chevron-up" : "chevron-down" : dirChevron, key: CSS.iconDropdown, scale: "s" })); } renderDropdownAction(dir) { const dirChevron = dir === "rtl" ? "chevron-left" : "chevron-right"; return (h("calcite-action", { class: CSS.dropdownAction, icon: this.topLevelMenuLayout === "vertical" || this.isTopLevelItem ? this.open ? "chevron-up" : "chevron-down" : dirChevron, key: CSS.dropdownAction, onClick: this.clickHandler, onKeyDown: this.keyDownHandler, text: this.messages.open, // eslint-disable-next-line react/jsx-sort-props ref: (el) => (this.dropdownActionEl = el) })); } renderSubmenuItems(dir) { return (h("calcite-menu", { class: { [CSS.dropdownMenuItems]: true, [CSS.open]: this.open, [CSS.nested]: !this.isTopLevelItem, [CSS_UTILITY.rtl]: dir === "rtl", [CSS.dropdownVertical]: this.topLevelMenuLayout === "vertical" }, label: this.messages.submenu, layout: "vertical", role: "menu" }, h("slot", { name: "submenu-item", onSlotchange: this.handleMenuItemSlotChange }))); } renderItemContent(dir) { return (h(Fragment, null, this.iconStart && this.renderIconStart(), h("div", { class: CSS.textContainer }, h("span", null, this.text)), this.iconEnd && this.renderIconEnd(), this.breadcrumb ? this.renderBreadcrumbIcon(dir) : null, !this.href && this.hasSubmenu ? this.renderDropdownIcon(dir) : null)); } render() { const dir = getElementDir(this.el); return (h(Host, { onBlur: this.blurHandler, onFocus: this.focusHandler }, h("li", { class: { [CSS.container]: true, [CSS.isParentVertical]: this.topLevelMenuLayout === "vertical" }, role: "none" }, h("div", { class: CSS.itemContent }, h("a", { "aria-current": this.isFocused ? "page" : false, "aria-expanded": this.open, "aria-haspopup": this.hasSubmenu, "aria-label": this.label, class: { [CSS.layoutVertical]: this.layout === "vertical", [CSS.content]: true }, href: this.href, onClick: this.clickHandler, onKeyDown: this.keyDownHandler, rel: this.rel, role: "menuitem", tabIndex: this.isTopLevelItem ? 0 : -1, target: this.target, // eslint-disable-next-line react/jsx-sort-props ref: (el) => (this.anchorEl = el) }, this.renderItemContent(dir), this.href && (this.topLevelMenuLayout === "vertical" || !this.isTopLevelItem) ? (h("calcite-icon", { class: CSS.hoverHrefIcon, icon: dir === "rtl" ? "arrow-left" : "arrow-right", scale: "s" })) : null), this.href && this.hasSubmenu ? this.renderDropdownAction(dir) : null), this.renderSubmenuItems(dir)))); } static get is() { return "calcite-menu-item"; } static get encapsulation() { return "shadow"; } static get originalStyleUrls() { return { "$": ["menu-item.scss"] }; } static get styleUrls() { return { "$": ["menu-item.css"] }; } static get assetsDirs() { return ["assets"]; } static get properties() { return { "active": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "When `true`, the component is highlighted." }, "attribute": "active", "reflect": true }, "breadcrumb": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "When `true`, the component displays a breadcrumb trail for use as a navigational aid." }, "attribute": "breadcrumb", "reflect": true }, "href": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies the URL destination of the component, which can be set as an absolute or relative path." }, "attribute": "href", "reflect": false }, "iconEnd": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies an icon to display at the end of the component." }, "attribute": "icon-end", "reflect": true }, "iconFlipRtl": { "type": "string", "mutable": false, "complexType": { "original": "FlipContext", "resolved": "\"both\" | \"end\" | \"start\"", "references": { "FlipContext": { "location": "import", "path": "../interfaces" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Displays the `iconStart` and/or `iconEnd` as flipped when the element direction is right-to-left (`\"rtl\"`)." }, "attribute": "icon-flip-rtl", "reflect": true }, "iconStart": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies an icon to display at the start of the component." }, "attribute": "icon-start", "reflect": true }, "isTopLevelItem": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "internal", "text": undefined }], "text": "" }, "attribute": "is-top-level-item", "reflect": false, "defaultValue": "false" }, "label": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": true, "optional": false, "docs": { "tags": [], "text": "Accessible name for the component." }, "attribute": "label", "reflect": false }, "layout": { "type": "string", "mutable": false, "complexType": { "original": "Layout", "resolved": "\"horizontal\" | \"vertical\"", "references": { "Layout": { "location": "global" } } }, "required": false, "optional": false, "docs": { "tags": [{ "name": "internal", "text": undefined }], "text": "" }, "attribute": "layout", "reflect": true }, "messageOverrides": { "type": "unknown", "mutable": true, "complexType": { "original": "Partial<MenuItemMessages>", "resolved": "{ submenu?: string; open?: string; }", "references": { "Partial": { "location": "global" }, "MenuItemMessages": { "location": "import", "path": "./assets/menu-item/t9n" } } }, "required": false, "optional": false, "docs": { "tags": [], "text": "Use this property to override individual strings used by the component." } }, "messages": { "type": "unknown", "mutable": true, "complexType": { "original": "MenuItemMessages", "resolved": "{ submenu: string; open: string; }", "references": { "MenuItemMessages": { "location": "import", "path": "./assets/menu-item/t9n" } } }, "required": false, "optional": false, "docs": { "tags": [{ "name": "internal", "text": undefined }], "text": "Made into a prop for testing purposes only." } }, "open": { "type": "boolean", "mutable": true, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "When `true`, the component will display any slotted `calcite-menu-item` in an open overflow menu." }, "attribute": "open", "reflect": true, "defaultValue": "false" }, "rel": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "mdn", "text": "[rel](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel)" }], "text": "Defines the relationship between the `href` value and the current document." }, "attribute": "rel", "reflect": true }, "target": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [{ "name": "mdn", "text": "[target](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-target)" }], "text": "Specifies where to open the linked document defined in the `href` property." }, "attribute": "target", "reflect": true }, "text": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Specifies the text to display." }, "attribute": "text", "reflect": false }, "topLevelMenuLayout": { "type": "string", "mutable": false, "complexType": { "original": "Layout", "resolved": "\"horizontal\" | \"vertical\"", "references": { "Layout": { "location": "global" } } }, "required": false, "optional": false, "docs": { "tags": [{ "name": "internal", "text": undefined }], "text": "" }, "attribute": "top-level-menu-layout", "reflect": false } }; } static get states() { return { "defaultMessages": {}, "effectiveLocale": {}, "hasSubmenu": {}, "submenuItems": {} }; } static get events() { return [{ "method": "calciteInternalMenuItemKeyEvent", "name": "calciteInternalMenuItemKeyEvent", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [{ "name": "internal", "text": undefined }], "text": "" }, "complexType": { "original": "MenuItemCustomEvent", "resolved": "MenuItemCustomEvent", "references": { "MenuItemCustomEvent": { "location": "import", "path": "./interfaces" } } } }, { "method": "calciteMenuItemSelect", "name": "calciteMenuItemSelect", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "Emits when the component is selected." }, "complexType": { "original": "void", "resolved": "void", "references": {} } }]; } static get methods() { return { "setFocus": { "complexType": { "signature": "() => Promise<void>", "parameters": [], "references": { "Promise": { "location": "global" } }, "return": "Promise<void>" }, "docs": { "text": "Sets focus on the component.", "tags": [] } } }; } static get elementRef() { return "el"; } static get watchers() { return [{ "propName": "messageOverrides", "methodName": "onMessagesChange" }, { "propName": "effectiveLocale", "methodName": "effectiveLocaleChange" }]; } static get listeners() { return [{ "name": "click", "method": "handleClickOut", "target": "window", "capture": false, "passive": false }, { "name": "focusout", "method": "handleFocusOut", "target": undefined, "capture": false, "passive": false }]; } }