UNPKG

@postnord/web-components

Version:
553 lines (552 loc) 21.7 kB
/*! * Built with Stencil * By PostNord. */ import { h, Host, forceUpdate, } from "@stencil/core"; import { awaitTopbar, en, isSmallScreen, reduceMotion } from "../../../index"; import { close as closeIcon } from "pn-design-assets/pn-assets/icons.js"; import { translations } from "./translation"; /** * Present content in a modal that overlays everything on the current page. * Use the prop `open` to toggle the visiblity. * Use the event `modalToggle` to determine when the modal is being closed/opened by the user. * * @slot - The default slot, will be placed in the middle content area. * @slot image - Place an image or illustration at the top of the modal. {@since v7.15.0} * @slot header - Place the title + lead text in the header. {@since v7.14.0} * @slot buttons - Place buttons inside the modal at the bottom. * pn-buttons aligns automatically to the right, set the data-left attribute on the button you want to align to the left. */ export class PnModal { mo; standardAnimationDuration = 400; animationDuration = this.standardAnimationDuration; modalTimeout; modalElement; modalContainer; modalPicture; hostElement; isClosing = false; isOpening = false; /** If true, the modal content will remove overflow CSS property. */ removeOverflow = false; /** Set a label for the modal. @since v7.14.0 */ label; /** Set a descriptive text for the modal. @since v7.14.0 */ helpertext; /** Set the language. @since v7.14.0 */ language = null; /** Set a custom HTML id. @since v7.24.0 */ pnId = null; /** Use this prop if you want to control the visibility of the modal programmatically. @category Features */ open = false; /** * Prevent users from closing the modal by clicking on the backdrop or the `ESC` key. * @since v7.14.0 * @category Features */ persistent = false; /** * Hide the close button. If you enable this, make sure you build your own cancel button. * @since v7.14.0 * @category Features */ hideClose = false; /** * Allow overflow *when* it is possible. * Do not use if you have a lot of conditional rendering inside the modal as it can cause layout shifts. * @since v7.14.0 * @category Features */ allowOverflow = false; /** * Use the `sheet` visual. Aligns the modal to the right instead of the center. * @since v7.14.0 * @category Visual */ sheet = false; /** * Define your own max width of the modal. Default is `45em`. * @category Visual * @since v7.14.0 */ maxWidth = null; handleOpen() { if (this.open) this.openModal(); else { this.closeModal(); /** In the next update, we can remove this one so you can finally stack modals. */ this.close.emit(true); } this.handleOverflow(); clearTimeout(this.modalTimeout); this.isClosing = !this.open && true; if (reduceMotion()) this.animationDuration = 0; else this.animationDuration = this.standardAnimationDuration; this.modalTimeout = setTimeout(() => { this.isClosing = false; /** @todo remove in v8.0.0 */ this.modalVisiblity.emit({ visible: this.open }); this.modalVisibility.emit({ visible: this.open }); }, this.animationDuration); } handleMaxWidth() { const width = this.maxWidth || '45em'; this.modalElement.style.setProperty('--pn-modal-max-width', width); } handleOverflow() { if (this.allowOverflow) this.setOverflow(); } /** This event is fired when the modal is toggled. {@since v7.14.0} */ modalToggle; /** This event is dispatched after the opening/closing animation has finished playing. {@since v7.22.0} */ modalVisibility; /** * This event is dispatched after the opening/closing animation has finished playing. * @since v7.14.0 * @deprecated v7.22.0. Use `modalVisibility` instead due to typo. */ modalVisiblity; /** * Event fired when the modal is closed, either by clicking on the dark background or by clicking on the close button. * @deprecated Use the new `modalToggle` event instead. */ close; connectedCallback() { this.mo = new MutationObserver(() => forceUpdate(this.hostElement)); this.mo.observe(this.hostElement, { childList: true, subtree: true }); } disconnectedCallback() { if (this.mo) this.mo.disconnect(); } async componentWillLoad() { if (this.language === null) await awaitTopbar(this.hostElement); } componentDidLoad() { this.handleMaxWidth(); if (this.open) this.openModal(); } translate(prop) { return translations?.[prop]?.[this.language || en] || prop; } showImage() { return !!this.modalPicture?.innerHTML; } showHeader() { return !!(this.label || this.helpertext || this.hostElement.querySelector('[slot="header"]')?.textContent); } setOverflow() { this.removeOverflow = !this.hasOverflow(); } hasOverflow() { const multiplyWith = isSmallScreen() ? 0.95 : 0.85; return this.modalContainer?.scrollHeight > innerHeight * multiplyWith; } isSameModal(target) { return this.modalElement.isSameNode(target); } handleKeyboard(event) { if (event.key !== 'Escape') return; event.preventDefault(); event.stopImmediatePropagation(); if (!this.persistent) this.setModalClose(); } clickModalBackground(event) { const { clientY, clientX } = event; const notInsideAnyModal = event.target.localName !== this.modalElement.localName; const notThisModal = !this.isSameModal(event.target); if (notInsideAnyModal || notThisModal) return; const { top, left, height, width } = this.modalElement.getBoundingClientRect(); const isInModal = top <= clientY && clientY <= top + height && left <= clientX && clientX <= left + width; if (!isInModal && !this.persistent) this.toggleOpen(false); } setModalClose() { this.toggleOpen(false); } closeModal() { this.modalElement.close(); } openModal() { this.modalElement.showModal(); } toggleOpen(state) { this.open = state ?? !this.open; } render() { return (h(Host, { key: '5b50fca359ed20cebcbdead766d08c2439463b4c' }, h("dialog", { key: '43eb7bb4f09c6ce9a665f7478f25b1ae4a253562', id: this.pnId, class: "pn-modal", "data-closing": this.isClosing, "data-sheet": this.sheet, "data-image": this.showImage(), "data-allow-overflow": this.removeOverflow, onClose: () => this.setModalClose(), onCancel: () => this.setModalClose(), onToggle: () => this.modalToggle.emit({ open: this.open }), onKeyDown: event => this.handleKeyboard(event), onClick: event => this.clickModalBackground(event), ref: el => (this.modalElement = el) }, !this.hideClose && (h("pn-button", { key: '0bdc665fe32c8531edb073166fe8940d12360661', small: true, class: "pn-modal-close-button", icon: closeIcon, iconOnly: true, arialabel: this.translate('CLOSE_MODAL'), appearance: "light", variant: "borderless", type: "button", onPnClick: () => this.setModalClose() })), h("div", { key: 'ce9529f76b572f6681e1b75528bcab67e3246dbd', class: "pn-modal-container", ref: el => (this.modalContainer = el) }, h("div", { key: 'a2addec74c8af3fc9084a0d6faba9053a255bf86', class: "pn-modal-image", hidden: !this.showImage() }, h("picture", { key: '4341ae13d01268cc0f9df74bf417522116bed2d9', class: "pn-modal-picture", ref: el => (this.modalPicture = el) }, h("slot", { key: '99a643af231cdcd76ad2dafbcdbaf4fbabac5390', name: "image" }))), h("header", { key: '849eea60adcfb7c3bbba8687dc49edf752004617', class: "pn-modal-header", hidden: !this.showHeader() }, this.label && h("h2", { key: 'e57185ebd04cd622ceca5acb81c6606747afa6f9', class: "pn-modal-label" }, this.label), this.helpertext && h("p", { key: '2e0290387a2b96f3eb70db67dd7ac349c5c4fe9e', class: "pn-modal-text" }, this.helpertext), h("slot", { key: 'e05316839bfc8883a2e8f4c5079054e9edcd3859', name: "header" })), h("section", { key: 'd59b818c18159c106df1f6c3b2b3f747f1b6af29', class: "pn-modal-content" }, h("slot", { key: 'c740eb0b7909c9fa5878caecb66887f5bb9b2b73' })), h("nav", { key: '22971631737901988e27f4f01c311d2d76839c6a', class: "pn-modal-buttons", "data-divider": this.sheet }, h("slot", { key: '3c0e8e66db96d8abc8f718270d94e09b2ad90320', 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 { "label": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": true, "docs": { "tags": [{ "name": "since", "text": "v7.14.0" }], "text": "Set a label for the modal." }, "getter": false, "setter": false, "reflect": false, "attribute": "label" }, "helpertext": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": true, "docs": { "tags": [{ "name": "since", "text": "v7.14.0" }], "text": "Set a descriptive text for the modal." }, "getter": false, "setter": false, "reflect": false, "attribute": "helpertext" }, "language": { "type": "string", "mutable": false, "complexType": { "original": "PnLanguages", "resolved": "\"\" | \"da\" | \"en\" | \"fi\" | \"no\" | \"sv\"", "references": { "PnLanguages": { "location": "import", "path": "@/index", "id": "src/index.ts::PnLanguages", "referenceLocation": "PnLanguages" } } }, "required": false, "optional": true, "docs": { "tags": [{ "name": "since", "text": "v7.14.0" }], "text": "Set the language." }, "getter": false, "setter": false, "reflect": false, "attribute": "language", "defaultValue": "null" }, "pnId": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": true, "docs": { "tags": [{ "name": "since", "text": "v7.24.0" }], "text": "Set a custom HTML id." }, "getter": false, "setter": false, "reflect": false, "attribute": "pn-id", "defaultValue": "null" }, "open": { "type": "boolean", "mutable": true, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": true, "docs": { "tags": [{ "name": "category", "text": "Features" }], "text": "Use this prop if you want to control the visibility of the modal programmatically." }, "getter": false, "setter": false, "reflect": true, "attribute": "open", "defaultValue": "false" }, "persistent": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": true, "docs": { "tags": [{ "name": "since", "text": "v7.14.0" }, { "name": "category", "text": "Features" }], "text": "Prevent users from closing the modal by clicking on the backdrop or the `ESC` key." }, "getter": false, "setter": false, "reflect": false, "attribute": "persistent", "defaultValue": "false" }, "hideClose": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": true, "docs": { "tags": [{ "name": "since", "text": "v7.14.0" }, { "name": "category", "text": "Features" }], "text": "Hide the close button. If you enable this, make sure you build your own cancel button." }, "getter": false, "setter": false, "reflect": false, "attribute": "hide-close", "defaultValue": "false" }, "allowOverflow": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": true, "docs": { "tags": [{ "name": "since", "text": "v7.14.0" }, { "name": "category", "text": "Features" }], "text": "Allow overflow *when* it is possible.\nDo not use if you have a lot of conditional rendering inside the modal as it can cause layout shifts." }, "getter": false, "setter": false, "reflect": false, "attribute": "allow-overflow", "defaultValue": "false" }, "sheet": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": true, "docs": { "tags": [{ "name": "since", "text": "v7.14.0" }, { "name": "category", "text": "Visual" }], "text": "Use the `sheet` visual. Aligns the modal to the right instead of the center." }, "getter": false, "setter": false, "reflect": false, "attribute": "sheet", "defaultValue": "false" }, "maxWidth": { "type": "string", "mutable": false, "complexType": { "original": "string", "resolved": "string", "references": {} }, "required": false, "optional": true, "docs": { "tags": [{ "name": "category", "text": "Visual" }, { "name": "since", "text": "v7.14.0" }], "text": "Define your own max width of the modal. Default is `45em`." }, "getter": false, "setter": false, "reflect": false, "attribute": "max-width", "defaultValue": "null" } }; } static get states() { return { "isClosing": {}, "isOpening": {}, "removeOverflow": {} }; } static get events() { return [{ "method": "modalToggle", "name": "modalToggle", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "This event is fired when the modal is toggled. {@since v7.14.0}" }, "complexType": { "original": "{ open: boolean }", "resolved": "{ open: boolean; }", "references": {} } }, { "method": "modalVisibility", "name": "modalVisibility", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [], "text": "This event is dispatched after the opening/closing animation has finished playing. {@since v7.22.0}" }, "complexType": { "original": "{ visible: boolean }", "resolved": "{ visible: boolean; }", "references": {} } }, { "method": "modalVisiblity", "name": "modalVisiblity", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [{ "name": "since", "text": "v7.14.0" }, { "name": "deprecated", "text": "v7.22.0. Use `modalVisibility` instead due to typo." }], "text": "This event is dispatched after the opening/closing animation has finished playing." }, "complexType": { "original": "{ visible: boolean }", "resolved": "{ visible: boolean; }", "references": {} } }, { "method": "close", "name": "close", "bubbles": true, "cancelable": true, "composed": true, "docs": { "tags": [{ "name": "deprecated", "text": "Use the new `modalToggle` event instead." }], "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" }, { "propName": "maxWidth", "methodName": "handleMaxWidth" }]; } static get listeners() { return [{ "name": "resize", "method": "handleOverflow", "target": "window", "capture": false, "passive": true }]; } }