@postnord/web-components
Version:
PostNord Web Components
178 lines (177 loc) • 7.08 kB
JavaScript
/*!
* 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