Skip to content

Commit 4a19843

Browse files
committed
Address toast review feedback
1 parent 1e9dd0f commit 4a19843

4 files changed

Lines changed: 59 additions & 11 deletions

File tree

‎sqlpage/sqlpage.js‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,25 @@ const toast_positions = {
336336
const toast_instances = new WeakMap();
337337

338338
function normalize_hash(hash) {
339-
return hash?.replace(/^#/, "");
339+
const normalized = hash?.replace(/^#/, "");
340+
if (!normalized) return normalized;
341+
try {
342+
return decodeURIComponent(normalized);
343+
} catch {
344+
return normalized;
345+
}
346+
}
347+
348+
function restore_focus_after_toast(toast, container) {
349+
if (!toast.contains(document.activeElement)) return;
350+
const next_close = [
351+
...container.querySelectorAll('.toast.show [data-bs-dismiss="toast"]'),
352+
].find((button) => !toast.contains(button));
353+
if (next_close) return next_close.focus();
354+
const main = document.querySelector("main");
355+
if (!main) return;
356+
if (!main.hasAttribute("tabindex")) main.setAttribute("tabindex", "-1");
357+
main.focus();
340358
}
341359

342360
function open_toasts_for_hash() {
@@ -369,6 +387,8 @@ function sqlpage_toast() {
369387
"toast-container",
370388
"position-fixed",
371389
"p-3",
390+
"mh-100",
391+
"overflow-auto",
372392
...toast_positions[position],
373393
);
374394
container.dataset.sqlpageToastPosition = position;
@@ -390,6 +410,7 @@ function sqlpage_toast() {
390410
});
391411
toast_instances.set(toast, instance);
392412
toast.addEventListener("hidden.bs.toast", () => {
413+
restore_focus_after_toast(toast, container);
393414
if (toast.dataset.toastTrigger) {
394415
if (
395416
normalize_hash(window.location.hash) ===
@@ -399,6 +420,8 @@ function sqlpage_toast() {
399420
}
400421
return;
401422
}
423+
instance.dispose();
424+
toast_instances.delete(toast);
402425
toast.remove();
403426
if (
404427
container.dataset.sqlpageGenerated === "toast-container" &&

‎sqlpage/templates/shell.handlebars‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
{{/if}}
4747
{{/if}}
4848

49-
<script src="{{static_path 'sqlpage.js'}}" defer nonce="{{@csp_nonce}}"></script>
49+
<script src="{{static_path 'sqlpage.js'}}" defer nonce="{{@csp_nonce}}" data-sqlpage-runtime="deferred"></script>
5050
{{#each (to_array javascript)}}
5151
{{#if this}}
5252
<script src="{{this}}" defer nonce="{{@../csp_nonce}}"></script>

‎sqlpage/templates/toast.handlebars‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,27 @@
1212
{{~/if~}}
1313
</div>
1414
{{~#if dismissible~}}
15-
<button type="button" class="btn-close{{#if color}} btn-close-white{{/if}} flex-shrink-0" data-bs-dismiss="toast" aria-label="Close notification"></button>
15+
<button type="button" class="btn p-0 border-0 bg-transparent text-reset flex-shrink-0" data-bs-dismiss="toast" aria-label="Close notification">{{icon_img "x"}}</button>
1616
{{~/if~}}
1717
</div>
1818
</div>
19+
<script nonce="{{@csp_nonce}}">
20+
(() => {
21+
const initialize = () => window.sqlpage_toast?.();
22+
if (window.bootstrap || window.tabler?.bootstrap) return initialize();
23+
24+
const runtime = document.querySelector("script[data-sqlpage-runtime]");
25+
if (!runtime) return;
26+
if (runtime.dataset.sqlpageRuntime !== "deferred") {
27+
runtime.addEventListener("load", initialize, { once: true });
28+
return;
29+
}
30+
31+
const early_runtime = runtime.cloneNode();
32+
runtime.remove();
33+
early_runtime.dataset.sqlpageRuntime = "loading";
34+
early_runtime.removeAttribute("defer");
35+
early_runtime.addEventListener("load", initialize, { once: true });
36+
document.head.appendChild(early_runtime);
37+
})();
38+
</script>

‎tests/end-to-end/official-site.spec.ts‎

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,18 @@ test("toast notifications initialize, stack, dismiss, and render safely", async
5353
const stackOne = page.locator("#toast-stack-one");
5454
const stackTwo = page.locator("#toast-stack-two");
5555
await expect(stackOne).toBeHidden();
56-
await page.getByRole("link", { name: "Show queued notifications" }).click();
56+
await page.getByRole("button", { name: "Show queued notifications" }).click();
5757
await expect(stackOne).toBeVisible();
5858
await expect(stackTwo).toBeVisible();
59+
const temporary = page.locator("#toast-short");
60+
await expect(temporary).toBeVisible();
5961
const stackContainer = stackOne.locator("xpath=..");
6062
await expect(stackContainer).toHaveAttribute(
6163
"data-sqlpage-toast-position",
6264
"top-end",
6365
);
66+
await expect(stackContainer).toHaveClass(/\bmh-100\b/);
67+
await expect(stackContainer).toHaveClass(/\boverflow-auto\b/);
6468
expect(
6569
await stackTwo
6670
.locator("xpath=..")
@@ -74,27 +78,28 @@ test("toast notifications initialize, stack, dismiss, and render safely", async
7478
(firstBox?.y ?? 0) + (firstBox?.height ?? 0),
7579
);
7680

77-
const temporary = page.locator("#toast-short");
78-
await expect(temporary).toBeVisible();
7981
await expect(temporary).toBeHidden({ timeout: 5000 });
8082
await expect(stackOne).toBeVisible();
8183

8284
const dismissible = page.locator("#toast-dismissible");
83-
await page.getByRole("link", { name: "Show dismissible error" }).click();
85+
await page.getByRole("button", { name: "Show dismissible error" }).click();
8486
await expect(
8587
dismissible.getByRole("button", { name: "Close notification" }),
8688
).toBeVisible();
8789
await dismissible.getByRole("button", { name: "Close notification" }).click();
8890
await expect(dismissible).toBeHidden();
89-
await page.getByRole("link", { name: "Show dismissible error" }).click();
91+
await expect(page.locator("main")).toBeFocused();
92+
await page.getByRole("button", { name: "Show dismissible error" }).click();
9093
await expect(dismissible).toBeVisible();
9194
await dismissible.getByRole("button", { name: "Close notification" }).click();
92-
await page.getByRole("link", { name: "Show non-dismissible status" }).click();
95+
await page
96+
.getByRole("button", { name: "Show non-dismissible status" })
97+
.click();
9398
await expect(
9499
page.locator("#toast-nondismissible").getByRole("button"),
95100
).toHaveCount(0);
96101

97-
await page.getByRole("link", { name: "Show rich notifications" }).click();
102+
await page.getByRole("button", { name: "Show rich notifications" }).click();
98103
await expect(page.locator("#toast-markdown strong")).toHaveText("2.0");
99104
await expect(page.locator("#toast-markdown a")).toHaveAttribute(
100105
"href",
@@ -108,7 +113,7 @@ test("toast notifications initialize, stack, dismiss, and render safely", async
108113
const bottomContainer = page.locator(
109114
'[data-sqlpage-toast-position="bottom-center"]',
110115
);
111-
await page.getByRole("link", { name: "Show bottom notification" }).click();
116+
await page.getByRole("button", { name: "Show bottom notification" }).click();
112117
await expect(page.locator("#toast-bottom-center")).toBeVisible();
113118
await expect(bottomContainer).toHaveClass(/\bbottom-0\b/);
114119
await expect(bottomContainer).toHaveClass(/\bstart-50\b/);

0 commit comments

Comments
 (0)