@postnord/web-components
Version:
PostNord Web Components
553 lines (552 loc) • 21.7 kB
JavaScript
/*!
* 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
}];
}
}