fix(ui): derive tab selection from the url instead of syncing to it
My diagnosis in the previous commit was wrong. The component root was not being replaced by a reactive re-render: the client router owns popstate and swaps the whole page shell on back and forward, which discards component state entirely. Every mechanism that tried to push state into the component from outside was therefore doomed -- clicking a tab, announcing an event, tracking the last applied value. In url mode the query parameter is now simply the source of truth, read where the selection is computed. Whatever render happens next produces the right tab, with no listener to lose and nothing to keep in step. This deletes the runtime tab sync entirely -- 1590 bytes -- and fixes the back/forward cases that were previously broken. Verified in the showcase: click writes the url, two backs and two forwards each land on the right tab, and a ?tab= deep link opens on it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1052,72 +1052,3 @@ test("an empty or false roving attribute opts the group out entirely", () => {
|
||||
a.dispatchEvent(keydown(win, "ArrowRight"));
|
||||
expect(doc.activeElement!.id).toBe("a");
|
||||
});
|
||||
|
||||
test("popstate announces the value the url names instead of clicking a tab", () => {
|
||||
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";
|
||||
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"]);
|
||||
|
||||
// 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"]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user