@nysds/nys-modal 1.13.0 → 1.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/nys-modal.js +53 -53
- package/dist/nys-modal.js.map +1 -1
- package/package.json +2 -2
package/dist/nys-modal.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { LitElement as g, unsafeCSS as b, html as f } from "lit";
|
|
2
|
-
import { property as
|
|
3
|
-
const w = ':host{--_nys-modal-width:
|
|
4
|
-
var
|
|
5
|
-
for (var
|
|
6
|
-
(
|
|
7
|
-
return
|
|
2
|
+
import { property as l, state as _ } from "lit/decorators.js";
|
|
3
|
+
const w = ':host{--_nys-modal-width: 439px;--_nys-modal-min-width: 320px;--_nys-modal-border-radius: var(--nys-radius-lg, 8px);--_nys-modal-border-color: var(--nys-color-neutral-200, #bec0c1);--_nys-modal-border-width: 1px;--_nys-modal-background-color: var(--nys-color-surface, #fff);--_nys-modal-margin: var(--nys-space-250, 20px);--_nys-modal-padding: var(--nys-space-300, 24px);--_nys-modal-gap: var(--nys-space-200, 16px);--_nys-modal-background-color--overlay: var( --nys-color-black-transparent-700, rgba(27, 27, 27, .7) );--_nys-modal-gap--header: var(--nys-space-100, 8px);--_nys-modal-gap--footer: var(--nys-space-250, 20px);--_nys-modal-font-size: var( --nys-font-size-body-md, var(--nys-font-size-md, 16px) );--_nys-modal-font-size--subheader: var( --nys-font-size-body-lg, var(--nys-font-size-lg, 18px) );--_nys-modal-font-weight--header: var(--nys-font-weight-bold, 700);--_nys-modal-font-weight--subheader: var(--nys-font-weight-semibold, 600);--_nys-modal-line-height: var(--nys-font-lineheight-ui-md, 24px);--_nys-modal-line-height--subheader: var(--nys-font-lineheight-body-lg, 28px);--_nys-modal-font-family: var( --nys-font-family-ui, var( --nys-font-family-sans, "Proxima Nova", "Helvetica Neue", "Helvetica", "Arial", sans-serif ) )}*{box-sizing:border-box}::slotted(p){margin:0!important}h2,p{flex:1;margin:0}.nys-modal-overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;display:flex;align-items:center;justify-content:center;z-index:1000;background:var(--_nys-modal-background-color--overlay)}.nys-modal{display:flex;flex-direction:column;margin:var(--_nys-modal-margin);padding:var(--_nys-modal-padding);gap:var(--_nys-modal-gap);width:var(--_nys-modal-width);border-radius:var(--_nys-modal-border-radius);border:var(--_nys-modal-border-width) solid var(--_nys-modal-border-color);font-family:var(--_nys-modal-font-family);font-size:var(--_nys-modal-font-size);line-height:var(--_nys-modal-line-height);background:var(--_nys-modal-background-color);position:relative;z-index:10000}.nys-modal_header{display:flex;flex-direction:column;align-items:flex-start;gap:var(--_nys-modal-gap--header)}.nys-modal_header p{font-size:var(--_nys-modal-font-size--subheader);font-weight:var(--_nys-modal-font-weight--subheader);line-height:var(--_nys-modal-line-height--subheader)}.nys-modal_header-inner{display:flex;align-items:center;width:100%;font-weight:var(--_nys-modal-font-weight--header)}.nys-modal_body{display:flex;flex-direction:column;align-items:flex-start}.nys-modal_body-inner{overflow:auto;width:100%;max-height:45vh}.nys-modal_body.hidden{display:none}.nys-modal_footer ::slotted(*){display:flex;flex-direction:column-reverse;justify-content:center;gap:var(--_nys-modal-gap--footer);align-self:stretch}.nys-modal_footer.hidden ::slotted(*){display:none}@media(min-width:480px){.nys-modal_body-inner{max-height:25vh}.nys-modal_footer ::slotted(*){flex-direction:row;justify-content:flex-end;align-items:center}.nys-modal{--_nys-modal-width: 439px}}@media(min-width:768px){.nys-modal{--_nys-modal-width: 600px}}@media(min-width:1024px){.nys-modal{--_nys-modal-width: 752px}}@media(min-width:1280px){.nys-modal{--_nys-modal-width: 840px}}';
|
|
4
|
+
var x = Object.defineProperty, i = (v, e, o, n) => {
|
|
5
|
+
for (var t = void 0, d = v.length - 1, y; d >= 0; d--)
|
|
6
|
+
(y = v[d]) && (t = y(e, o, t) || t);
|
|
7
|
+
return t && x(e, o, t), t;
|
|
8
8
|
};
|
|
9
|
-
let
|
|
9
|
+
let B = 0;
|
|
10
10
|
const p = class p extends g {
|
|
11
11
|
/**
|
|
12
12
|
* Lifecycle Methods
|
|
@@ -16,13 +16,13 @@ const p = class p extends g {
|
|
|
16
16
|
super(), this.id = "", this.heading = "", this.subheading = "", this.open = !1, this.mandatory = !1, this.width = "md", this._actionButtonSlot = null, this._prevFocusedElement = null, this._originalBodyOverflow = null, this.hasBodySlots = !1, this.hasActionSlots = !1;
|
|
17
17
|
}
|
|
18
18
|
connectedCallback() {
|
|
19
|
-
super.connectedCallback(), this.id || (this.id = `nys-
|
|
19
|
+
super.connectedCallback(), this.id || (this.id = `nys-modal-${Date.now()}-${B++}`), window.addEventListener("resize", () => this._updateSlottedButtonWidth()), window.addEventListener("keydown", (e) => this._handleKeydown(e));
|
|
20
20
|
}
|
|
21
21
|
disconnectedCallback() {
|
|
22
|
-
super.disconnectedCallback(), this._restoreBodyScroll(), window.removeEventListener("keydown", (
|
|
22
|
+
super.disconnectedCallback(), this._restoreBodyScroll(), window.removeEventListener("keydown", (e) => this._handleKeydown(e));
|
|
23
23
|
}
|
|
24
|
-
async updated(
|
|
25
|
-
|
|
24
|
+
async updated(e) {
|
|
25
|
+
e.has("open") && (this.open ? (this._hideBodyScroll(), this._dispatchOpenEvent(), await this.updateComplete, this._savePrevFocused(), this._focusOnModal(), this._updateDismissAria()) : (this._restorePrevFocused(), this._restoreBodyScroll(), this._dispatchCloseEvent(), this._updateDismissAria()));
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
28
|
* Functions
|
|
@@ -41,9 +41,9 @@ const p = class p extends g {
|
|
|
41
41
|
this.shadowRoot?.querySelector(".nys-modal")?.focus();
|
|
42
42
|
}
|
|
43
43
|
async _restorePrevFocused() {
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
46
|
-
const n = await
|
|
44
|
+
const e = this._prevFocusedElement;
|
|
45
|
+
if (e && e.tagName.toLowerCase() === "nys-button") {
|
|
46
|
+
const n = await e.getButtonElement();
|
|
47
47
|
if (n) {
|
|
48
48
|
n.focus();
|
|
49
49
|
return;
|
|
@@ -54,28 +54,28 @@ const p = class p extends g {
|
|
|
54
54
|
}
|
|
55
55
|
// Check if the slot contains stuff (aka user add texts & action buttons), and render visibility accordingly
|
|
56
56
|
async _handleBodySlotChange() {
|
|
57
|
-
const
|
|
58
|
-
|
|
57
|
+
const e = this.shadowRoot?.querySelector("slot");
|
|
58
|
+
e && (this.hasBodySlots = e.assignedNodes({ flatten: !0 }).some(
|
|
59
59
|
(o) => o.nodeType === Node.ELEMENT_NODE || o.textContent?.trim()
|
|
60
60
|
));
|
|
61
61
|
}
|
|
62
62
|
// Determines whether we hide the action buttons slot container based on if user put in action buttons
|
|
63
63
|
async _handleActionSlotChange() {
|
|
64
|
-
const
|
|
64
|
+
const e = this.shadowRoot?.querySelector(
|
|
65
65
|
'slot[name="actions"]'
|
|
66
66
|
);
|
|
67
|
-
|
|
67
|
+
e && (this.hasActionSlots = e.assignedNodes({ flatten: !0 }).some(
|
|
68
68
|
(o) => o.nodeType === Node.ELEMENT_NODE || o.textContent?.trim()
|
|
69
|
-
), this._actionButtonSlot =
|
|
69
|
+
), this._actionButtonSlot = e, this._updateSlottedButtonWidth());
|
|
70
70
|
}
|
|
71
71
|
// Design has it that the slotted action buttons should be fullWidth and display:column direction for mobile view.
|
|
72
72
|
// Therefore, we need to account for mobile size and screen resizes
|
|
73
73
|
_updateSlottedButtonWidth() {
|
|
74
74
|
if (!this._actionButtonSlot) return;
|
|
75
|
-
const
|
|
75
|
+
const e = window.innerWidth <= 480;
|
|
76
76
|
this._actionButtonSlot.assignedElements().forEach((o) => {
|
|
77
77
|
o.querySelectorAll("nys-button").forEach((n) => {
|
|
78
|
-
|
|
78
|
+
e ? n?.setAttribute("fullWidth", "") : n?.removeAttribute("fullWidth");
|
|
79
79
|
});
|
|
80
80
|
});
|
|
81
81
|
}
|
|
@@ -98,50 +98,50 @@ const p = class p extends g {
|
|
|
98
98
|
);
|
|
99
99
|
}
|
|
100
100
|
_getAriaDescribedBy() {
|
|
101
|
-
const
|
|
102
|
-
return this.subheading &&
|
|
101
|
+
const e = [];
|
|
102
|
+
return this.subheading && e.push(`${this.id}-subheading`), this.hasBodySlots && e.push(`${this.id}-desc`), e.join(" ");
|
|
103
103
|
}
|
|
104
104
|
/**
|
|
105
105
|
* This exist to prevent the VO for dismiss button from announcing itself between the heading & subheading/slot content.
|
|
106
106
|
* We add the "Close this window" ariaLabel after the initial VO is done
|
|
107
107
|
*/
|
|
108
108
|
_updateDismissAria() {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
const e = this.shadowRoot?.querySelector("nys-button");
|
|
110
|
+
e && (e.setAttribute("ariaLabel", " "), this.open && setTimeout(() => {
|
|
111
|
+
e.setAttribute("ariaLabel", "Close this window");
|
|
112
112
|
}, 100));
|
|
113
113
|
}
|
|
114
114
|
/**
|
|
115
115
|
* Event Handlers
|
|
116
116
|
* --------------------------------------------------------------------------
|
|
117
117
|
*/
|
|
118
|
-
async _handleKeydown(
|
|
119
|
-
if (this.open && (
|
|
118
|
+
async _handleKeydown(e) {
|
|
119
|
+
if (this.open && (e.key === "Escape" && !this.mandatory && (e.preventDefault(), this._closeModal()), e.key === "Tab")) {
|
|
120
120
|
const o = this.shadowRoot?.querySelector(".nys-modal");
|
|
121
121
|
if (!o) return;
|
|
122
|
-
const n = 'a[href], area[href], button:not([disabled]), details, iframe, object, input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [contentEditable="true"], [tabindex]:not([tabindex^="-"])',
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
for (const r of
|
|
126
|
-
const
|
|
127
|
-
for (const a of
|
|
128
|
-
a instanceof HTMLElement && a.matches(n) &&
|
|
129
|
-
(
|
|
130
|
-
|
|
122
|
+
const n = 'a[href], area[href], button:not([disabled]), details, iframe, object, input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [contentEditable="true"], [tabindex]:not([tabindex^="-"])', t = [], d = o.querySelector("nys-button");
|
|
123
|
+
d && t.push(d);
|
|
124
|
+
const y = Array.from(o.querySelectorAll("slot"));
|
|
125
|
+
for (const r of y) {
|
|
126
|
+
const c = r.assignedElements({ flatten: !0 });
|
|
127
|
+
for (const a of c)
|
|
128
|
+
a instanceof HTMLElement && a.matches(n) && t.push(a), a.querySelectorAll("nys-button").forEach(
|
|
129
|
+
(m) => {
|
|
130
|
+
t.push(m);
|
|
131
131
|
}
|
|
132
132
|
);
|
|
133
133
|
}
|
|
134
|
-
if (
|
|
135
|
-
const r =
|
|
136
|
-
let a = document.activeElement,
|
|
137
|
-
if (
|
|
138
|
-
|
|
139
|
-
let h =
|
|
140
|
-
h < 0 && (h =
|
|
141
|
-
const
|
|
142
|
-
|
|
134
|
+
if (t.length > 0) {
|
|
135
|
+
const r = t[0], c = t[t.length - 1];
|
|
136
|
+
let a = document.activeElement, m = t.indexOf(a);
|
|
137
|
+
if (e.shiftKey) {
|
|
138
|
+
e.preventDefault();
|
|
139
|
+
let h = m - 1;
|
|
140
|
+
h < 0 && (h = t.length - 1);
|
|
141
|
+
const u = t[h];
|
|
142
|
+
t[h].tagName.toLowerCase() === "nys-button" ? (await u.getButtonElement())?.focus() : u.focus();
|
|
143
143
|
} else
|
|
144
|
-
a ===
|
|
144
|
+
a === c && (e.preventDefault(), r.tagName.toLowerCase() === "nys-button" ? (await r.getButtonElement())?.focus() : r.focus());
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
}
|
|
@@ -195,22 +195,22 @@ const p = class p extends g {
|
|
|
195
195
|
p.styles = b(w);
|
|
196
196
|
let s = p;
|
|
197
197
|
i([
|
|
198
|
-
|
|
198
|
+
l({ type: String, reflect: !0 })
|
|
199
199
|
], s.prototype, "id");
|
|
200
200
|
i([
|
|
201
|
-
|
|
201
|
+
l({ type: String })
|
|
202
202
|
], s.prototype, "heading");
|
|
203
203
|
i([
|
|
204
|
-
|
|
204
|
+
l({ type: String })
|
|
205
205
|
], s.prototype, "subheading");
|
|
206
206
|
i([
|
|
207
|
-
|
|
207
|
+
l({ type: Boolean, reflect: !0 })
|
|
208
208
|
], s.prototype, "open");
|
|
209
209
|
i([
|
|
210
|
-
|
|
210
|
+
l({ type: Boolean, reflect: !0 })
|
|
211
211
|
], s.prototype, "mandatory");
|
|
212
212
|
i([
|
|
213
|
-
|
|
213
|
+
l({ type: String, reflect: !0 })
|
|
214
214
|
], s.prototype, "width");
|
|
215
215
|
i([
|
|
216
216
|
_()
|
package/dist/nys-modal.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nys-modal.js","sources":["../src/nys-modal.ts"],"sourcesContent":["import { LitElement, html, unsafeCSS } from \"lit\";\nimport { property, state } from \"lit/decorators.js\";\n// @ts-ignore: SCSS module imported via bundler as inline\nimport styles from \"./nys-modal.scss?inline\";\n\nlet componentIdCounter = 0;\n\n/**\n * `<nys-modal>` renders an accessible modal dialog.\n *\n * Supports headings, optional subheading, body content, and action buttons.\n * Manages focus trapping, escape key handling, and body scroll locking.\n *\n * @slot - Modal body content\n * @slot actions - Action buttons shown in the footer\n *\n * @fires nys-open - Emitted when the modal opens\n * @fires nys-close - Emitted when the modal closes\n */\n\nexport class NysModal extends LitElement {\n static styles = unsafeCSS(styles);\n\n @property({ type: String, reflect: true }) id = \"\";\n @property({ type: String }) heading = \"\";\n @property({ type: String }) subheading = \"\";\n @property({ type: Boolean, reflect: true }) open = false;\n @property({ type: Boolean, reflect: true }) mandatory = false;\n @property({ type: String, reflect: true }) width: \"sm\" | \"md\" | \"lg\" = \"md\";\n\n private _actionButtonSlot: HTMLSlotElement | null = null; // cache action button slots (if given) so we can manipulate their widths for mobile vs desktop\n private _prevFocusedElement: HTMLElement | null = null;\n private _originalBodyOverflow: string | null = null;\n\n // Track slot contents to control what HTML is rendered\n @state() private hasBodySlots = false;\n @state() private hasActionSlots = false;\n\n /**\n * Lifecycle Methods\n * --------------------------------------------------------------------------\n */\n\n constructor() {\n super();\n }\n\n connectedCallback() {\n super.connectedCallback();\n if (!this.id) {\n this.id = `nys-{{componentName}}-${Date.now()}-${componentIdCounter++}`;\n }\n window.addEventListener(\"resize\", () => this._updateSlottedButtonWidth());\n window.addEventListener(\"keydown\", (e) => this._handleKeydown(e));\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n this._restoreBodyScroll(); // make sure scroll is restored when modal is removed\n window.removeEventListener(\"keydown\", (e) => this._handleKeydown(e));\n }\n\n async updated(changeProps: Map<string, any>) {\n // Hide main body's scroll bar if modal is open/active\n if (changeProps.has(\"open\")) {\n if (this.open) {\n this._hideBodyScroll();\n this._dispatchOpenEvent();\n await this.updateComplete;\n this._savePrevFocused();\n this._focusOnModal();\n this._updateDismissAria();\n } else {\n this._restorePrevFocused();\n this._restoreBodyScroll();\n this._dispatchCloseEvent();\n this._updateDismissAria();\n }\n }\n }\n\n /**\n * Functions\n * --------------------------------------------------------------------------\n */\n\n private _hideBodyScroll() {\n if (this._originalBodyOverflow === null) {\n this._originalBodyOverflow = document.body.style.overflow;\n }\n document.body.style.overflow = \"hidden\";\n }\n\n private _restoreBodyScroll() {\n if (this._originalBodyOverflow !== null) {\n document.body.style.overflow = this._originalBodyOverflow;\n this._originalBodyOverflow = null;\n }\n }\n\n private _savePrevFocused() {\n this._prevFocusedElement = document.activeElement as HTMLElement;\n }\n\n private _focusOnModal() {\n const modal = this.shadowRoot?.querySelector<HTMLElement>(\".nys-modal\");\n modal?.focus();\n }\n\n private async _restorePrevFocused() {\n const prev = this._prevFocusedElement;\n\n if (prev && prev.tagName.toLowerCase() === \"nys-button\") {\n const nysButton = prev as HTMLElement & {\n getButtonElement: () => Promise<HTMLButtonElement>;\n };\n const innerBtn = await nysButton.getButtonElement();\n if (innerBtn) {\n innerBtn.focus();\n return;\n }\n } else {\n this._prevFocusedElement?.focus();\n }\n this._prevFocusedElement = null;\n }\n\n // Check if the slot contains stuff (aka user add texts & action buttons), and render visibility accordingly\n private async _handleBodySlotChange() {\n const slot = this.shadowRoot?.querySelector<HTMLSlotElement>(\"slot\");\n if (!slot) return;\n this.hasBodySlots = slot\n .assignedNodes({ flatten: true })\n .some(\n (node) =>\n node.nodeType === Node.ELEMENT_NODE || node.textContent?.trim(),\n );\n }\n\n // Determines whether we hide the action buttons slot container based on if user put in action buttons\n private async _handleActionSlotChange() {\n const slot = this.shadowRoot?.querySelector<HTMLSlotElement>(\n 'slot[name=\"actions\"]',\n );\n if (!slot) return;\n this.hasActionSlots = slot\n .assignedNodes({ flatten: true })\n .some(\n (node) =>\n node.nodeType === Node.ELEMENT_NODE || node.textContent?.trim(),\n );\n\n // Cached the action button slot container so we can use it continuously for _updateSlottedButtonWidth() during screen resize\n this._actionButtonSlot = slot;\n // Update button widths immediately\n this._updateSlottedButtonWidth();\n }\n\n // Design has it that the slotted action buttons should be fullWidth and display:column direction for mobile view.\n // Therefore, we need to account for mobile size and screen resizes\n private _updateSlottedButtonWidth() {\n if (!this._actionButtonSlot) return; // use the cached variable\n const isMobile = window.innerWidth <= 480;\n\n this._actionButtonSlot.assignedElements().forEach((el) => {\n el.querySelectorAll(\"nys-button\").forEach((btn) => {\n if (isMobile) {\n btn?.setAttribute(\"fullWidth\", \"\");\n } else {\n btn?.removeAttribute(\"fullWidth\");\n }\n });\n });\n }\n\n private _dispatchOpenEvent() {\n this.dispatchEvent(\n new CustomEvent(\"nys-open\", {\n detail: { id: this.id },\n bubbles: true,\n composed: true,\n }),\n );\n }\n\n private _dispatchCloseEvent() {\n this.dispatchEvent(\n new CustomEvent(\"nys-close\", {\n detail: { id: this.id },\n bubbles: true,\n composed: true,\n }),\n );\n }\n\n private _getAriaDescribedBy() {\n // Handling what aria-describedby needs to announce in VO based on if the subheading or slot contents exists\n const ariaDescriptions: string[] = [];\n if (this.subheading) {\n ariaDescriptions.push(`${this.id}-subheading`);\n }\n if (this.hasBodySlots) {\n ariaDescriptions.push(`${this.id}-desc`);\n }\n return ariaDescriptions.join(\" \");\n }\n\n /**\n * This exist to prevent the VO for dismiss button from announcing itself between the heading & subheading/slot content.\n * We add the \"Close this window\" ariaLabel after the initial VO is done\n */\n private _updateDismissAria() {\n const dismissBtn = this.shadowRoot?.querySelector(\"nys-button\");\n if (!dismissBtn) return;\n\n // Hide from VO initially\n dismissBtn.setAttribute(\"ariaLabel\", \" \");\n\n if (this.open) {\n // After focus is moved into modal, update label\n setTimeout(() => {\n dismissBtn.setAttribute(\"ariaLabel\", \"Close this window\");\n }, 100);\n }\n }\n\n /**\n * Event Handlers\n * --------------------------------------------------------------------------\n */\n\n private async _handleKeydown(e: KeyboardEvent) {\n if (!this.open) return;\n\n // Exit the modal for \"escape\" key\n if (e.key === \"Escape\" && !this.mandatory) {\n e.preventDefault();\n this._closeModal();\n }\n\n // Trap focus to be within the modal only\n if (e.key === \"Tab\") {\n const modal = this.shadowRoot?.querySelector(\".nys-modal\");\n if (!modal) return;\n\n // Gather all elements from slots + dismissible btn\n const knownFocusableElements =\n 'a[href], area[href], button:not([disabled]), details, iframe, object, input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [contentEditable=\"true\"], [tabindex]:not([tabindex^=\"-\"])';\n const focusableElements: HTMLElement[] = [];\n const dismissBtn = modal.querySelector(\"nys-button\") as HTMLElement;\n\n if (dismissBtn) {\n focusableElements.push(dismissBtn);\n }\n\n // Gather from slot elements to store the focusable elements in focusableElements for focus trapping\n const slotElements = Array.from(modal.querySelectorAll(\"slot\"));\n for (const slot of slotElements) {\n const assigned = slot.assignedElements({ flatten: true });\n for (const el of assigned) {\n if (el instanceof HTMLElement && el.matches(knownFocusableElements)) {\n focusableElements.push(el);\n }\n // also account for the action slot container that has nys-buttons\n el.querySelectorAll<HTMLElement>(\"nys-button\").forEach(\n (actionBtn) => {\n focusableElements.push(actionBtn);\n },\n );\n }\n }\n\n if (focusableElements.length > 0) {\n // Laying out the starting (i.e. dismiss btn) and ending elements for looping focus elements\n const firstFocusableEl = focusableElements[0];\n const lastFocusableEl = focusableElements[focusableElements.length - 1];\n let active = document.activeElement as HTMLElement | null;\n let activeIndex = focusableElements.indexOf(active as HTMLElement);\n\n /**\n * Move focus backward when Shift+Tab is pressed.\n * Focus goes to the previous element in focusableElements.\n * If currently at the first element, wrap around to the last element.\n * For <nys-button>, focus the internal button. For other elements, focus directly.\n */\n if (e.shiftKey) {\n e.preventDefault();\n\n let prevIndex = activeIndex - 1;\n if (prevIndex < 0) {\n prevIndex = focusableElements.length - 1; // wrap back to lastFocusableEl\n }\n\n const prevElement = focusableElements[prevIndex];\n const isNysButton =\n focusableElements[prevIndex].tagName.toLowerCase() === \"nys-button\";\n\n // When users slot in \"nys-button\", we have to call focus for the native button within its shadowDOM.\n // Hence the condition statement below\n if (isNysButton) {\n const nysButton = prevElement as HTMLElement & {\n getButtonElement: () => Promise<HTMLButtonElement>;\n };\n const innerBtn = await nysButton.getButtonElement();\n innerBtn?.focus();\n } else {\n prevElement.focus();\n }\n } else {\n // Tab (go back to first focusable element if we're at last)\n if (active === lastFocusableEl) {\n e.preventDefault();\n if (firstFocusableEl.tagName.toLowerCase() === \"nys-button\") {\n const nysButton = firstFocusableEl as HTMLElement & {\n getButtonElement: () => Promise<HTMLButtonElement>;\n };\n const innerBtn = await nysButton.getButtonElement();\n innerBtn?.focus();\n } else {\n firstFocusableEl.focus();\n }\n }\n }\n }\n }\n }\n\n private _closeModal() {\n this.open = false;\n this._dispatchCloseEvent();\n }\n\n render() {\n return this.open\n ? html`<div\n class=\"nys-modal-overlay\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"${this.id}-heading\"\n aria-describedby=\"${this._getAriaDescribedBy()}\"\n >\n <div class=\"nys-modal\" tabindex=\"-1\">\n <div class=\"nys-modal_header\">\n <div class=\"nys-modal_header-inner\">\n <h2 id=\"${this.id}-heading\">${this.heading}</h2>\n ${!this.mandatory\n ? html`<nys-button\n id=\"dismiss-modal\"\n circle\n icon=\"close\"\n variant=\"ghost\"\n @nys-click=${this._closeModal}\n ></nys-button>`\n : \"\"}\n </div>\n ${this.subheading\n ? html`<p id=\"${this.id}-subheading\">${this.subheading}</p>`\n : \"\"}\n </div>\n\n <div\n id=\"${this.id}-desc\"\n class=\"nys-modal_body ${!this.hasBodySlots ? \"hidden\" : \"\"}\"\n >\n <div class=\"nys-modal_body-inner\">\n <slot @slotchange=${this._handleBodySlotChange}></slot>\n </div>\n </div>\n\n <div\n class=\"nys-modal_footer ${!this.hasActionSlots ? \"hidden\" : \"\"}\"\n >\n <slot\n name=\"actions\"\n @slotchange=${this._handleActionSlotChange}\n ></slot>\n </div>\n </div>\n </div>`\n : \"\";\n }\n}\n\nif (!customElements.get(\"nys-modal\")) {\n customElements.define(\"nys-modal\", NysModal);\n}\n"],"names":["componentIdCounter","_NysModal","LitElement","e","changeProps","prev","innerBtn","slot","node","isMobile","el","btn","ariaDescriptions","dismissBtn","modal","knownFocusableElements","focusableElements","slotElements","assigned","actionBtn","firstFocusableEl","lastFocusableEl","active","activeIndex","prevIndex","prevElement","html","unsafeCSS","styles","NysModal","__decorateClass","property","state"],"mappings":";;;;;;;;AAKA,IAAIA,IAAqB;AAelB,MAAMC,IAAN,MAAMA,UAAiBC,EAAW;AAAA;AAAA;AAAA;AAAA;AAAA,EAuBvC,cAAc;AACZ,UAAA,GArByC,KAAA,KAAK,IACpB,KAAA,UAAU,IACV,KAAA,aAAa,IACG,KAAA,OAAO,IACP,KAAA,YAAY,IACb,KAAA,QAA4B,MAEvE,KAAQ,oBAA4C,MACpD,KAAQ,sBAA0C,MAClD,KAAQ,wBAAuC,MAGtC,KAAQ,eAAe,IACvB,KAAQ,iBAAiB;AAAA,EASlC;AAAA,EAEA,oBAAoB;AAClB,UAAM,kBAAA,GACD,KAAK,OACR,KAAK,KAAK,yBAAyB,KAAK,KAAK,IAAIF,GAAoB,KAEvE,OAAO,iBAAiB,UAAU,MAAM,KAAK,2BAA2B,GACxE,OAAO,iBAAiB,WAAW,CAACG,MAAM,KAAK,eAAeA,CAAC,CAAC;AAAA,EAClE;AAAA,EAEA,uBAAuB;AACrB,UAAM,qBAAA,GACN,KAAK,mBAAA,GACL,OAAO,oBAAoB,WAAW,CAACA,MAAM,KAAK,eAAeA,CAAC,CAAC;AAAA,EACrE;AAAA,EAEA,MAAM,QAAQC,GAA+B;AAE3C,IAAIA,EAAY,IAAI,MAAM,MACpB,KAAK,QACP,KAAK,gBAAA,GACL,KAAK,mBAAA,GACL,MAAM,KAAK,gBACX,KAAK,iBAAA,GACL,KAAK,cAAA,GACL,KAAK,mBAAA,MAEL,KAAK,oBAAA,GACL,KAAK,mBAAA,GACL,KAAK,oBAAA,GACL,KAAK,mBAAA;AAAA,EAGX;AAAA;AAAA;AAAA;AAAA;AAAA,EAOQ,kBAAkB;AACxB,IAAI,KAAK,0BAA0B,SACjC,KAAK,wBAAwB,SAAS,KAAK,MAAM,WAEnD,SAAS,KAAK,MAAM,WAAW;AAAA,EACjC;AAAA,EAEQ,qBAAqB;AAC3B,IAAI,KAAK,0BAA0B,SACjC,SAAS,KAAK,MAAM,WAAW,KAAK,uBACpC,KAAK,wBAAwB;AAAA,EAEjC;AAAA,EAEQ,mBAAmB;AACzB,SAAK,sBAAsB,SAAS;AAAA,EACtC;AAAA,EAEQ,gBAAgB;AAEtB,IADc,KAAK,YAAY,cAA2B,YAAY,GAC/D,MAAA;AAAA,EACT;AAAA,EAEA,MAAc,sBAAsB;AAClC,UAAMC,IAAO,KAAK;AAElB,QAAIA,KAAQA,EAAK,QAAQ,YAAA,MAAkB,cAAc;AAIvD,YAAMC,IAAW,MAHCD,EAGe,iBAAA;AACjC,UAAIC,GAAU;AACZ,QAAAA,EAAS,MAAA;AACT;AAAA,MACF;AAAA,IACF;AACE,WAAK,qBAAqB,MAAA;AAE5B,SAAK,sBAAsB;AAAA,EAC7B;AAAA;AAAA,EAGA,MAAc,wBAAwB;AACpC,UAAMC,IAAO,KAAK,YAAY,cAA+B,MAAM;AACnE,IAAKA,MACL,KAAK,eAAeA,EACjB,cAAc,EAAE,SAAS,GAAA,CAAM,EAC/B;AAAA,MACC,CAACC,MACCA,EAAK,aAAa,KAAK,gBAAgBA,EAAK,aAAa,KAAA;AAAA,IAAK;AAAA,EAEtE;AAAA;AAAA,EAGA,MAAc,0BAA0B;AACtC,UAAMD,IAAO,KAAK,YAAY;AAAA,MAC5B;AAAA,IAAA;AAEF,IAAKA,MACL,KAAK,iBAAiBA,EACnB,cAAc,EAAE,SAAS,GAAA,CAAM,EAC/B;AAAA,MACC,CAACC,MACCA,EAAK,aAAa,KAAK,gBAAgBA,EAAK,aAAa,KAAA;AAAA,IAAK,GAIpE,KAAK,oBAAoBD,GAEzB,KAAK,0BAAA;AAAA,EACP;AAAA;AAAA;AAAA,EAIQ,4BAA4B;AAClC,QAAI,CAAC,KAAK,kBAAmB;AAC7B,UAAME,IAAW,OAAO,cAAc;AAEtC,SAAK,kBAAkB,iBAAA,EAAmB,QAAQ,CAACC,MAAO;AACxD,MAAAA,EAAG,iBAAiB,YAAY,EAAE,QAAQ,CAACC,MAAQ;AACjD,QAAIF,IACFE,GAAK,aAAa,aAAa,EAAE,IAEjCA,GAAK,gBAAgB,WAAW;AAAA,MAEpC,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA,EAEQ,qBAAqB;AAC3B,SAAK;AAAA,MACH,IAAI,YAAY,YAAY;AAAA,QAC1B,QAAQ,EAAE,IAAI,KAAK,GAAA;AAAA,QACnB,SAAS;AAAA,QACT,UAAU;AAAA,MAAA,CACX;AAAA,IAAA;AAAA,EAEL;AAAA,EAEQ,sBAAsB;AAC5B,SAAK;AAAA,MACH,IAAI,YAAY,aAAa;AAAA,QAC3B,QAAQ,EAAE,IAAI,KAAK,GAAA;AAAA,QACnB,SAAS;AAAA,QACT,UAAU;AAAA,MAAA,CACX;AAAA,IAAA;AAAA,EAEL;AAAA,EAEQ,sBAAsB;AAE5B,UAAMC,IAA6B,CAAA;AACnC,WAAI,KAAK,cACPA,EAAiB,KAAK,GAAG,KAAK,EAAE,aAAa,GAE3C,KAAK,gBACPA,EAAiB,KAAK,GAAG,KAAK,EAAE,OAAO,GAElCA,EAAiB,KAAK,GAAG;AAAA,EAClC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,qBAAqB;AAC3B,UAAMC,IAAa,KAAK,YAAY,cAAc,YAAY;AAC9D,IAAKA,MAGLA,EAAW,aAAa,aAAa,GAAG,GAEpC,KAAK,QAEP,WAAW,MAAM;AACf,MAAAA,EAAW,aAAa,aAAa,mBAAmB;AAAA,IAC1D,GAAG,GAAG;AAAA,EAEV;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAc,eAAeV,GAAkB;AAC7C,QAAK,KAAK,SAGNA,EAAE,QAAQ,YAAY,CAAC,KAAK,cAC9BA,EAAE,eAAA,GACF,KAAK,YAAA,IAIHA,EAAE,QAAQ,QAAO;AACnB,YAAMW,IAAQ,KAAK,YAAY,cAAc,YAAY;AACzD,UAAI,CAACA,EAAO;AAGZ,YAAMC,IACJ,4MACIC,IAAmC,CAAA,GACnCH,IAAaC,EAAM,cAAc,YAAY;AAEnD,MAAID,KACFG,EAAkB,KAAKH,CAAU;AAInC,YAAMI,IAAe,MAAM,KAAKH,EAAM,iBAAiB,MAAM,CAAC;AAC9D,iBAAWP,KAAQU,GAAc;AAC/B,cAAMC,IAAWX,EAAK,iBAAiB,EAAE,SAAS,IAAM;AACxD,mBAAWG,KAAMQ;AACf,UAAIR,aAAc,eAAeA,EAAG,QAAQK,CAAsB,KAChEC,EAAkB,KAAKN,CAAE,GAG3BA,EAAG,iBAA8B,YAAY,EAAE;AAAA,YAC7C,CAACS,MAAc;AACb,cAAAH,EAAkB,KAAKG,CAAS;AAAA,YAClC;AAAA,UAAA;AAAA,MAGN;AAEA,UAAIH,EAAkB,SAAS,GAAG;AAEhC,cAAMI,IAAmBJ,EAAkB,CAAC,GACtCK,IAAkBL,EAAkBA,EAAkB,SAAS,CAAC;AACtE,YAAIM,IAAS,SAAS,eAClBC,IAAcP,EAAkB,QAAQM,CAAqB;AAQjE,YAAInB,EAAE,UAAU;AACd,UAAAA,EAAE,eAAA;AAEF,cAAIqB,IAAYD,IAAc;AAC9B,UAAIC,IAAY,MACdA,IAAYR,EAAkB,SAAS;AAGzC,gBAAMS,IAAcT,EAAkBQ,CAAS;AAM/C,UAJER,EAAkBQ,CAAS,EAAE,QAAQ,kBAAkB,gBAQtC,MAHCC,EAGe,iBAAA,IACvB,MAAA,IAEVA,EAAY,MAAA;AAAA,QAEhB;AAEE,UAAIH,MAAWD,MACblB,EAAE,eAAA,GACEiB,EAAiB,QAAQ,YAAA,MAAkB,gBAI5B,MAHCA,EAGe,iBAAA,IACvB,MAAA,IAEVA,EAAiB,MAAA;AAAA,MAIzB;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,cAAc;AACpB,SAAK,OAAO,IACZ,KAAK,oBAAA;AAAA,EACP;AAAA,EAEA,SAAS;AACP,WAAO,KAAK,OACRM;AAAA;AAAA;AAAA;AAAA,6BAIqB,KAAK,EAAE;AAAA,8BACN,KAAK,qBAAqB;AAAA;AAAA;AAAA;AAAA;AAAA,0BAK9B,KAAK,EAAE,aAAa,KAAK,OAAO;AAAA,kBACvC,KAAK,YAQJ,KAPAA;AAAA;AAAA;AAAA;AAAA;AAAA,mCAKe,KAAK,WAAW;AAAA,mCAE7B;AAAA;AAAA,gBAEN,KAAK,aACHA,WAAc,KAAK,EAAE,gBAAgB,KAAK,UAAU,SACpD,EAAE;AAAA;AAAA;AAAA;AAAA,oBAIA,KAAK,EAAE;AAAA,sCACY,KAAK,eAA0B,KAAX,QAAa;AAAA;AAAA;AAAA,oCAGpC,KAAK,qBAAqB;AAAA;AAAA;AAAA;AAAA;AAAA,wCAKrB,KAAK,iBAA4B,KAAX,QAAa;AAAA;AAAA;AAAA;AAAA,8BAI9C,KAAK,uBAAuB;AAAA;AAAA;AAAA;AAAA,kBAKlD;AAAA,EACN;AACF;AAxWEzB,EAAO,SAAS0B,EAAUC,CAAM;AAD3B,IAAMC,IAAN5B;AAGsC6B,EAAA;AAAA,EAA1CC,EAAS,EAAE,MAAM,QAAQ,SAAS,IAAM;AAAA,GAH9BF,EAGgC,WAAA,IAAA;AACfC,EAAA;AAAA,EAA3BC,EAAS,EAAE,MAAM,OAAA,CAAQ;AAAA,GAJfF,EAIiB,WAAA,SAAA;AACAC,EAAA;AAAA,EAA3BC,EAAS,EAAE,MAAM,OAAA,CAAQ;AAAA,GALfF,EAKiB,WAAA,YAAA;AACgBC,EAAA;AAAA,EAA3CC,EAAS,EAAE,MAAM,SAAS,SAAS,IAAM;AAAA,GAN/BF,EAMiC,WAAA,MAAA;AACAC,EAAA;AAAA,EAA3CC,EAAS,EAAE,MAAM,SAAS,SAAS,IAAM;AAAA,GAP/BF,EAOiC,WAAA,WAAA;AACDC,EAAA;AAAA,EAA1CC,EAAS,EAAE,MAAM,QAAQ,SAAS,IAAM;AAAA,GAR9BF,EAQgC,WAAA,OAAA;AAO1BC,EAAA;AAAA,EAAhBE,EAAA;AAAM,GAfIH,EAeM,WAAA,cAAA;AACAC,EAAA;AAAA,EAAhBE,EAAA;AAAM,GAhBIH,EAgBM,WAAA,gBAAA;AA2Vd,eAAe,IAAI,WAAW,KACjC,eAAe,OAAO,aAAaA,CAAQ;"}
|
|
1
|
+
{"version":3,"file":"nys-modal.js","sources":["../src/nys-modal.ts"],"sourcesContent":["import { LitElement, html, unsafeCSS } from \"lit\";\nimport { property, state } from \"lit/decorators.js\";\n// @ts-ignore: SCSS module imported via bundler as inline\nimport styles from \"./nys-modal.scss?inline\";\n\nlet componentIdCounter = 0;\n\n/**\n * An accessible modal dialog with focus trapping, keyboard navigation, and scroll management.\n *\n * Set `open` to show the modal. Content goes in the default slot; action buttons in the `actions` slot.\n * Dismisses via close button or Escape key unless `mandatory` is set. Focus returns to trigger on close.\n *\n * @summary Accessible modal dialog with focus trap, keyboard support, and action slots.\n * @element nys-modal\n *\n * @slot - Default slot for body content.\n * @slot actions - Action buttons displayed in footer. Buttons auto-resize on mobile.\n *\n * @fires nys-open - Fired when modal opens. Detail: `{id}`.\n * @fires nys-close - Fired when modal closes. Detail: `{id}`.\n *\n * @example Basic modal\n * ```html\n * <nys-modal id=\"confirm-modal\" heading=\"Confirm action\" open>\n * <p>Are you sure you want to proceed?</p>\n * <div slot=\"actions\">\n * <nys-button label=\"Cancel\" variant=\"outline\"></nys-button>\n * <nys-button label=\"Confirm\" variant=\"filled\"></nys-button>\n * </div>\n * </nys-modal>\n * ```\n */\n\nexport class NysModal extends LitElement {\n static styles = unsafeCSS(styles);\n\n /** Unique identifier. Auto-generated if not provided. */\n @property({ type: String, reflect: true }) id = \"\";\n\n /** Modal heading text. Required for accessibility. */\n @property({ type: String }) heading = \"\";\n\n /** Secondary heading below the main heading. */\n @property({ type: String }) subheading = \"\";\n\n /** Controls modal visibility. Set to `true` to show. */\n @property({ type: Boolean, reflect: true }) open = false;\n\n /** Prevents dismissal via close button or Escape key. User must take an action. */\n @property({ type: Boolean, reflect: true }) mandatory = false;\n\n /**\n * Modal width: `sm` (400px), `md` (600px), or `lg` (800px).\n * @default \"md\"\n */\n @property({ type: String, reflect: true }) width: \"sm\" | \"md\" | \"lg\" = \"md\";\n\n private _actionButtonSlot: HTMLSlotElement | null = null; // cache action button slots (if given) so we can manipulate their widths for mobile vs desktop\n private _prevFocusedElement: HTMLElement | null = null;\n private _originalBodyOverflow: string | null = null;\n\n // Track slot contents to control what HTML is rendered\n @state() private hasBodySlots = false;\n @state() private hasActionSlots = false;\n\n /**\n * Lifecycle Methods\n * --------------------------------------------------------------------------\n */\n\n constructor() {\n super();\n }\n\n connectedCallback() {\n super.connectedCallback();\n if (!this.id) {\n this.id = `nys-modal-${Date.now()}-${componentIdCounter++}`;\n }\n window.addEventListener(\"resize\", () => this._updateSlottedButtonWidth());\n window.addEventListener(\"keydown\", (e) => this._handleKeydown(e));\n }\n\n disconnectedCallback() {\n super.disconnectedCallback();\n this._restoreBodyScroll(); // make sure scroll is restored when modal is removed\n window.removeEventListener(\"keydown\", (e) => this._handleKeydown(e));\n }\n\n async updated(changeProps: Map<string, any>) {\n // Hide main body's scroll bar if modal is open/active\n if (changeProps.has(\"open\")) {\n if (this.open) {\n this._hideBodyScroll();\n this._dispatchOpenEvent();\n await this.updateComplete;\n this._savePrevFocused();\n this._focusOnModal();\n this._updateDismissAria();\n } else {\n this._restorePrevFocused();\n this._restoreBodyScroll();\n this._dispatchCloseEvent();\n this._updateDismissAria();\n }\n }\n }\n\n /**\n * Functions\n * --------------------------------------------------------------------------\n */\n\n private _hideBodyScroll() {\n if (this._originalBodyOverflow === null) {\n this._originalBodyOverflow = document.body.style.overflow;\n }\n document.body.style.overflow = \"hidden\";\n }\n\n private _restoreBodyScroll() {\n if (this._originalBodyOverflow !== null) {\n document.body.style.overflow = this._originalBodyOverflow;\n this._originalBodyOverflow = null;\n }\n }\n\n private _savePrevFocused() {\n this._prevFocusedElement = document.activeElement as HTMLElement;\n }\n\n private _focusOnModal() {\n const modal = this.shadowRoot?.querySelector<HTMLElement>(\".nys-modal\");\n modal?.focus();\n }\n\n private async _restorePrevFocused() {\n const prev = this._prevFocusedElement;\n\n if (prev && prev.tagName.toLowerCase() === \"nys-button\") {\n const nysButton = prev as HTMLElement & {\n getButtonElement: () => Promise<HTMLButtonElement>;\n };\n const innerBtn = await nysButton.getButtonElement();\n if (innerBtn) {\n innerBtn.focus();\n return;\n }\n } else {\n this._prevFocusedElement?.focus();\n }\n this._prevFocusedElement = null;\n }\n\n // Check if the slot contains stuff (aka user add texts & action buttons), and render visibility accordingly\n private async _handleBodySlotChange() {\n const slot = this.shadowRoot?.querySelector<HTMLSlotElement>(\"slot\");\n if (!slot) return;\n this.hasBodySlots = slot\n .assignedNodes({ flatten: true })\n .some(\n (node) =>\n node.nodeType === Node.ELEMENT_NODE || node.textContent?.trim(),\n );\n }\n\n // Determines whether we hide the action buttons slot container based on if user put in action buttons\n private async _handleActionSlotChange() {\n const slot = this.shadowRoot?.querySelector<HTMLSlotElement>(\n 'slot[name=\"actions\"]',\n );\n if (!slot) return;\n this.hasActionSlots = slot\n .assignedNodes({ flatten: true })\n .some(\n (node) =>\n node.nodeType === Node.ELEMENT_NODE || node.textContent?.trim(),\n );\n\n // Cached the action button slot container so we can use it continuously for _updateSlottedButtonWidth() during screen resize\n this._actionButtonSlot = slot;\n // Update button widths immediately\n this._updateSlottedButtonWidth();\n }\n\n // Design has it that the slotted action buttons should be fullWidth and display:column direction for mobile view.\n // Therefore, we need to account for mobile size and screen resizes\n private _updateSlottedButtonWidth() {\n if (!this._actionButtonSlot) return; // use the cached variable\n const isMobile = window.innerWidth <= 480;\n\n this._actionButtonSlot.assignedElements().forEach((el) => {\n el.querySelectorAll(\"nys-button\").forEach((btn) => {\n if (isMobile) {\n btn?.setAttribute(\"fullWidth\", \"\");\n } else {\n btn?.removeAttribute(\"fullWidth\");\n }\n });\n });\n }\n\n private _dispatchOpenEvent() {\n this.dispatchEvent(\n new CustomEvent(\"nys-open\", {\n detail: { id: this.id },\n bubbles: true,\n composed: true,\n }),\n );\n }\n\n private _dispatchCloseEvent() {\n this.dispatchEvent(\n new CustomEvent(\"nys-close\", {\n detail: { id: this.id },\n bubbles: true,\n composed: true,\n }),\n );\n }\n\n private _getAriaDescribedBy() {\n // Handling what aria-describedby needs to announce in VO based on if the subheading or slot contents exists\n const ariaDescriptions: string[] = [];\n if (this.subheading) {\n ariaDescriptions.push(`${this.id}-subheading`);\n }\n if (this.hasBodySlots) {\n ariaDescriptions.push(`${this.id}-desc`);\n }\n return ariaDescriptions.join(\" \");\n }\n\n /**\n * This exist to prevent the VO for dismiss button from announcing itself between the heading & subheading/slot content.\n * We add the \"Close this window\" ariaLabel after the initial VO is done\n */\n private _updateDismissAria() {\n const dismissBtn = this.shadowRoot?.querySelector(\"nys-button\");\n if (!dismissBtn) return;\n\n // Hide from VO initially\n dismissBtn.setAttribute(\"ariaLabel\", \" \");\n\n if (this.open) {\n // After focus is moved into modal, update label\n setTimeout(() => {\n dismissBtn.setAttribute(\"ariaLabel\", \"Close this window\");\n }, 100);\n }\n }\n\n /**\n * Event Handlers\n * --------------------------------------------------------------------------\n */\n\n private async _handleKeydown(e: KeyboardEvent) {\n if (!this.open) return;\n\n // Exit the modal for \"escape\" key\n if (e.key === \"Escape\" && !this.mandatory) {\n e.preventDefault();\n this._closeModal();\n }\n\n // Trap focus to be within the modal only\n if (e.key === \"Tab\") {\n const modal = this.shadowRoot?.querySelector(\".nys-modal\");\n if (!modal) return;\n\n // Gather all elements from slots + dismissible btn\n const knownFocusableElements =\n 'a[href], area[href], button:not([disabled]), details, iframe, object, input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [contentEditable=\"true\"], [tabindex]:not([tabindex^=\"-\"])';\n const focusableElements: HTMLElement[] = [];\n const dismissBtn = modal.querySelector(\"nys-button\") as HTMLElement;\n\n if (dismissBtn) {\n focusableElements.push(dismissBtn);\n }\n\n // Gather from slot elements to store the focusable elements in focusableElements for focus trapping\n const slotElements = Array.from(modal.querySelectorAll(\"slot\"));\n for (const slot of slotElements) {\n const assigned = slot.assignedElements({ flatten: true });\n for (const el of assigned) {\n if (el instanceof HTMLElement && el.matches(knownFocusableElements)) {\n focusableElements.push(el);\n }\n // also account for the action slot container that has nys-buttons\n el.querySelectorAll<HTMLElement>(\"nys-button\").forEach(\n (actionBtn) => {\n focusableElements.push(actionBtn);\n },\n );\n }\n }\n\n if (focusableElements.length > 0) {\n // Laying out the starting (i.e. dismiss btn) and ending elements for looping focus elements\n const firstFocusableEl = focusableElements[0];\n const lastFocusableEl = focusableElements[focusableElements.length - 1];\n let active = document.activeElement as HTMLElement | null;\n let activeIndex = focusableElements.indexOf(active as HTMLElement);\n\n /**\n * Move focus backward when Shift+Tab is pressed.\n * Focus goes to the previous element in focusableElements.\n * If currently at the first element, wrap around to the last element.\n * For <nys-button>, focus the internal button. For other elements, focus directly.\n */\n if (e.shiftKey) {\n e.preventDefault();\n\n let prevIndex = activeIndex - 1;\n if (prevIndex < 0) {\n prevIndex = focusableElements.length - 1; // wrap back to lastFocusableEl\n }\n\n const prevElement = focusableElements[prevIndex];\n const isNysButton =\n focusableElements[prevIndex].tagName.toLowerCase() === \"nys-button\";\n\n // When users slot in \"nys-button\", we have to call focus for the native button within its shadowDOM.\n // Hence the condition statement below\n if (isNysButton) {\n const nysButton = prevElement as HTMLElement & {\n getButtonElement: () => Promise<HTMLButtonElement>;\n };\n const innerBtn = await nysButton.getButtonElement();\n innerBtn?.focus();\n } else {\n prevElement.focus();\n }\n } else {\n // Tab (go back to first focusable element if we're at last)\n if (active === lastFocusableEl) {\n e.preventDefault();\n if (firstFocusableEl.tagName.toLowerCase() === \"nys-button\") {\n const nysButton = firstFocusableEl as HTMLElement & {\n getButtonElement: () => Promise<HTMLButtonElement>;\n };\n const innerBtn = await nysButton.getButtonElement();\n innerBtn?.focus();\n } else {\n firstFocusableEl.focus();\n }\n }\n }\n }\n }\n }\n\n private _closeModal() {\n this.open = false;\n this._dispatchCloseEvent();\n }\n\n render() {\n return this.open\n ? html`<div\n class=\"nys-modal-overlay\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"${this.id}-heading\"\n aria-describedby=\"${this._getAriaDescribedBy()}\"\n >\n <div class=\"nys-modal\" tabindex=\"-1\">\n <div class=\"nys-modal_header\">\n <div class=\"nys-modal_header-inner\">\n <h2 id=\"${this.id}-heading\">${this.heading}</h2>\n ${!this.mandatory\n ? html`<nys-button\n id=\"dismiss-modal\"\n circle\n icon=\"close\"\n variant=\"ghost\"\n @nys-click=${this._closeModal}\n ></nys-button>`\n : \"\"}\n </div>\n ${this.subheading\n ? html`<p id=\"${this.id}-subheading\">${this.subheading}</p>`\n : \"\"}\n </div>\n\n <div\n id=\"${this.id}-desc\"\n class=\"nys-modal_body ${!this.hasBodySlots ? \"hidden\" : \"\"}\"\n >\n <div class=\"nys-modal_body-inner\">\n <slot @slotchange=${this._handleBodySlotChange}></slot>\n </div>\n </div>\n\n <div\n class=\"nys-modal_footer ${!this.hasActionSlots ? \"hidden\" : \"\"}\"\n >\n <slot\n name=\"actions\"\n @slotchange=${this._handleActionSlotChange}\n ></slot>\n </div>\n </div>\n </div>`\n : \"\";\n }\n}\n\nif (!customElements.get(\"nys-modal\")) {\n customElements.define(\"nys-modal\", NysModal);\n}\n"],"names":["componentIdCounter","_NysModal","LitElement","changeProps","prev","innerBtn","slot","node","isMobile","el","btn","ariaDescriptions","dismissBtn","modal","knownFocusableElements","focusableElements","slotElements","assigned","actionBtn","firstFocusableEl","lastFocusableEl","active","activeIndex","prevIndex","prevElement","html","unsafeCSS","styles","NysModal","__decorateClass","property","state"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nysds/nys-modal",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.1",
|
|
4
4
|
"description": "The Modal component from the NYS Design System.",
|
|
5
5
|
"module": "dist/nys-modal.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"test:watch": "vite build && wtr --watch"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@nysds/nys-button": "^1.13.
|
|
25
|
+
"@nysds/nys-button": "^1.13.1"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"lit": "^3.3.1",
|