UNPKG

@postnord/web-components

Version:

PostNord Web Components

178 lines (177 loc) 7.08 kB
/*! * Built with Stencil * By PostNord. */ import { h, Host, forceUpdate } from "@stencil/core"; import { close } from "pn-design-assets/pn-assets/icons.js"; /** * Present content in a modal that overlays everything on the current page. * Use the prop `open` to toggle the visiblity. * Use the event `close` to determine when the modal is being closed by the user. * * @slot buttons - Place buttons inside the modal at the bottom. */ export class PnModal { mo; untabbable; elToFocus; handleFocus = this.focusHandler.bind(this); handleBlur = this.blurHandler.bind(this); handleEsc = this.escHandler.bind(this); handleGlobalClick = this.globalClickHandler.bind(this); hostElement; focusableEls; /** Bind to this property if you want to control the visibility of the modal from your own data. */ open = false; handleOpen() { if (this.open) { this.addEventListeners(); } else { this.removeEventListeners(); this.elToFocus = null; this.close.emit(this.open); } } /** Event fired when the modal is closed, either by clicking on the dark background or by clicking on the close button. */ close; componentDidLoad() { if (this.mo) this.mo.disconnect(); this.mo = new MutationObserver(() => { forceUpdate(this.hostElement); this.setFocusableElements(); }); this.mo.observe(this.hostElement.querySelector('.pn-modal'), { childList: true, subtree: true }); // If the modal is opened when the page loads we still want to register the events. if (this.open) this.handleOpen(); this.setFocusableElements(); } toggleOpen(state) { this.open = state ?? !this.open; } setFocusableElements() { // This place is where I see the most coming changes/bugs taking place. requestAnimationFrame(() => { this.focusableEls = Array.from(this.hostElement.querySelectorAll('a[href], button, input, textarea, select, details, [tabindex]:not([tabindex="-1"]):not(.pn-modal-backdrop)')); this.untabbable = Array.from(this.hostElement.querySelectorAll('[tabindex="-1"]')); }); } addEventListeners() { const root = this.hostElement.getRootNode(); root.addEventListener('focusin', this.handleFocus); root.addEventListener('focusout', this.handleBlur); root.addEventListener('keydown', this.handleEsc); // Adding RAF to ensure clicks aren't registered before the modal has opened. requestAnimationFrame(() => document.addEventListener('pointerdown', this.handleGlobalClick)); } removeEventListeners() { const root = this.hostElement.getRootNode(); root.removeEventListener('focusin', this.handleFocus); root.removeEventListener('focusout', this.handleBlur); root.removeEventListener('keydown', this.handleEsc); document.removeEventListener('pointerdown', this.handleGlobalClick); } focusHandler(e) { const target = e.composedPath()[0]; if ((!this.focusableEls.includes(target) && !this.untabbable.includes(target)) || target.classList.contains('pn-modal-backdrop')) { if (this.elToFocus) { this.elToFocus.focus(); return; } this.focusableEls[0].focus(); } } blurHandler(e) { const target = e.composedPath?.()[0]; const index = this.focusableEls.indexOf(target); const numberOfEls = this.focusableEls.length - 1; if (index === 0) this.elToFocus = this.focusableEls[numberOfEls]; if (index === numberOfEls) this.elToFocus = this.focusableEls[0]; } escHandler({ code }) { if (code === 'Escape') this.toggleOpen(false); } globalClickHandler(e) { const target = e.composedPath?.()[0]; if (!this.hostElement.contains(target) || target.classList.contains('pn-modal-backdrop')) { // This is to prevent the focus and blur events from being triggered when closing the modal. e.preventDefault(); this.toggleOpen(false); } } render() { return (h(Host, { key: 'e147efddf99cb12726295253e89ec8f83c16d6a1', "data-open": this.open }, h("div", { key: '9ad10f6e7fdb2445d6b1a99801bd416cc77df976', class: "pn-modal-backdrop", tabindex: "0" }), h("div", { key: '479ee53eac130748bc33d2b49da57fe183a26c34', class: "pn-modal" }, h("pn-button", { key: 'fe7d40eed5b260949e889ed172d77311748873e9', small: true, class: "pn-modal-close-button", icon: close, iconOnly: true, arialabel: "close", appearance: "light", type: "button", onPnClick: () => this.toggleOpen() }), h("div", { key: '6f0a21860e2eaa16f01b87ac11cce3095768e0d9', class: "pn-modal-content" }, h("slot", { key: 'd15335e6f44b2a4b4d394bd3779e65ab2f3d7d4f' })), h("slot", { key: '24b457f92a61552a0cdd375c97d2a3cc1106013d', name: "buttons" })))); } static get is() { return "pn-modal"; } static get originalStyleUrls() { return { "$": ["pn-modal.scss"] }; } static get styleUrls() { return { "$": ["pn-modal.css"] }; } static get properties() { return { "open": { "type": "boolean", "mutable": true, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Bind to this property if you want to control the visibility of the modal from your own data." }, "getter": false, "setter": false, "attribute": "open", "reflect": false, "defaultValue": "false" } }; } static get states() { return { "focusableEls": {} }; } static get events() { return [{ "method": "close", "name": "close", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "Event fired when the modal is closed, either by clicking on the dark background or by clicking on the close button." }, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} } }]; } static get elementRef() { return "hostElement"; } static get watchers() { return [{ "propName": "open", "methodName": "handleOpen" }]; } } //# sourceMappingURL=pn-modal.js.map