UNPKG

@scania/tegel

Version:
529 lines (528 loc) 20.5 kB
import { h, Host, } from "@stencil/core"; import hasSlot from "../../utils/hasSlot"; import generateUniqueId from "../../utils/generateUniqueId"; /** * @slot header - Slot for header text * @slot body - Slot for main content of modal * @slot actions - Slot for extra buttons * */ export class TdsModal { constructor() { this.handleClose = (event) => { const closeEvent = this.tdsClose.emit(event); this.returnFocusOnClose(); if (closeEvent.defaultPrevented) { return; } this.isShown = false; }; this.handleShow = () => { const showEvent = this.tdsOpen.emit(); if (showEvent.defaultPrevented) { return; } this.isShown = true; }; /** Checks if click on Modal is on overlay, if so it closes the Modal if prevent is not true. */ this.handleOverlayClick = (event) => { const targetList = event.composedPath(); const target = targetList[0]; if (target.classList[0] === 'tds-modal-close' || (target.classList[0] === 'tds-modal-backdrop' && this.prevent === false)) { this.handleClose(event); } }; this.handleReferenceElementClick = (event) => { if (this.isShown) { this.handleClose(event); } else { this.handleShow(); } }; /** Check if there is a referenceElement or selector and adds event listener to them if so. */ this.setShowButton = () => { var _a; if (this.selector || this.referenceEl) { const referenceEl = (_a = this.referenceEl) !== null && _a !== void 0 ? _a : document.querySelector(this.selector); if (referenceEl) { this.initializeReferenceElement(referenceEl); } } }; /** Adds an event listener to the reference element that shows/closes the Modal. */ this.initializeReferenceElement = (referenceEl) => { if (referenceEl) { referenceEl.addEventListener('click', this.handleReferenceElementClick); } }; this.header = undefined; this.prevent = false; this.size = 'md'; this.actionsPosition = 'static'; this.selector = undefined; this.referenceEl = undefined; this.show = undefined; this.closable = true; this.tdsAlertDialog = 'dialog'; this.isShown = false; this.activeElementIndex = 0; } /** Shows the Modal. */ async showModal() { this.isShown = true; // Set focus on first element when opened requestAnimationFrame(() => { const focusableElements = this.getFocusableElements(); if (focusableElements.length > 0) { focusableElements[0].focus(); this.activeElementIndex = 0; } }); } /** Closes the Modal. */ async closeModal() { this.isShown = false; this.returnFocusOnClose(); } /** Returns the current open state of the Modal. */ async isOpen() { return this.isShown; } connectedCallback() { if (this.closable === undefined) { this.closable = true; } if (this.show !== undefined) { this.isShown = this.show; } this.initializeModal(); if (this.header && hasSlot('header', this.host)) { console.warn("Tegel Modal component: Using both header prop and header slot might break modal's design. Please use just one of them. "); } if (!this.selector && !this.referenceEl) { console.warn('Tegel Modal: Missing focus origin. Please provide either a "referenceEl" or a "selector" to ensure focus returns to the element that opened the modal. If the modal is opened programmatically, this message can be ignored.'); } } componentWillLoad() { this.initializeModal(); } disconnectedCallback() { this.cleanupModal(); } /** Initializes or re-initializes the modal, setting up event listeners. */ async initializeModal() { this.setDismissButtons(); this.setShowButton(); } /** Cleans up event listeners and other resources. */ async cleanupModal() { var _a; if (this.selector || this.referenceEl) { const referenceEl = (_a = this.referenceEl) !== null && _a !== void 0 ? _a : document.querySelector(this.selector); if (referenceEl) { referenceEl.removeEventListener('click', this.handleReferenceElementClick); } } this.host.querySelectorAll('[data-dismiss-modal]').forEach((dismissButton) => { dismissButton.removeEventListener('click', this.handleClose); }); } returnFocusOnClose() { var _a; let referenceElement = (_a = this.referenceEl) !== null && _a !== void 0 ? _a : document.querySelector(this.selector); if (!referenceElement) { return; // no element to return focus to } const potentialReferenceElements = ['BUTTON', 'A', 'INPUT']; const isNativeFocusable = potentialReferenceElements.includes(referenceElement.tagName); if (isNativeFocusable) { referenceElement.focus(); } else { // If referenced element is a custom element eg: tds-button we find the interactive element inside: const interactiveElement = referenceElement.querySelector(potentialReferenceElements.join(',')); if (interactiveElement) { interactiveElement.focus(); } } } getFocusableElements() { 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(this.host.shadowRoot.querySelectorAll(focusableSelectors)); const focusableInSlots = Array.from(this.host.querySelectorAll(focusableSelectors)); /** Focusable elements */ return [...focusableInShadowRoot, ...focusableInSlots]; } handleFocusTrap(event) { if (event.key === 'Escape' && this.isShown && !this.prevent) { this.handleClose(event); return; } // Only trap focus if the modal is open if (!this.isShown) 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; event.preventDefault(); // Going backwards (Shift + Tab) on the first element => move to last if (event.shiftKey) { this.activeElementIndex -= 1; if (this.activeElementIndex === -1) { this.activeElementIndex = focusableElements.length - 1; } } // // Going forwards (Tab) on the last element => move to first if (!event.shiftKey) { this.activeElementIndex += 1; if (this.activeElementIndex === focusableElements.length) { this.activeElementIndex = 0; } } const nextElement = focusableElements[this.activeElementIndex]; nextElement.focus(); } /** Adds an event listener to the dismiss buttons that closes the Modal. */ setDismissButtons() { this.host.querySelectorAll('[data-dismiss-modal]').forEach((dismissButton) => { dismissButton.addEventListener('click', this.handleClose); }); } render() { const usesHeaderSlot = hasSlot('header', this.host); const usesActionsSlot = hasSlot('actions', this.host); const headerId = this.header ? `tds-modal-header-${generateUniqueId()}` : undefined; const bodyId = `tds-modal-body-${generateUniqueId()}`; return (h(Host, { key: '2d39760de58fd957585e4370aa88447d85249eca', role: this.tdsAlertDialog, "aria-modal": "true", "aria-describedby": bodyId, "aria-labelledby": headerId, class: { show: this.isShown, hide: !this.isShown, }, onClick: (event) => this.handleOverlayClick(event) }, h("div", { key: '5eeaf5c29529bf77ddde4892067f13d312d1bba7', class: "tds-modal-backdrop" }), h("div", { key: '8a6ccd911dfec947a809320a0631d5f9904ef78d', class: `tds-modal tds-modal__actions-${this.actionsPosition} tds-modal-${this.size}` }, h("div", { key: 'b7c760ebded4e98319cec75c161002a579316902', id: headerId, class: "header" }, this.header && h("div", { key: 'db14e71913052ba5367ab95a41a366c7f2dea4a3', class: "header-text" }, this.header), usesHeaderSlot && h("slot", { key: '62b394718c78e564fd50e7ff3328e637a63679e6', name: "header" }), this.closable && (h("button", { key: '9ff8ad81be4f72ee6a298a9ba39a1024269d3ec2', class: "tds-modal-close", "aria-label": "close", onClick: (event) => this.handleClose(event) }, h("tds-icon", { key: 'b34b8a55c7f5c65ce7a3fdee6114c66ee4ab82c3', name: "cross", size: "20px" })))), h("div", { key: 'ab04e506dd94c69d8f91fb774fa85f355f3629d4', id: bodyId, class: "body" }, h("slot", { key: 'f4a8e2377b4cae3a9c4fa1032e896b0b0c58a06a', name: "body" })), usesActionsSlot && h("slot", { key: '09137d7c8b81f91dbf419b10c8a085de25df1272', name: "actions" })))); } static get is() { return "tds-modal"; } static get encapsulation() { return "shadow"; } static get originalStyleUrls() { return { "$": ["modal.scss"] }; } static get styleUrls() { return { "$": ["modal.css"] }; } static get properties() { return { "header": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Sets the header of the Modal." }, "attribute": "header", "reflect": false }, "prevent": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Disables closing Modal on clicking on overlay area." }, "attribute": "prevent", "reflect": false, "defaultValue": "false" }, "size": { "type": "string", "mutable": false, "complexType": { "original": "'xs' | 'sm' | 'md' | 'lg'", "resolved": "\"lg\" | \"md\" | \"sm\" | \"xs\"", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Size of Modal" }, "attribute": "size", "reflect": false, "defaultValue": "'md'" }, "actionsPosition": { "type": "string", "mutable": false, "complexType": { "original": "'sticky' | 'static'", "resolved": "\"static\" | \"sticky\"", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Changes the position behaviour of the actions slot." }, "attribute": "actions-position", "reflect": false, "defaultValue": "'static'" }, "selector": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "CSS selector for the element that will show the Modal." }, "attribute": "selector", "reflect": false }, "referenceEl": { "type": "unknown", "mutable": false, "complexType": { "original": "HTMLElement | null", "resolved": "HTMLElement", "references": { "HTMLElement": { "location": "global", "id": "global::HTMLElement" } } }, "required": false, "optional": true, "docs": { "tags": [], "text": "Element that will show the Modal (takes priority over selector)" } }, "show": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Controls whether the Modal is shown or not. If this is set hiding and showing\nwill be decided by this prop and will need to be controlled from the outside." }, "attribute": "show", "reflect": false }, "closable": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Shows or hides the close [X] button." }, "attribute": "closable", "reflect": false, "defaultValue": "true" }, "tdsAlertDialog": { "type": "string", "mutable": false, "complexType": { "original": "'alertdialog' | 'dialog'", "resolved": "\"alertdialog\" | \"dialog\"", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Role of the modal component. Can be either 'alertdialog' for important messages that require immediate attention, or 'dialog' for regular messages." }, "attribute": "tds-alert-dialog", "reflect": false, "defaultValue": "'dialog'" } }; } static get states() { return { "isShown": {}, "activeElementIndex": {} }; } static get events() { return [{ "method": "tdsClose", "name": "tdsClose", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "Emits when the Modal is closed." }, "complexType": { "original": "any", "resolved": "any", "references": {} } }, { "method": "tdsOpen", "name": "tdsOpen", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "Emits just before Modal is opened." }, "complexType": { "original": "void", "resolved": "void", "references": {} } }]; } static get methods() { return { "showModal": { "complexType": { "signature": "() => Promise<void>", "parameters": [], "references": { "Promise": { "location": "global", "id": "global::Promise" } }, "return": "Promise<void>" }, "docs": { "text": "Shows the Modal.", "tags": [] } }, "closeModal": { "complexType": { "signature": "() => Promise<void>", "parameters": [], "references": { "Promise": { "location": "global", "id": "global::Promise" } }, "return": "Promise<void>" }, "docs": { "text": "Closes the Modal.", "tags": [] } }, "isOpen": { "complexType": { "signature": "() => Promise<boolean>", "parameters": [], "references": { "Promise": { "location": "global", "id": "global::Promise" } }, "return": "Promise<boolean>" }, "docs": { "text": "Returns the current open state of the Modal.", "tags": [] } }, "initializeModal": { "complexType": { "signature": "() => Promise<void>", "parameters": [], "references": { "Promise": { "location": "global", "id": "global::Promise" } }, "return": "Promise<void>" }, "docs": { "text": "Initializes or re-initializes the modal, setting up event listeners.", "tags": [] } }, "cleanupModal": { "complexType": { "signature": "() => Promise<void>", "parameters": [], "references": { "Promise": { "location": "global", "id": "global::Promise" } }, "return": "Promise<void>" }, "docs": { "text": "Cleans up event listeners and other resources.", "tags": [] } } }; } static get elementRef() { return "host"; } static get listeners() { return [{ "name": "keydown", "method": "handleFocusTrap", "target": "window", "capture": true, "passive": false }]; } }