@tillsc/progressive-web-components 0.1.0 → 0.1.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.
Files changed (45) hide show
  1. package/package.json +3 -4
  2. package/src/core/pwc-children-observer-element.js +53 -0
  3. package/src/core/pwc-element.js +76 -0
  4. package/src/core/pwc-sentinel-init-element.js +56 -0
  5. package/src/core/pwc-simple-init-element.js +25 -0
  6. package/src/core/utils.js +9 -0
  7. package/src/dialog-opener/INTERNALS.md +47 -0
  8. package/src/dialog-opener/README.md +145 -0
  9. package/src/dialog-opener/base.js +226 -0
  10. package/src/dialog-opener/bs5/dialog-opener.js +73 -0
  11. package/src/dialog-opener/bs5/index.js +10 -0
  12. package/src/dialog-opener/dialog-opener.css +21 -0
  13. package/src/dialog-opener/dialog-opener.js +41 -0
  14. package/src/dialog-opener/index.js +12 -0
  15. package/src/dialog-opener/test/basic.html +109 -0
  16. package/src/dialog-opener/test/bs5-basic.html +100 -0
  17. package/src/dialog-opener/test/bs5-iframe-target.html +41 -0
  18. package/src/dialog-opener/test/iframe-target.html +28 -0
  19. package/src/dialog-opener/test/index.html +19 -0
  20. package/src/index-bs5.js +3 -0
  21. package/src/index.js +3 -0
  22. package/src/modal-dialog/INTERNALS.md +55 -0
  23. package/src/modal-dialog/README.md +139 -0
  24. package/src/modal-dialog/base.js +117 -0
  25. package/src/modal-dialog/bs5/index.js +7 -0
  26. package/src/modal-dialog/bs5/modal-dialog.js +109 -0
  27. package/src/modal-dialog/index.js +9 -0
  28. package/src/modal-dialog/modal-dialog.css +103 -0
  29. package/src/modal-dialog/modal-dialog.js +97 -0
  30. package/src/modal-dialog/test/basic.html +84 -0
  31. package/src/modal-dialog/test/bs5-basic.html +123 -0
  32. package/src/modal-dialog/test/index.html +19 -0
  33. package/src/multiselect-dual-list/INTERNALS.md +101 -0
  34. package/src/multiselect-dual-list/README.md +191 -0
  35. package/src/multiselect-dual-list/base.js +215 -0
  36. package/src/multiselect-dual-list/bs5/index.js +10 -0
  37. package/src/multiselect-dual-list/bs5/multiselect-dual-list.js +103 -0
  38. package/src/multiselect-dual-list/index.js +9 -0
  39. package/src/multiselect-dual-list/multiselect-dual-list.css +123 -0
  40. package/src/multiselect-dual-list/multiselect-dual-list.js +100 -0
  41. package/src/multiselect-dual-list/test/basic.html +115 -0
  42. package/src/multiselect-dual-list/test/bs5-basic.html +106 -0
  43. package/src/multiselect-dual-list/test/dynamic-options.html +70 -0
  44. package/src/multiselect-dual-list/test/filter-api.html +66 -0
  45. package/src/multiselect-dual-list/test/index.html +21 -0
