Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3116de354 |
@@ -89,7 +89,7 @@
|
||||
"packages/ui/components/StrongPassword.wrn": "c7c5ef26ece6170dd7db9882f0dc98cb2e4607f1e5d43eb3fd39e5d15bbd3a2e",
|
||||
"packages/ui/components/StyledIcon.wrn": "4a4504e357dee9dd0418edbe85fc90b824ccdb3752123a2125da95b77bf0b336",
|
||||
"packages/ui/components/Switch.wrn": "ccb74599fab72b0d68b09a7f1f90b7732cb2f9cbed67a84219e897575ce30c28",
|
||||
"packages/ui/components/Tabs.wrn": "37e125c6f6ec2c3f58a22cd9203cf75de40289c6be39cdbe17afd73e2d82b566",
|
||||
"packages/ui/components/Tabs.wrn": "4fa0c0854700532960043290700d7cf99663ec41692068101f0aae4c2b1a1181",
|
||||
"packages/ui/components/TextLink.wrn": "30782039293eb36d63b7b3a4f32a71a47177a3a68c7e90184be7cf7b4385eb19",
|
||||
"packages/ui/components/Textarea.wrn": "ddf0b4f124b2cf0c0ab3d820d3ac0085f7c20466e977231949be264cd0cee8cf",
|
||||
"packages/ui/components/TimePicker.wrn": "2e8e7a90f6b6069a07e7ffd2725ba1e1031e84d55a4f1254025befbb314fa695",
|
||||
|
||||
@@ -3193,40 +3193,6 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
syncRovingGroups();
|
||||
}
|
||||
|
||||
/*
|
||||
* 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]");
|
||||
for (var index = 0; index < groups.length; index += 1) {
|
||||
var group = groups[index];
|
||||
var param = group.getAttribute("data-wrn-tabs-param");
|
||||
if (!param) continue;
|
||||
var value = new URLSearchParams(window.location.search).get(param);
|
||||
// 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 } }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function setupTabUrlSync() {
|
||||
if (window.__wrnexusTabUrlBound) return;
|
||||
window.__wrnexusTabUrlBound = true;
|
||||
window.addEventListener("popstate", syncTabsFromUrl);
|
||||
// Once after hydration, so a shared link opens on the right tab.
|
||||
window.setTimeout(syncTabsFromUrl, 0);
|
||||
}
|
||||
|
||||
function hydrateScopes(root) {
|
||||
var host = root || document;
|
||||
|
||||
@@ -5445,7 +5411,6 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
setupAnchoredOverlays();
|
||||
setupModalDialogs();
|
||||
setupRovingFocus();
|
||||
setupTabUrlSync();
|
||||
window.__wrnexusRepositionAnchored = repositionAnchored;
|
||||
window.__wrnexusHydrateScopes = hydrateScopes;
|
||||
window.__wrnexusInvalidateClientModule = function (url) { clientModuleCache.delete(url); };
|
||||
|
||||
@@ -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"]);
|
||||
});
|
||||
|
||||
@@ -38,15 +38,23 @@ component Tabs {
|
||||
return String(item.value || item.id || index)
|
||||
}
|
||||
|
||||
/*
|
||||
* In url mode the query parameter is the source of truth, not local state.
|
||||
*
|
||||
* The client router owns popstate and swaps the whole page on back and
|
||||
* forward, which discards component state anyway. Reading the URL means
|
||||
* the right tab simply falls out of whatever render happens next, with no
|
||||
* listener to lose and nothing to keep in step.
|
||||
*/
|
||||
shared function currentValue() {
|
||||
if (mode === "url" && typeof window !== "undefined" && window.location) {
|
||||
var fromUrl = new URLSearchParams(window.location.search).get(param || "tab")
|
||||
return fromUrl ? fromUrl : defaultValue()
|
||||
}
|
||||
if (activeValue) {
|
||||
return activeValue
|
||||
}
|
||||
if (active) {
|
||||
return active
|
||||
}
|
||||
var list = itemList()
|
||||
return list.length ? valueOf(list[0], 0) : ""
|
||||
return defaultValue()
|
||||
}
|
||||
|
||||
// The selection this instance started with. The runtime falls back to it
|
||||
@@ -67,34 +75,6 @@ component Tabs {
|
||||
return orientation === "vertical" ? "vertical" : "horizontal"
|
||||
}
|
||||
|
||||
// Back and forward arrive here. The runtime announces the value the URL
|
||||
// now names; applying it must not write history, or stepping back would
|
||||
// push a new entry and trap the user.
|
||||
client function applyUrlValue(sourceEvent) {
|
||||
var detail = sourceEvent ? sourceEvent.detail : null
|
||||
var value = detail ? detail.value : ""
|
||||
if (!value) {
|
||||
return
|
||||
}
|
||||
var list = itemList()
|
||||
var found = -1
|
||||
for (var index = 0; index < list.length; index += 1) {
|
||||
if (valueOf(list[index], index) === value) {
|
||||
found = index
|
||||
}
|
||||
}
|
||||
if (found === -1) {
|
||||
return
|
||||
}
|
||||
// Idempotent against our own state rather than a DOM attribute the
|
||||
// re-render owns: popstate can fire for a value already selected.
|
||||
if (value === currentValue()) {
|
||||
return
|
||||
}
|
||||
activeValue = value
|
||||
output.change({ value: value, item: list[found], index: found })
|
||||
}
|
||||
|
||||
client function selectTab(item, index, sourceEvent) {
|
||||
if (item.disabled) {
|
||||
return
|
||||
@@ -123,9 +103,6 @@ component Tabs {
|
||||
data-size='{size}'
|
||||
data-mode='{mode}'
|
||||
data-param='{param}'
|
||||
data-wrn-tabs-param='{mode === "url" ? param : ""}'
|
||||
data-wrn-tabs-default='{defaultValue()}'
|
||||
@wrnexus:tabs:restore='applyUrlValue(event)'
|
||||
>
|
||||
<div
|
||||
class="wire-tabs__list"
|
||||
|
||||
@@ -2678,6 +2678,16 @@ test("tabs in url mode declare the query parameter they sync to", async () => {
|
||||
const root = dom.querySelector(".wire-tabs") as HTMLElement;
|
||||
expect(root.getAttribute("data-mode")).toBe("url");
|
||||
expect(root.getAttribute("data-param")).toBe("tab");
|
||||
|
||||
/*
|
||||
* In url mode the query parameter is the source of truth rather than
|
||||
* component state. The client router owns popstate and swaps the whole page
|
||||
* on back and forward, discarding component state, so the selection has to
|
||||
* fall out of the URL for history to work at all.
|
||||
*/
|
||||
const source2 = readFileSync(uiComponentPath("Tabs"), "utf8");
|
||||
expect(source2).toContain("URLSearchParams(window.location.search)");
|
||||
expect(source2).toContain("history.pushState");
|
||||
});
|
||||
|
||||
test("tabs vertical orientation switches the roving axis", async () => {
|
||||
|
||||
Reference in New Issue
Block a user