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:
@@ -3070,11 +3070,9 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
var ROVING_ITEM_SELECTOR = "[data-wrn-roving-item]";
|
||||
|
||||
/*
|
||||
* These attributes are written by templates, so they arrive stringified:
|
||||
* data-wrn-roving="" or data-wrn-roving-item="false" is how a component
|
||||
* says "not this time". A bare [attr] selector matches either, so the value
|
||||
* has to be checked -- otherwise a stepper with clickable=false still takes
|
||||
* arrow-key focus.
|
||||
* Templates stringify these, so data-wrn-roving="" and
|
||||
* data-wrn-roving-item="false" mean "not this time". A bare [attr] selector
|
||||
* matches either, so the value must be checked.
|
||||
*/
|
||||
// A bare data-wrn-roving-item means yes; only an explicit "false" opts out.
|
||||
function rovingItemOff(value) {
|
||||
@@ -3102,11 +3100,8 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
if (candidate.closest(ROVING_SELECTOR) !== container) continue;
|
||||
if (candidate.hasAttribute("disabled")) continue;
|
||||
if (candidate.getAttribute("aria-disabled") === "true") continue;
|
||||
/*
|
||||
* Deliberately not a size check. The test DOM reports every element as
|
||||
* zero-sized, so measuring here would make the whole feature impossible
|
||||
* to cover -- the same trap the dialog visibility check fell into.
|
||||
*/
|
||||
// Markers, not measurement: the test DOM reports every element as
|
||||
// zero-sized, which is what left the dialog trap uncovered.
|
||||
if (candidate.hasAttribute("hidden")) continue;
|
||||
if (candidate.closest('[data-show="false"]')) continue;
|
||||
found.push(candidate);
|
||||
@@ -3199,16 +3194,10 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
}
|
||||
|
||||
/*
|
||||
* Back/forward support for tabs that mirror their selection into the URL.
|
||||
*
|
||||
* The runtime deliberately does not touch component state. A popstate
|
||||
* listener that assigned to it would be writing after the client function
|
||||
* returned, and that write is dropped. Instead it finds the tab matching the
|
||||
* query parameter and activates it, so the component updates itself through
|
||||
* its own handler.
|
||||
*
|
||||
* data-wrn-tabs-restoring marks the round trip, so the component knows not
|
||||
* to push another history entry for a navigation that came from history.
|
||||
* Back/forward for tabs that mirror their selection into the URL. The
|
||||
* runtime never touches component state -- a popstate listener assigning to
|
||||
* it would write after the client function returned, and that write is
|
||||
* dropped. It announces the value instead and the component applies it.
|
||||
*/
|
||||
function syncTabsFromUrl() {
|
||||
var groups = document.querySelectorAll("[data-wrn-tabs-param]");
|
||||
@@ -3217,16 +3206,16 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
var param = group.getAttribute("data-wrn-tabs-param");
|
||||
if (!param) continue;
|
||||
var value = new URLSearchParams(window.location.search).get(param);
|
||||
if (value === null) continue;
|
||||
var tab = group.querySelector('[role="tab"][data-value="' + value + '"]');
|
||||
if (!tab || !tab.click) continue;
|
||||
if (tab.getAttribute("aria-selected") === "true") continue;
|
||||
group.setAttribute("data-wrn-tabs-restoring", "true");
|
||||
try {
|
||||
tab.click();
|
||||
} finally {
|
||||
group.removeAttribute("data-wrn-tabs-restoring");
|
||||
}
|
||||
// Back past the first click lands on a URL with no parameter at all;
|
||||
// the selection the component started with is the answer there.
|
||||
if (value === null) value = group.getAttribute("data-wrn-tabs-default");
|
||||
if (value === null || value === "") continue;
|
||||
if (!group.querySelector('[role="tab"][data-value="' + value + '"]')) continue;
|
||||
// Announce the value; synthesising a click hits nodes a re-render may
|
||||
// have replaced and left unbound.
|
||||
group.dispatchEvent(
|
||||
new CustomEvent("wrnexus:tabs:restore", { detail: { value: value } }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user