@@ -0,0 +1,73 @@
1
+ import { defineOnce } from "../../core/utils.js";
2
+ import { BaseDialogOpener } from "../base.js";
3
+
4
+ /**
5
+ * <pwc-dialog-opener-bs5>
6
+ *
7
+ * Uses <pwc-modal-dialog-bs5> as the Bootstrap modal socket.
8
+ *
9
+ * Requirements:
10
+ * - Bootstrap 5 CSS + JS present (globalThis.bootstrap.Modal)
11
+ * - pwc-modal-dialog-bs5 defined (either imported/bundled or already on page)
12
+ */
13
+ export class PwcDialogOpenerBs5 extends BaseDialogOpener {
14
+ findOrCreateDialog(src) {
15
+ const tag = "pwc-modal-dialog-bs5";
16
+
17
+ // Prefer one modal socket per opener instance.
18
+ if (!this.dialog) {
19
+ // Use existing child socket if provided, otherwise create one.
20
+ this.dialog = this.querySelector(tag) || document.createElement(tag);
21
+
22
+ // If caller didn't place it into the DOM, keep it associated with this component.
23
+ // ModalDialogBase will auto-append itself to <body> on open() if not connected.
24
+ if (!this.dialog.isConnected) {
25
+ this.appendChild(this.dialog);
26
+ }
27
+ }
28
+
29
+ // Open modal and get access to the body/footer containers.
30
+ this.dialog.open({
31
+ title: this.getAttribute("title") || "",
32
+ size: this.getAttribute("size") || "lg",
33
+ closeText: this.getAttribute("close") || "Close",
34
+ showClose: false,
35
+ backdrop: true,
36
+ keyboard: true,
37
+ focus: true
38
+ });
39
+
40
+ const closeText = this.getAttribute("close") || "Close";
41
+ this.dialog.footerEl.innerHTML = `
42
+ <div class="pwc-dialog-opener-actions">
43
+ <button type="button" class="btn btn-secondary" data-pwc-action="close" aria-label="${closeText}">
44
+ ${closeText}
45
+ </button>
46
+ </div>
47
+ `;
48
+
49
+ const body = this.dialog.bodyEl;
50
+ body.replaceChildren(this.createIFrame(src));
51
+
52
+ // BaseDialogOpener expects this.modal.show()/hide()
53
+ // Map those to the modal-dialog component API.
54
+ this.modal = {
55
+ show: () => {
56
+ // already shown by open(); no-op for compatibility
57
+ },
58
+ hide: () => this.dialog.close()
59
+ };
60
+ }
61
+
62
+ _moveOutSelector() {
63
+ let selector = super._moveOutSelector();
64
+ if (selector === "primary") {
65
+ selector = ".btn-primary[type=submit]";
66
+ }
67
+ return selector
68
+ }
69
+ }
70
+
71
+ export function define() {
72
+ defineOnce("pwc-dialog-opener-bs5", PwcDialogOpenerBs5);
73
+ }
@@ -0,0 +1,10 @@
1
+ import { define } from "./dialog-opener.js";
2
+
3
+ // Ensure the modal-dialog-bs5 is registered first.
4
+ import "../../modal-dialog/bs5/index.js";
5
+
6
+ export function register() {
7
+ define();
8
+ }
9
+
10
+ register();
@@ -0,0 +1,21 @@
1
+ /* Footer actions container (used by move-out) */
2
+ .pwc-dialog-opener-footer {
3
+ display: flex;
4
+ justify-content: flex-end;
5
+ gap: 8px;
6
+ }
7
+
8
+ /* Close button */
9
+ .pwc-dialog-opener-close {
10
+ appearance: none;
11
+ border: 1px solid rgba(0, 0, 0, 0.25);
12
+ background: transparent;
13
+ font: inherit;
14
+ padding: 6px 12px;
15
+ border-radius: 4px;
16
+ cursor: pointer;
17
+ }
18
+
19
+ .pwc-dialog-opener-close:hover {
20
+ background: rgba(0, 0, 0, 0.06);
21
+ }
@@ -0,0 +1,41 @@
1
+ import { defineOnce } from "../core/utils.js";
2
+ import { BaseDialogOpener } from "./base.js";
3
+
4
+ export class PwcDialogOpener extends BaseDialogOpener {
5
+ findOrCreateDialog(src) {
6
+ if (!this.modalDialog) {
7
+ this.modalDialog = document.createElement("pwc-modal-dialog");
8
+ document.body.appendChild(this.modalDialog);
9
+ }
10
+
11
+ const closeText = this.getAttribute("close") || "Close";
12
+ this.modalDialog.open({
13
+ closeText,
14
+ showClose: false
15
+ });
16
+ this.modalDialog.footerEl.innerHTML = `
17
+ <div class="pwc-dialog-opener-actions pwc-dialog-opener-footer">
18
+ <button type="button" class="pwc-dialog-opener-close" data-pwc-action="close" aria-label="${closeText}">
19
+ ${closeText}
20
+ </button>
21
+ </div>
22
+ `;
23
+ const iframe = this.createIFrame(src);
24
+ this.modalDialog.bodyEl.replaceChildren(iframe);
25
+
26
+ // Contract for BaseDialogOpener.enhanceIFrame():
27
+ // it queries this.dialog for "iframe".
28
+ this.dialog = this.modalDialog.ui.rootEl;
29
+
30
+ // Contract for BaseDialogOpener.open():
31
+ // it calls this.modal.show()/hide().
32
+ this.modal = {
33
+ show: () => {}, // modal-dialog is already shown by open()
34
+ hide: () => this.modalDialog.close()
35
+ };
36
+ }
37
+ }
38
+
39
+ export function define() {
40
+ defineOnce("pwc-dialog-opener", PwcDialogOpener);
41
+ }
@@ -0,0 +1,12 @@
1
+ import { PwcDialogOpener, define } from "./dialog-opener.js";
2
+ import cssText from "./dialog-opener.css";
3
+
4
+ // Ensure the modal-dialog is registered first.
5
+ import "../modal-dialog/index.js";
6
+
7
+ export function register() {
8
+ PwcDialogOpener.registerCss(cssText);
9
+ define();
10
+ }
11
+
12
+ register();
@@ -0,0 +1,109 @@
1
+ <!-- src/dialog-opener/test/click-opens-dialog.html -->
2
+ <!doctype html>
3
+ <html>
4
+
5
+ <head>
6
+ <meta charset="utf-8" />
7
+ <title>dialog-opener: click opens dialog</title>
8
+ <script type="module" src="../../../dist/dialog-opener.js"></script>
9
+ </head>
10
+
11
+ <body>
12
+ <pwc-dialog-opener id="dialog-opener-demo" move-out="submit" local-reload="with-scripts replace-url">
13
+ <a href="./iframe-target.html">Open</a><br>
14
+ Hello <span id="first_name"></span> <span id="last_name"></span>
15
+ <input type="hidden" name="default_for_iframe_target">
16
+ <script type="module">
17
+ const url = new URL(window.location.href);
18
+ const firstName = url.searchParams.get("first_name") || "";
19
+ const lastName = url.searchParams.get("last_name") || "";
20
+ document.getElementById("first_name").textContent = firstName
21
+ document.getElementById("last_name").textContent = lastName
22
+ document.querySelector("input[name=default_for_iframe_target]").value = JSON.stringify({firstName , lastName});
23
+ </script>
24
+ </pwc-dialog-opener>
25
+
26
+ <script type="module">
27
+ import { run } from "../../../test/static/harness.js";
28
+
29
+ const u = new URL(window.location.href);
30
+ if (u.searchParams.has("commit")) {
31
+ // IFrame subission mode
32
+ if (u.searchParams.has("commit") && !u.searchParams.has("dialog_finished_with")) {
33
+ // Ensure the iframe URL contains the marker param your component expects.
34
+ // This runs inside the iframe page.
35
+ // This would usually be done by the server handling the form submission,
36
+ // but we can simulate it here for testing.
37
+ u.searchParams.set("dialog_finished_with", "ok");
38
+ window.location.replace(u.toString());
39
+ }
40
+ }
41
+ else {
42
+ run(async ({ assert, equal, waitFor, nextTick, log }) => {
43
+ log("checking element definition");
44
+ assert(customElements.get("pwc-dialog-opener"), "pwc-dialog-opener not defined");
45
+
46
+ const el = document.querySelector("#dialog-opener-demo");
47
+ assert(el, "missing pwc-dialog-opener");
48
+
49
+ const link = el.querySelector("a");
50
+ assert(link, "missing link");
51
+
52
+ log("clicking link");
53
+ link.dispatchEvent(
54
+ new MouseEvent("click", { bubbles: true, cancelable: true, composed: true })
55
+ );
56
+ await nextTick();
57
+
58
+ log("waiting for dialog to exist");
59
+ await waitFor(() => Boolean(document.querySelector("dialog.pwc-modal-dialog")), {
60
+ message: "dialog not created"
61
+ });
62
+
63
+ const dlg = document.querySelector("dialog.pwc-modal-dialog");
64
+ assert(dlg, "dialog missing");
65
+
66
+ log("waiting for dialog to be open");
67
+ await waitFor(() => dlg.open === true, { message: "dialog did not open" });
68
+
69
+ log("checking iframe exists");
70
+ const iframe = dlg.querySelector("iframe");
71
+ assert(iframe, "iframe not created");
72
+
73
+ log("waiting for iframe load");
74
+ await waitFor(() => {
75
+ return iframe.contentDocument && iframe.contentDocument.readyState === "complete";
76
+ }, { timeoutMs: 10_000, message: "iframe did not load" });
77
+
78
+ log("checking iframe src has _layout=false");
79
+ {
80
+ const raw = iframe.getAttribute("src") || "";
81
+ const u = new URL(raw, window.location.href);
82
+ assert(u.pathname.endsWith("/src/dialog-opener/test/iframe-target.html"), `unexpected iframe pathname: ${u.pathname}`);
83
+ assert(u.searchParams.get("_layout") === "false", `missing or wrong _layout param: ${u.search}`);
84
+ }
85
+
86
+ log("waiting for moved-out OK button");
87
+ await waitFor(() => {
88
+ const btn = dlg.querySelector(".pwc-dialog-opener-actions button[type=submit]");
89
+ return Boolean(btn);
90
+ }, { timeoutMs: 10_000, message: "moved-out submit button not found" });
91
+
92
+ const okBtn = dlg.querySelector(".pwc-dialog-opener-actions button[type=submit]");
93
+ assert(okBtn, "moved-out submit button missing");
94
+
95
+ log("clicking moved-out OK button");
96
+ okBtn.dispatchEvent(
97
+ new MouseEvent("click", { bubbles: true, cancelable: true, composed: true })
98
+ );
99
+
100
+ log("waiting for dialog to close after redirect");
101
+ await waitFor(() => dlg.open === false, { timeoutMs: 10_000, message: "dialog did not close after OK" });
102
+
103
+ equal(document.querySelectorAll("pwc-modal-dialog").length, 1, "modal-dialog element should persist for reuse");
104
+ });
105
+ }
106
+ </script>
107
+ </body>
108
+
109
+ </html>
@@ -0,0 +1,100 @@
1
+ <!doctype html>
2
+ <html>
3
+
4
+ <head>
5
+ <meta charset="utf-8" />
6
+ <title>dialog-opener-bs5: basic</title>
7
+
8
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
9
+ </head>
10
+
11
+ <body>
12
+ <pwc-dialog-opener-bs5 id="opener" local-reload="with-scripts replace-url">
13
+ <a href="./bs5-iframe-target.html">Open dialog</a>
14
+ Hello <span id="first_name"></span> <span id="last_name"></span>
15
+ <input type="hidden" name="default_for_iframe_target">
16
+ <script type="module">
17
+ const url = new URL(window.location.href);
18
+ const firstName = url.searchParams.get("first_name") || "";
19
+ const lastName = url.searchParams.get("last_name") || "";
20
+ document.getElementById("first_name").textContent = firstName
21
+ document.getElementById("last_name").textContent = lastName
22
+ document.querySelector("input[name=default_for_iframe_target]").value = JSON.stringify({ firstName, lastName });
23
+ </script>
24
+ </pwc-dialog-opener-bs5>
25
+
26
+ <!-- component bundle -->
27
+ <script type="module" src="../../../dist/dialog-opener-bs5.js"></script>
28
+
29
+ <!-- bootstrap js -->
30
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
31
+
32
+ <script type="module">
33
+ import { run } from "../../../test/static/harness.js";
34
+
35
+ const u = new URL(window.location.href);
36
+ if (u.searchParams.has("commit")) {
37
+ // IFrame subission mode
38
+ if (u.searchParams.has("commit") && !u.searchParams.has("dialog_finished_with")) {
39
+ // Ensure the iframe URL contains the marker param your component expects.
40
+ // This runs inside the iframe page.
41
+ // This would usually be done by the server handling the form submission,
42
+ // but we can simulate it here for testing.
43
+ u.searchParams.set("dialog_finished_with", "ok");
44
+ window.location.replace(u.toString());
45
+ }
46
+ }
47
+ else {
48
+
49
+ run(async ({ assert, waitFor, log }) => {
50
+ log("checking custom element definition");
51
+ assert(
52
+ customElements.get("pwc-dialog-opener-bs5"),
53
+ "pwc-dialog-opener-bs5 not defined"
54
+ );
55
+
56
+ const opener = document.getElementById("opener");
57
+ assert(opener, "missing dialog opener element");
58
+
59
+ const link = opener.querySelector("a");
60
+ assert(link, "missing link");
61
+
62
+ log("clicking opener link");
63
+ link.click();
64
+
65
+ log("waiting for bootstrap modal to appear");
66
+ await waitFor(() => document.querySelector(".modal.show"), {
67
+ message: "modal not shown"
68
+ });
69
+
70
+ const modal = document.querySelector(".modal.show");
71
+ assert(modal, "modal element missing");
72
+
73
+ log("checking iframe existence");
74
+ const iframe = modal.querySelector("iframe");
75
+ assert(iframe, "iframe not created");
76
+
77
+ log("waiting for iframe to load");
78
+ await waitFor(() => {
79
+ return iframe.contentDocument && iframe.contentDocument.readyState === "complete";
80
+ }, { message: "iframe did not load" });
81
+
82
+ log("finding modal close button");
83
+ const closeButton = modal.querySelector("[data-pwc-action='close']");
84
+ assert(closeButton, "close button not found on modal");
85
+
86
+ log("clicking close button");
87
+ closeButton.click();
88
+
89
+ log("waiting for modal to close");
90
+ await waitFor(() => !modal.classList.contains("show"), {
91
+ message: "modal did not close after clicking close button"
92
+ });
93
+
94
+ log("done");
95
+ });
96
+ }
97
+ </script>
98
+ </body>
99
+
100
+ </html>
@@ -0,0 +1,41 @@
1
+ <!doctype html>
2
+ <html>
3
+
4
+ <head>
5
+ <meta charset="utf-8" />
6
+ <title>dialog-opener-bs5 iframe target</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
8
+ </head>
9
+
10
+ <body>
11
+ <h1>iframe target</h1>
12
+ <form method="get" action="./bs5-basic.html" class="container py-3">
13
+ <div class="mb-3">
14
+ <label for="first_name" class="form-label">First name</label>
15
+ <input id="first_name" name="first_name" value="John" class="form-control" />
16
+ </div>
17
+
18
+ <div class="mb-3">
19
+ <label for="last_name" class="form-label">Last name</label>
20
+ <input id="last_name" name="last_name" value="Doe" class="form-control" />
21
+ </div>
22
+
23
+ <button type="submit" name="commit" value="ok" class="btn btn-primary">OK</button>
24
+ </form>
25
+
26
+ <script type="module">
27
+ const url = new URL(window.location.href);
28
+ const defaultData = url.searchParams.get("default");
29
+ if (defaultData) {
30
+ try {
31
+ const data = JSON.parse(defaultData);
32
+ document.querySelector("input[name=first_name]").value = data.firstName;
33
+ document.querySelector("input[name=last_name]").value = data.lastName;
34
+ } catch (e) {
35
+ console.error("Failed to parse default data:", e);
36
+ }
37
+ }
38
+ </script>
39
+ </body>
40
+
41
+ </html>
@@ -0,0 +1,28 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>dialog-opener iframe target</title>
6
+ </head>
7
+ <body>
8
+ <h1>iframe target</h1>
9
+ <form method="get" action="./basic.html">
10
+ <label>First name: </label><input name="first_name" value="John" /></label><br />
11
+ <label>Last name: </label><input name="last_name" value="Doe" /></label><br />
12
+ <button type="submit" name="commit" value="ok">OK</button>
13
+ </form>
14
+ <script type="module">
15
+ const url = new URL(window.location.href);
16
+ const defaultData = url.searchParams.get("default");
17
+ if (defaultData) {
18
+ try {
19
+ const data = JSON.parse(defaultData);
20
+ document.querySelector("input[name=first_name]").value = data.firstName;
21
+ document.querySelector("input[name=last_name]").value = data.lastName;
22
+ } catch (e) {
23
+ console.error("Failed to parse default data:", e);
24
+ }
25
+ }
26
+ </script>
27
+ </body>
28
+ </html>
@@ -0,0 +1,19 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>dialog-opener</title>
6
+ <link rel="stylesheet" href="../../../test/static/index.css">
7
+ <script type="module" src="https://cdn.jsdelivr.net/npm/zero-md@3?register"></script>
8
+ </head>
9
+ <body>
10
+ <zero-md src="../README.md"></zero-md>
11
+
12
+ <h2>Weitere Links</h2>
13
+ <ul>
14
+ <li><a href="./basic.html" data-test-page>Test: basic</a></li>
15
+ <li><a href="./bs5-basic.html" data-test-page>Test: bs5-basic</a></li>
16
+ <li><a href="../INTERNALS.md">INTERNALS.md</a></li>
17
+ </ul>
18
+ </body>
19
+ </html>
@@ -0,0 +1,3 @@
1
+ import './dialog-opener/bs5';
2
+ import './modal-dialog/bs5';
3
+ import './multiselect-dual-list/bs5';
package/src/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import './dialog-opener';
2
+ import './modal-dialog';
3
+ import './multiselect-dual-list';
@@ -0,0 +1,55 @@
1
+ # Modal-Dialog — Internals
2
+
3
+ ## Architecture
4
+
5
+ `ModalDialogBase` (`base.js`) provides orchestration for opening, closing, and stacking modals.
6
+ It defines a **subclass contract** — a set of hooks that variants must implement:
7
+
8
+ | Hook | Responsibility |
9
+ |------|---------------|
10
+ | `_render(ctx)` | Build DOM, return `{ rootEl, bodyEl, headerEl, footerEl, teardown? }` |
11
+ | `_getOpenSibling()` | Find an already-open modal (for stacking) |
12
+ | `_suspend(el)` / `_restore(el)` | Hide/show a sibling modal during stacking |
13
+ | `_show(ui, options)` / `_hide(ui)` | Actually show/hide the modal |
14
+ | `_armFinalClose(ui, onFinalClose)` | Wire up the "real close" callback |
15
+
16
+ The vanilla variant uses native `<dialog>` + `showModal()`.
17
+ The BS5 variant uses `bootstrap.Modal`.
18
+
19
+ ## Open lifecycle
20
+
21
+ 1. If the element is not in the DOM, it auto-appends to `<body>` (and auto-removes on close)
22
+ 2. Any previous UI is torn down
23
+ 3. `_render()` builds fresh DOM and returns the `ui` object
24
+ 4. `_getOpenSibling()` checks for a currently open modal
25
+ 5. If a sibling exists → `_suspend()` hides it (marked as `closeReason=suspend`)
26
+ 6. `_armFinalClose()` registers the close callback
27
+ 7. `_show()` makes the modal visible
28
+
29
+ ## Stacking
30
+
31
+ When a second modal opens while the first is visible:
32
+
33
+ - The first modal is suspended (hidden but not destroyed)
34
+ - Its `dataset.closeReason` is set to `"suspend"` so close handlers know to ignore it
35
+ - When the second modal closes, `_onFinalClose()` restores the first via `_restore()`
36
+ - The restore happens in a `queueMicrotask` to avoid event ordering issues
37
+
38
+ ## Close detection
39
+
40
+ The tricky part is distinguishing a **final close** (user dismissed the dialog) from a
41
+ **suspend close** (another modal is temporarily hiding this one).
42
+
43
+ - Vanilla: listens to the `<dialog>` `close` event, checks `dataset.closeReason`
44
+ - BS5: listens to `hidden.bs.modal`, same check
45
+
46
+ Only a final close triggers teardown and parent restoration.
47
+
48
+ ## Close triggers
49
+
50
+ Handled in `ModalDialogBase.handleEvent()`:
51
+
52
+ - Click on `[data-pwc-action="close"]` → `close()`
53
+ - Click directly on `ui.rootEl` (backdrop) → `close()`
54
+
55
+ `close()` sets `dataset.closeReason = "final"` and calls `_hide()`.
@@ -0,0 +1,139 @@
1
+ # `<pwc-modal-dialog>`
2
+
3
+ Minimal modal dialog web component.
4
+
5
+ `<pwc-modal-dialog>` provides a small, explicit API to open modal dialogs from JavaScript.
6
+ It is designed as a low-level building block and does **not** manage navigation, iframes,
7
+ or application flow on its own.
8
+
9
+ The component does **not** use Shadow DOM and relies on regular DOM structure and CSS hooks.
10
+
11
+ ---
12
+
13
+ ## Basic usage
14
+
15
+ ```html
16
+ <pwc-modal-dialog></pwc-modal-dialog>
17
+ ```
18
+
19
+ ```js
20
+ const dialog = document.querySelector("pwc-modal-dialog");
21
+
22
+ dialog.open({ title: "Example" });
23
+ dialog.bodyEl.innerHTML = "<p>Hello world</p>";
24
+ ```
25
+
26
+ Calling `open()` renders and shows the dialog. Content is populated **after** opening.
27
+
28
+ ---
29
+
30
+ ## JavaScript API
31
+
32
+ ### `open(options)`
33
+
34
+ Opens the dialog.
35
+
36
+ Supported options:
37
+ - `title` – dialog title text
38
+ - `size` – size token (implementation-specific)
39
+ - `closeText` – accessible label for close actions
40
+
41
+ ```js
42
+ dialog.open({
43
+ title: "Edit item",
44
+ size: "lg",
45
+ closeText: "Cancel"
46
+ });
47
+ ```
48
+
49
+ ### `close()`
50
+
51
+ Closes the dialog programmatically.
52
+
53
+ ```js
54
+ dialog.close();
55
+ ```
56
+
57
+ ---
58
+
59
+ ## Content accessors
60
+
61
+ After `open()` the following properties are available:
62
+
63
+ - `bodyEl` – main content container
64
+ - `headerEl` – header container
65
+ - `footerEl` – footer container
66
+
67
+ ```js
68
+ dialog.bodyEl.append(form);
69
+ dialog.footerEl.append(button);
70
+ ```
71
+
72
+ Accessing these before `open()` throws an error.
73
+
74
+ ---
75
+
76
+ ## Close behavior
77
+
78
+ The dialog closes when:
79
+
80
+ - `close()` is called
81
+ - an element with `data-pwc-action="close"` is clicked
82
+ - the backdrop is clicked (implementation dependent)
83
+
84
+ Example close button:
85
+
86
+ ```html
87
+ <button data-pwc-action="close">Close</button>
88
+ ```
89
+
90
+ ---
91
+
92
+ ## Stacking behavior
93
+
94
+ If a dialog is opened while another dialog is already open:
95
+
96
+ - the parent dialog is temporarily suspended
97
+ - the child dialog is shown
98
+ - when the child closes, the parent is restored
99
+
100
+ This works for both vanilla and Bootstrap variants.
101
+
102
+ ---
103
+
104
+ ## Styling (vanilla)
105
+
106
+ The vanilla implementation uses the native `<dialog>` element.
107
+
108
+ Key CSS hooks:
109
+
110
+ - `pwc-modal-dialog`
111
+ - `.pwc-modal-dialog-surface`
112
+ - `.pwc-modal-dialog-header`
113
+ - `.pwc-modal-dialog-body`
114
+ - `.pwc-modal-dialog-footer`
115
+
116
+ Custom properties are defined on the host element:
117
+
118
+ ```css
119
+ pwc-modal-dialog {
120
+ --pwc-modal-width: 640px;
121
+ --pwc-modal-padding: 1rem;
122
+ }
123
+ ```
124
+
125
+ ---
126
+
127
+ ## Bootstrap 5 variant
128
+
129
+ A Bootstrap 5 based implementation is available:
130
+
131
+ ```html
132
+ <pwc-modal-dialog-bs5></pwc-modal-dialog-bs5>
133
+ ```
134
+
135
+ Notes:
136
+ - Same JavaScript API as the vanilla component
137
+ - Uses Bootstrap modal markup and classes
138
+ - No additional close button is injected
139
+ - Requires Bootstrap 5 JavaScript and CSS to be present globally