Skip to content

Commit 31f7022

Browse files
committed
fix(frontend) :: remove any types from sqlpage.ts
`sqlpage.ts` now has no implicit anys. Fixing this smoked out two bugs :: 1. `init_bootstrap_components` read `event.target` without knowing what kind of element. We also tighting scope from `Element` to `HTMLElement`. 2. `normalize_hash` passed `undefined` to `decodeURIComponent`, which stringifies it, so a toast with no `data-toast-trigger` opened at `#undefined`.
1 parent 1768fcc commit 31f7022

3 files changed

Lines changed: 67 additions & 14 deletions

File tree

‎frontend/src/sqlpage.ts‎

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ function sqlpage_form() {
372372
}
373373
}
374374

375-
function get_tabler_color(name) {
375+
function get_tabler_color(name: string) {
376376
return getComputedStyle(document.documentElement).getPropertyValue(
377377
`--tblr-${name}`,
378378
);
@@ -394,16 +394,16 @@ function load_scripts() {
394394
}
395395
}
396396

397-
function normalize_hash(hash) {
398-
const normalized = hash?.replace(/^#/, "");
397+
function normalize_hash(hash: string | undefined) {
398+
const normalized = hash?.replace(/^#/, "") ?? "";
399399
try {
400400
return decodeURIComponent(normalized);
401401
} catch {
402402
return normalized;
403403
}
404404
}
405405

406-
function open_toasts_for_hash(toasts) {
406+
function open_toasts_for_hash(toasts: Iterable<HTMLElement>) {
407407
const Toast = page_bootstrap().Toast;
408408
const hash = normalize_hash(window.location.hash);
409409
if (!hash) return;
@@ -414,9 +414,9 @@ function open_toasts_for_hash(toasts) {
414414
}
415415
}
416416

417-
function restore_focus_after_toast(toast, container) {
417+
function restore_focus_after_toast(toast: HTMLElement, container: HTMLElement) {
418418
if (!toast.contains(document.activeElement)) return;
419-
const next_close = container.querySelector(
419+
const next_close = container.querySelector<HTMLElement>(
420420
'.toast.show [data-bs-dismiss="toast"]',
421421
);
422422
if (next_close) {
@@ -501,27 +501,39 @@ add_init_fn(sqlpage_modal);
501501
add_init_fn(load_scripts);
502502
add_init_fn(sqlpage_toast);
503503
window.addEventListener("hashchange", () =>
504-
open_toasts_for_hash(document.querySelectorAll("[data-toast-trigger]")),
504+
open_toasts_for_hash(
505+
document.querySelectorAll<HTMLElement>("[data-toast-trigger]"),
506+
),
505507
);
506508

507-
function init_bootstrap_components(event) {
509+
function init_bootstrap_components(fragment: Element | Document) {
508510
const bootstrap = page_bootstrap();
509-
const fragment = event.target;
510-
for (const el of fragment.querySelectorAll('[data-bs-toggle="tooltip"]')) {
511+
for (const el of fragment.querySelectorAll<HTMLElement>(
512+
'[data-bs-toggle="tooltip"]',
513+
)) {
511514
new bootstrap.Tooltip(el);
512515
}
513-
for (const el of fragment.querySelectorAll('[data-bs-toggle="popover"]')) {
516+
for (const el of fragment.querySelectorAll<HTMLElement>(
517+
'[data-bs-toggle="popover"]',
518+
)) {
514519
new bootstrap.Popover(el);
515520
}
516-
for (const el of fragment.querySelectorAll('[data-bs-toggle="dropdown"]')) {
521+
for (const el of fragment.querySelectorAll<HTMLElement>(
522+
'[data-bs-toggle="dropdown"]',
523+
)) {
517524
new bootstrap.Dropdown(el);
518525
}
519-
for (const el of fragment.querySelectorAll('[data-bs-ride="carousel"]')) {
526+
for (const el of fragment.querySelectorAll<HTMLElement>(
527+
'[data-bs-ride="carousel"]',
528+
)) {
520529
new bootstrap.Carousel(el);
521530
}
522531
}
523532

524-
document.addEventListener("fragment-loaded", init_bootstrap_components);
533+
document.addEventListener("fragment-loaded", ({ target }) => {
534+
if (target instanceof Element || target instanceof Document)
535+
init_bootstrap_components(target);
536+
});
525537

526538
function open_modal_for_hash() {
527539
const hash = window.location.hash.substring(1);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
SELECT 'text' AS component, 'Markup added after load is initialized when a fragment is announced.' AS contents;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { expect, type Page, test } from "../../fixture.ts";
2+
3+
const INJECTED_HINT = "injected hint";
4+
5+
async function addTooltip(page: Page) {
6+
await page.evaluate((hint) => {
7+
const span = document.createElement("span");
8+
span.id = "added";
9+
span.textContent = "added";
10+
span.setAttribute("data-bs-toggle", "tooltip");
11+
span.setAttribute("title", hint);
12+
document.querySelector("main")?.appendChild(span);
13+
}, INJECTED_HINT);
14+
}
15+
16+
test("shows a tooltip added before the document announces a fragment", async ({
17+
page,
18+
}) => {
19+
await addTooltip(page);
20+
await page.evaluate(() =>
21+
document.dispatchEvent(new CustomEvent("fragment-loaded")),
22+
);
23+
await page.locator("#added").hover();
24+
25+
await expect(page.locator(".tooltip")).toHaveText(INJECTED_HINT);
26+
});
27+
28+
test("shows a tooltip added before an element announces a fragment", async ({
29+
page,
30+
}) => {
31+
await addTooltip(page);
32+
await page.evaluate(() =>
33+
document
34+
.querySelector("main")
35+
?.dispatchEvent(new CustomEvent("fragment-loaded", { bubbles: true })),
36+
);
37+
await page.locator("#added").hover();
38+
39+
await expect(page.locator(".tooltip")).toHaveText(INJECTED_HINT);
40+
});

0 commit comments

Comments
 (0)