UNPKG

@scania/tegel

Version:
269 lines (265 loc) 18.3 kB
import { r as registerInstance, c as createEvent, h, H as Host, a as getElement } from './index-9xxNGlso.js'; import { h as hasSlot } from './hasSlot-DDX6uFcm.js'; import { g as generateUniqueId } from './generateUniqueId-Cn4f8w1e.js'; const modalCss = () => `:host,:root{--tds-scrollbar-width-standard:thin;--tds-scrollbar-width:10px;--tds-scrollbar-height:10px;--tds-scrollbar-thumb-border-width:3px;--tds-scrollbar-thumb-border-hover-width:2px}body{scrollbar-width:thin}.tds-modal{box-sizing:border-box;box-shadow:var(--tds-modal-box-shadow);background-color:var(--background-elevation-layer-02);margin:auto;position:relative;border-radius:var(--radius-narrow);max-height:85vh;overflow-y:auto;pointer-events:auto}.tds-modal *{box-sizing:border-box}.tds-modal:hover::-webkit-scrollbar-thumb{border:var(--tds-scrollbar-thumb-border-hover-width) solid transparent;background-clip:padding-box}.tds-modal::-webkit-scrollbar{width:var(--tds-scrollbar-width)}.tds-modal::-webkit-scrollbar-track{background:var(--tds-scrollbar-track-color)}.tds-modal::-webkit-scrollbar-thumb{border-radius:40px;background:var(--tds-scrollbar-thumb-color);border:var(--tds-scrollbar-thumb-border-width) solid transparent;background-clip:padding-box}.tds-modal::-webkit-scrollbar-button{height:0;width:0}@supports not selector(::-webkit-scrollbar){.tds-modal{scrollbar-color:var(--tds-scrollbar-thumb-color) var(--tds-scrollbar-track-color);scrollbar-width:var(--tds-scrollbar-width-standard)}}.tds-modal__actions-sticky{overflow:hidden;display:flex;flex-direction:column}.tds-modal__actions-sticky .body{font-family:var(--body-01-font-family);font-size:var(--body-01-font-size);line-height:var(--body-01-line-height);font-weight:var(--body-01-font-weight);letter-spacing:var(--body-01-letter-spacing);text-transform:var(--body-01-text-transform);max-height:calc(85vh - 36px);overflow-y:auto}.tds-modal__actions-sticky slot[name=actions]{bottom:-1px;left:0;right:0;background-color:var(--background-elevation-layer-02);padding:24px 16px 16px;display:flex;gap:16px}.tds-modal__actions-static slot[name=actions]{background-color:var(--background-elevation-layer-02);display:flex;gap:16px;padding:24px 16px 16px}@media (min-width: 320px){.tds-modal-xs{width:100%}.tds-modal-sm{width:100%}.tds-modal-md{width:100%}.tds-modal-lg{width:100%}}@media (min-width: 672px){.tds-modal-xs{width:50%}.tds-modal-sm{width:62.5%}.tds-modal-md{width:75%}.tds-modal-lg{width:100%}}@media (min-width: 1056px){.tds-modal-xs{width:31.25%}.tds-modal-sm{width:43.75%}.tds-modal-md{width:62.5%}.tds-modal-lg{width:75%}}@media (min-width: 1312px){.tds-modal-xs{width:31.25%}.tds-modal-sm{width:37.5%}.tds-modal-md{width:62.5%}.tds-modal-lg{width:75%}}@media (min-width: 1584px){.tds-modal-xs{width:25%}.tds-modal-sm{width:37.5%}.tds-modal-md{width:50%}.tds-modal-lg{width:75%}}@media (max-width: 320px){.tds-modal-md,.tds-modal-lg,.tds-modal-sm{height:100%}.tds-modal-md slot[name=actions]::slotted(*),.tds-modal-lg slot[name=actions]::slotted(*),.tds-modal-sm slot[name=actions]::slotted(*){display:flex}}.header{display:flex;padding:16px;position:sticky;top:0;background-color:var(--background-elevation-layer-02);z-index:1}.header,slot[name=header]::slotted(*){font-family:var(--headline-05-font-family);font-size:var(--headline-05-font-size);line-height:var(--headline-05-line-height);font-weight:var(--headline-05-font-weight);letter-spacing:var(--headline-05-letter-spacing);text-transform:var(--headline-05-text-transform);color:var(--foreground-text-strong);margin:0;flex:1}.body{font-family:var(--body-01-font-family);font-size:var(--body-01-font-size);line-height:var(--body-01-line-height);font-weight:var(--body-01-font-weight);letter-spacing:var(--body-01-letter-spacing);text-transform:var(--body-01-text-transform);color:var(--foreground-text-strong);overflow-y:visible;padding:0 16px 16px}.body:hover::-webkit-scrollbar-thumb{border:var(--tds-scrollbar-thumb-border-hover-width) solid transparent;background-clip:padding-box}.body::-webkit-scrollbar{width:var(--tds-scrollbar-width)}.body::-webkit-scrollbar-track{background:var(--tds-scrollbar-track-color)}.body::-webkit-scrollbar-thumb{border-radius:40px;background:var(--tds-scrollbar-thumb-color);border:var(--tds-scrollbar-thumb-border-width) solid transparent;background-clip:padding-box}.body::-webkit-scrollbar-button{height:0;width:0}@supports not selector(::-webkit-scrollbar){.body{scrollbar-color:var(--tds-scrollbar-thumb-color) var(--tds-scrollbar-track-color);scrollbar-width:var(--tds-scrollbar-width-standard)}}.tds-modal-backdrop{box-sizing:border-box;position:fixed;top:0;right:0;bottom:0;left:0;background-color:var(--tds-modal-backdrop-background);pointer-events:auto}.tds-modal-backdrop *{box-sizing:border-box}button.tds-modal-close{margin:0 0 auto auto;background-color:transparent;border:0;padding:0;appearance:unset}.tds-modal-close{display:inline-block;height:auto;color:var(--foreground-text-strong);cursor:pointer}.tds-modal-close:focus{outline:2px solid var(--tds-focus-outline-color);box-shadow:0 0 0 1px var(--tds-white);outline-offset:1px;z-index:1}@media (min-width: 320px){.tds-modal-close{margin-left:16px}}@media (min-width: 1056px){.tds-modal-close{margin-left:48px}}.tds-modal-close-btn{display:inline-block;height:auto;background-repeat:no-repeat;cursor:pointer}@media (min-width: 320px){.tds-modal-close-btn{margin-left:16px}}@media (min-width: 1056px){.tds-modal-close-btn{margin-left:48px}}.tds-modal-close-btn svg{fill:var(--foreground-text-strong)}.tds-modal-overflow{overflow:hidden}:host{box-sizing:border-box;position:fixed;top:0;right:0;bottom:0;left:0;padding:0 16px;z-index:700;pointer-events:none}:host *{box-sizing:border-box}:host .tds-modal-close{border:none;background-color:transparent}:host .tds-modal-close-btn{border:none;background-color:transparent}@media (max-width: 320px){:host{padding:0}}:host.show{display:flex}:host.hide{display:none}:host(.show){display:flex}:host(.hide){display:none}`; const TdsModal = class { constructor(hostRef) { registerInstance(this, hostRef); this.tdsClose = createEvent(this, "tdsClose", 7); this.tdsOpen = createEvent(this, "tdsOpen", 7); /** Disables closing Modal on clicking on overlay area. */ this.prevent = false; /** Size of Modal */ this.size = 'md'; /** Changes the position behaviour of the actions slot. */ this.actionsPosition = 'static'; /** Shows or hides the close [X] button. */ this.closable = true; /** Role of the modal component. Can be either 'alertdialog' for important messages that require immediate attention, or 'dialog' for regular messages. */ this.tdsAlertDialog = 'dialog'; // State that keeps track of show/closed state for the Modal. this.isShown = false; // Focus state index in focusable Elements this.activeElementIndex = 0; this.handleClose = (event) => { const closeEvent = this.tdsClose.emit(event); if (closeEvent.defaultPrevented) return; this.isShown = false; this.returnFocusOnClose(); }; this.handleShow = () => { const showEvent = this.tdsOpen.emit(); if (showEvent.defaultPrevented) return; this.isShown = true; this.onOpen(); }; /** 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 : (this.selector ? document.querySelector(this.selector) : null); 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); } }; } /** Shows the Modal. */ async showModal() { this.isShown = true; this.onOpen(); } /** Closes the Modal. */ async closeModal() { this.isShown = false; this.returnFocusOnClose(); } /** Returns the current open state of the Modal. */ async isOpen() { return this.isShown; } /** Runs whenever the show prop changes. */ handleShowPropChange(newValue, oldValue) { if (newValue === oldValue || newValue === undefined) return; this.isShown = newValue; if (newValue) { this.onOpen(); } else { this.returnFocusOnClose(); } } 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 : (this.selector ? document.querySelector(this.selector) : null); if (referenceEl) { referenceEl.removeEventListener('click', this.handleReferenceElementClick); } } this.host.querySelectorAll('[data-dismiss-modal]').forEach((dismissButton) => { dismissButton.removeEventListener('click', this.handleClose); }); } returnFocusOnClose() { var _a; const referenceElement = (_a = this.referenceEl) !== null && _a !== void 0 ? _a : (this.selector ? document.querySelector(this.selector) : null); if (!referenceElement) return; const potentialReferenceElements = ['BUTTON', 'A', 'INPUT']; const isNativeFocusable = potentialReferenceElements.includes(referenceElement.tagName); const interactiveElement = isNativeFocusable ? referenceElement : referenceElement.querySelector(potentialReferenceElements.join(',')); if (!interactiveElement) return; interactiveElement.classList.remove('active'); interactiveElement.focus(); } getFocusableElements() { var _a, _b; 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)); /** Focusable elements */ return [...focusableInShadowRoot, ...focusableInSlots]; } /** Resets the scroll position to the top. */ resetScrollPosition() { var _a; const root = this.host.shadowRoot; const scroller = (_a = root === null || root === void 0 ? void 0 : root.querySelector('.tds-modal__actions-sticky .body')) !== null && _a !== void 0 ? _a : root === null || root === void 0 ? void 0 : root.querySelector('.tds-modal'); scroller === null || scroller === void 0 ? void 0 : scroller.scrollTo(0, 0); } focusFirstElement() { var _a, _b; const focusableSelectors = [ 'a[href]', 'button:not([disabled])', 'textarea:not([disabled])', 'input:not([disabled])', 'select:not([disabled])', '[tabindex]:not([tabindex="-1"])', ].join(','); // Prioritize focusable elements in slotted content (actions/body) over shadow DOM elements (like close button) const focusableInSlots = Array.from(this.host.querySelectorAll(focusableSelectors)); if (focusableInSlots.length > 0) { focusableInSlots[0].focus(); this.activeElementIndex = this.getFocusableElements().indexOf(focusableInSlots[0]); } else { // Fallback to shadow DOM elements if no slotted content is focusable const focusableInShadowRoot = Array.from((_b = (_a = this.host.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelectorAll(focusableSelectors)) !== null && _b !== void 0 ? _b : []); if (focusableInShadowRoot.length > 0) { focusableInShadowRoot[0].focus(); this.activeElementIndex = 0; } } } /** Runs whenever the modal is opened and updates it. */ onOpen() { // Focus immediately to preserve interaction modality for :focus-visible this.focusFirstElement(); // Defer scroll reset to next frame requestAnimationFrame(() => { this.resetScrollPosition(); }); } 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: '627470cc4fd39a05084149c125b34e8c41fca76b', 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: '64cbcfcc22244a706d448dab7834a83173943d85', class: "tds-modal-backdrop" }), h("div", { key: 'ab7af811011361c8b5fb311dff6fd12143ad9fa4', class: `tds-modal tds-modal__actions-${this.actionsPosition} tds-modal-${this.size}`, tabindex: "-1" }, h("div", { key: '4aa8706b05c2b2dfd286368b0f1dd7e4eaaec10f', id: headerId, class: "header" }, this.header && h("div", { key: 'a24a3998f441a80a5ba39082e1b8c6a4568424a1', class: "header-text" }, this.header), usesHeaderSlot && h("slot", { key: '5aba912426f4743d5c136230cfbccbc3730b2adf', name: "header" }), this.closable && (h("button", { key: '14da162e80c29bdaad47990af70b533a1a2e0fc2', class: "tds-modal-close", "aria-label": "close", onClick: (event) => this.handleClose(event) }, h("tds-icon", { key: 'f28f4aedb9e432849803a5961db568db4c157003', name: "cross", size: "20px" })))), h("div", { key: '928a57c57f5bbbcd6411da9378d2001c9befb855', id: bodyId, class: "body" }, h("slot", { key: '800c2deda52545ce5ca457c3274734df18a1bd57', name: "body" })), usesActionsSlot && h("slot", { key: 'd453d7e1fe3062aa238ca19883345aa09c0b87c5', name: "actions" })))); } get host() { return getElement(this); } static get watchers() { return { "show": [{ "handleShowPropChange": 0 }] }; } }; TdsModal.style = modalCss(); export { TdsModal as tds_modal };