@scania/tegel
Version:
Tegel Design System
360 lines (359 loc) • 15.4 kB
JavaScript
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
}];
}
}