fix(csr): drive tab url restore by announcement, anchor nav submenus

Two mechanisms tried and rejected while testing this against the live
showcase, both failing the same way on a second history step:

  - synthesising a click on the matching tab: a re-render replaces the tab
    buttons, and clicking a freshly replaced node that has not been bound
    does nothing at all
  - tracking the last applied value in the runtime: that state drifts out of
    step with the component and silently swallows real changes

The runtime is now stateless. It announces the value the URL names via a
wrnexus:tabs:restore event and the component applies it, comparing against
its own selection rather than a DOM attribute a re-render owns.

Nav submenus are anchored so the viewport clamp keeps them on screen.

Comments in the runtime template trimmed to stay inside the size budget
rather than raising the ceiling again.

Known limitation: a second consecutive back/forward does not update the
selection, because the re-render replaces the component root without
rebinding its declarative listeners. That is a framework defect, not a Tabs
one, and needs its own fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 17:47:41 +05:30
co-authored by Claude Opus 5
parent f1d1081b67
commit f6993e6cdb
6 changed files with 114 additions and 58 deletions
+52 -17
View File
@@ -1053,21 +1053,20 @@ test("an empty or false roving attribute opts the group out entirely", () => {
expect(doc.activeElement!.id).toBe("a");
});
test("popstate activates the tab named by the query parameter without re-pushing", () => {
test("popstate announces the value the url names instead of clicking a tab", () => {
const win = mount(
`<section data-wrn-tabs-param="tab">
`<section data-wrn-tabs-param="tab" data-wrn-tabs-default="one">
<div role="tablist">
<button role="tab" data-value="one" aria-selected="true" id="t1">One</button>
<button role="tab" data-value="two" aria-selected="false" id="t2">Two</button>
<button role="tab" data-value="one" id="t1">One</button>
<button role="tab" data-value="two" id="t2">Two</button>
</div>
</section>`,
);
const doc = win.document;
let restoringAtClick: string | null = "not-clicked";
(doc.querySelector("#t2") as unknown as HTMLElement).addEventListener("click", () => {
restoringAtClick = doc
.querySelector("[data-wrn-tabs-param]")!
.getAttribute("data-wrn-tabs-restoring");
const group = doc.querySelector("[data-wrn-tabs-param]") as unknown as HTMLElement;
const seen: string[] = [];
group.addEventListener("wrnexus:tabs:restore", (event) => {
seen.push((event as CustomEvent).detail.value);
});
win.location.search = "?tab=two";
@@ -1076,13 +1075,49 @@ test("popstate activates the tab named by the query parameter without re-pushing
"popstate",
) as unknown as Parameters<Window["dispatchEvent"]>[0],
);
expect(seen).toEqual(["two"]);
// The runtime activates the matching tab and marks the round trip, so the
// component can tell a history navigation from a real click and skip
// pushing another entry.
expect(restoringAtClick).toBe("true");
// The marker is cleaned up once the activation is done.
expect(
doc.querySelector("[data-wrn-tabs-param]")!.getAttribute("data-wrn-tabs-restoring"),
).toBeNull();
// Back past the first click lands on a url with no parameter at all; the
// component's starting selection is the right answer there.
win.location.search = "";
win.dispatchEvent(
new (win as unknown as { Event: new (t: string) => unknown }).Event(
"popstate",
) as unknown as Parameters<Window["dispatchEvent"]>[0],
);
expect(seen).toEqual(["two", "one"]);
});
test("the url sync announces on every popstate and leaves idempotence to the component", () => {
const win = mount(
`<section data-wrn-tabs-param="tab" data-wrn-tabs-default="one">
<div role="tablist">
<button role="tab" data-value="one" id="t1">One</button>
<button role="tab" data-value="two" id="t2">Two</button>
</div>
</section>`,
);
const doc = win.document;
const group = doc.querySelector("[data-wrn-tabs-param]") as unknown as HTMLElement;
const seen: string[] = [];
group.addEventListener("wrnexus:tabs:restore", (event) => {
seen.push((event as CustomEvent).detail.value);
});
win.location.search = "?tab=two";
for (let i = 0; i < 3; i += 1) {
win.dispatchEvent(
new (win as unknown as { Event: new (t: string) => unknown }).Event(
"popstate",
) as unknown as Parameters<Window["dispatchEvent"]>[0],
);
}
/*
* Deliberately not deduplicated here. Tracking what was last applied meant
* the runtime held state that drifted out of step with the component, which
* silently swallowed real changes. The component compares against its own
* selection instead, which cannot drift.
*/
expect(seen).toEqual(["two", "two", "two"]);
});