UNPKG

@scania/tegel

Version:
360 lines (359 loc) 15.4 kB
import { h, Host, } from "@stencil/core"; const GRID_LG_BREAKPOINT = 992; /** * @slot overlay - Used of injection of tds-side-menu-overlay * @slot close-button - Used for injection of tds-side-menu-close-button that is show when in mobile view * @slot <default> - <b>Unnamed slot.</b> For primary content of the side menu - like buttons. * Used for nesting main content of Side Menu, e.g. <code><tds-side-menu-item></code> and <code><tds-side-menu-dropdown></code> components * @slot end - Used for items that are presented at the bottom of the Side Menu, e.g. profile settings * @slot sticky-end - Used for tds-side-menu-collapse-button component * */ export class TdsSideMenu { constructor() { /** Applicable only for mobile. If the Side Menu is open or not. */ this.open = false; /** Applicable only for desktop. If the Side Menu should always be shown. */ this.persistent = false; /** If the Side Menu is collapsed. Only a persistent desktop menu can be collapsed. * NOTE: Only use this if you have prevented the automatic collapsing with preventDefault on the tdsCollapse event. */ this.collapsed = false; this.isMobile = false; this.isUpperSlotEmpty = false; this.isCollapsed = false; /* To preserved initial state of collapsed prop as it is changed in runtime */ this.initialCollapsedState = false; /** @internal Tracks the currently focused element index for keyboard navigation */ this.activeElementIndex = 0; this.handleMatchesLgBreakpointChange = (e) => { const isMobile = !e.matches; this.isMobile = isMobile; if (isMobile) { this.collapsed = false; } else { this.open = false; this.collapsed = this.initialCollapsedState; } }; } handleKeyDown(event) { if (event.key === 'Escape' && this.isMobile && this.open) { this.open = false; } } connectedCallback() { this.matchesLgBreakpointMq = window.matchMedia(`(min-width: ${GRID_LG_BREAKPOINT}px)`); this.matchesLgBreakpointMq.addEventListener('change', this.handleMatchesLgBreakpointChange); this.isMobile = !this.matchesLgBreakpointMq.matches; this.isCollapsed = this.collapsed; this.initialCollapsedState = this.collapsed; } componentDidLoad() { var _a; const upperSlot = (_a = this.host.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector('slot:not([name])'); const upperSlotElements = upperSlot.assignedElements(); const hasUpperSlotElements = (upperSlotElements === null || upperSlotElements === void 0 ? void 0 : upperSlotElements.length) > 0; if (!hasUpperSlotElements) { this.isUpperSlotEmpty = true; } if (this.isMobile) { this.collapsed = false; } } disconnectedCallback() { if (this.matchesLgBreakpointMq) { this.matchesLgBreakpointMq.removeEventListener('change', this.handleMatchesLgBreakpointChange); } } onCollapsedChange(newVal) { /** Emits the internal collapse event when the prop has changed. */ this.internalTdsSideMenuPropChange.emit({ changed: ['collapsed'], collapsed: newVal, }); this.isCollapsed = newVal; } onOpenChange(newVal) { if (!this.isMobile) { if (newVal) this.open = false; return; } if (newVal) { // When menu opens, focus the first interactive element setTimeout(() => { const focusableElements = this.getFocusableElements(); if (focusableElements.length > 0) { this.activeElementIndex = 0; focusableElements[0].focus(); } }, 100); } else { // When menu closes, focus the hamburger button const hamburgerComponent = document.querySelector('tds-header-hamburger'); if (hamburgerComponent && hamburgerComponent.shadowRoot) { const hamburgerButton = hamburgerComponent.shadowRoot.querySelector('button'); if (hamburgerButton) { hamburgerButton.focus(); } } } } getFocusableElements() { var _a, _b, _c; const focusableSelectors = [ 'a[href]', 'button:not([disabled])', 'textarea:not([disabled])', 'input:not([disabled])', 'select:not([disabled])', '[tabindex]:not([tabindex="-1"])', ].join(','); const focusableInShadowRoot = Array.from((_b = (_a = this.host.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelectorAll(focusableSelectors)) !== null && _b !== void 0 ? _b : []); const focusableInSlots = Array.from(this.host.querySelectorAll(focusableSelectors)); const slottedBtn = this.host.querySelector('[slot="close-button"]'); let closeBtn; if (slottedBtn) closeBtn = (_c = slottedBtn.shadowRoot) === null || _c === void 0 ? void 0 : _c.querySelector('button'); const focusableElements = [...focusableInShadowRoot, ...focusableInSlots]; if (closeBtn) focusableElements.push(closeBtn); /** Focusable elements */ return focusableElements; } handleFocusTrap(event) { // Only trap focus if the menu is open if (!this.open || !this.isMobile) return; // We care only about the Tab key if (event.key !== 'Tab') return; const focusableElements = this.getFocusableElements(); // If there are no focusable elements if (focusableElements.length === 0) return; // Prevent default tab behavior event.preventDefault(); // Going backwards (Shift + Tab) on the first element => move to last if (event.shiftKey) { this.activeElementIndex -= 1; if (this.activeElementIndex < 0) { this.activeElementIndex = focusableElements.length - 1; } } // Going forwards (Tab) on the last element => move to first else { this.activeElementIndex += 1; if (this.activeElementIndex >= focusableElements.length) { this.activeElementIndex = 0; } } // Focus the next element const nextElement = focusableElements[this.activeElementIndex]; nextElement.focus(); } collapsedSideMenuEventHandler(event) { this.collapsed = event.detail.collapsed; } render() { return (h(Host, { key: '3e7770cc6891a71d1757b0a7622f2912066643d8', class: { 'menu-opened': this.open, 'menu-persistent': this.persistent, 'menu-collapsed': this.collapsed, }, "aria-expanded": (this.isMobile ? this.open : !this.collapsed) ? 'true' : 'false' }, h("div", { key: '5c556407075da5eae0b3343b3e0ee74c340575ac', class: { 'wrapper': true, 'state-upper-slot-empty': this.isUpperSlotEmpty, 'state-open': this.open, 'state-closed': !this.open, } }, h("slot", { key: '6b87fbf18cbfa65ab8552eb64456b87304022404', name: "overlay" }), h("aside", { key: '7d63212f845d1bba59a1b3a6e0b99998fd6a2202', class: `menu` }, h("div", { key: 'd8ce4ff50101691aeb9b88d17cc7ee2490272ad4', role: "navigation" }, h("slot", { key: 'f8a7ee24fbced61ff4931168902c397b082521eb', name: "close-button" }), h("div", { key: '072baef69437e6a444b32f0b62cef196cefe1575', class: "tds-side-menu-wrapper" }, h("ul", { key: 'bf1e19f6ba886f2c89b480b4f58b45c018daac4d', class: `tds-side-menu-list tds-side-menu-list-upper` }, h("li", { key: 'e319ea5fa177d10d2251b8ab26a06bef18bbd623' }, h("slot", { key: '5fec36575727498f764896d4c585a77f4c83f8b4' }))), h("ul", { key: 'b3a8d1af131f3ac8531d1f63bf7aa728ae87927b', class: `tds-side-menu-list tds-side-menu-list-end` }, h("li", { key: '2d82fba190cdb96f6e7bb77a1f44d94b1a4f461b' }, h("slot", { key: 'c5d3f65e00c57911b5aae71d4a75b1ec894b3fcb', name: "end" })))), h("slot", { key: 'eb8c2a7a4130e9e71851735219414a9d87e95853', name: "sticky-end" })))))); } static get is() { return "tds-side-menu"; } static get encapsulation() { return "shadow"; } static get originalStyleUrls() { return { "$": ["side-menu.scss"] }; } static get styleUrls() { return { "$": ["side-menu.css"] }; } static get properties() { return { "open": { "type": "boolean", "mutable": true, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Applicable only for mobile. If the Side Menu is open or not." }, "getter": false, "setter": false, "reflect": false, "attribute": "open", "defaultValue": "false" }, "persistent": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Applicable only for desktop. If the Side Menu should always be shown." }, "getter": false, "setter": false, "reflect": false, "attribute": "persistent", "defaultValue": "false" }, "collapsed": { "type": "boolean", "mutable": true, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "If the Side Menu is collapsed. Only a persistent desktop menu can be collapsed.\nNOTE: Only use this if you have prevented the automatic collapsing with preventDefault on the tdsCollapse event." }, "getter": false, "setter": false, "reflect": false, "attribute": "collapsed", "defaultValue": "false" } }; } static get states() { return { "isMobile": {}, "isUpperSlotEmpty": {}, "isCollapsed": {}, "initialCollapsedState": {}, "activeElementIndex": {} }; } static get events() { return [{ "method": "tdsCollapse", "name": "tdsCollapse", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "Event that is emitted when the Side Menu is collapsed." }, "complexType": { "original": "CollapseEvent", "resolved": "{ collapsed: boolean; }", "references": { "CollapseEvent": { "location": "local", "path": "/home/runner/work/tegel/tegel/packages/core/src/components/side-menu/side-menu.tsx", "id": "src/components/side-menu/side-menu.tsx::CollapseEvent" } } } }, { "method": "internalTdsCollapse", "name": "internalTdsCollapse", "bubbles": true, "cancelable": false, "composed": true, "docs": { "tags": [{ "name": "internal", "text": "Broadcasts collapsed state to child components." }], "text": "" }, "complexType": { "original": "CollapseEvent", "resolved": "{ collapsed: boolean; }", "references": { "CollapseEvent": { "location": "local", "path": "/home/runner/work/tegel/tegel/packages/core/src/components/side-menu/side-menu.tsx", "id": "src/components/side-menu/side-menu.tsx::CollapseEvent" } } } }, { "method": "internalTdsSideMenuPropChange", "name": "internalTdsSideMenuPropChange", "bubbles": true, "cancelable": false, "composed": true, "docs": { "tags": [{ "name": "internal", "text": "Broadcasts collapsed state to child components." }], "text": "" }, "complexType": { "original": "InternalTdsSideMenuPropChange", "resolved": "{ changed: \"collapsed\"[]; } & Partial<Props>", "references": { "InternalTdsSideMenuPropChange": { "location": "local", "path": "/home/runner/work/tegel/tegel/packages/core/src/components/side-menu/side-menu.tsx", "id": "src/components/side-menu/side-menu.tsx::InternalTdsSideMenuPropChange" } } } }]; } static get elementRef() { return "host"; } static get watchers() { return [{ "propName": "collapsed", "methodName": "onCollapsedChange" }, { "propName": "open", "methodName": "onOpenChange" }]; } static get listeners() { return [{ "name": "keydown", "method": "handleKeyDown", "target": "window", "capture": false, "passive": false }, { "name": "keydown", "method": "handleFocusTrap", "target": "window", "capture": true, "passive": false }, { "name": "internalTdsCollapse", "method": "collapsedSideMenuEventHandler", "target": "body", "capture": false, "passive": false }]; } }