Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3116de354 |
@@ -89,7 +89,7 @@
|
|||||||
"packages/ui/components/StrongPassword.wrn": "c7c5ef26ece6170dd7db9882f0dc98cb2e4607f1e5d43eb3fd39e5d15bbd3a2e",
|
"packages/ui/components/StrongPassword.wrn": "c7c5ef26ece6170dd7db9882f0dc98cb2e4607f1e5d43eb3fd39e5d15bbd3a2e",
|
||||||
"packages/ui/components/StyledIcon.wrn": "4a4504e357dee9dd0418edbe85fc90b824ccdb3752123a2125da95b77bf0b336",
|
"packages/ui/components/StyledIcon.wrn": "4a4504e357dee9dd0418edbe85fc90b824ccdb3752123a2125da95b77bf0b336",
|
||||||
"packages/ui/components/Switch.wrn": "ccb74599fab72b0d68b09a7f1f90b7732cb2f9cbed67a84219e897575ce30c28",
|
"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/TextLink.wrn": "30782039293eb36d63b7b3a4f32a71a47177a3a68c7e90184be7cf7b4385eb19",
|
||||||
"packages/ui/components/Textarea.wrn": "ddf0b4f124b2cf0c0ab3d820d3ac0085f7c20466e977231949be264cd0cee8cf",
|
"packages/ui/components/Textarea.wrn": "ddf0b4f124b2cf0c0ab3d820d3ac0085f7c20466e977231949be264cd0cee8cf",
|
||||||
"packages/ui/components/TimePicker.wrn": "2e8e7a90f6b6069a07e7ffd2725ba1e1031e84d55a4f1254025befbb314fa695",
|
"packages/ui/components/TimePicker.wrn": "2e8e7a90f6b6069a07e7ffd2725ba1e1031e84d55a4f1254025befbb314fa695",
|
||||||
|
|||||||
@@ -3193,40 +3193,6 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
syncRovingGroups();
|
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) {
|
function hydrateScopes(root) {
|
||||||
var host = root || document;
|
var host = root || document;
|
||||||
|
|
||||||
@@ -5445,7 +5411,6 @@ export const REACTIVE_RUNTIME = String.raw`
|
|||||||
setupAnchoredOverlays();
|
setupAnchoredOverlays();
|
||||||
setupModalDialogs();
|
setupModalDialogs();
|
||||||
setupRovingFocus();
|
setupRovingFocus();
|
||||||
setupTabUrlSync();
|
|
||||||
window.__wrnexusRepositionAnchored = repositionAnchored;
|
window.__wrnexusRepositionAnchored = repositionAnchored;
|
||||||
window.__wrnexusHydrateScopes = hydrateScopes;
|
window.__wrnexusHydrateScopes = hydrateScopes;
|
||||||
window.__wrnexusInvalidateClientModule = function (url) { clientModuleCache.delete(url); };
|
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"));
|
a.dispatchEvent(keydown(win, "ArrowRight"));
|
||||||
expect(doc.activeElement!.id).toBe("a");
|
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)
|
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() {
|
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) {
|
if (activeValue) {
|
||||||
return activeValue
|
return activeValue
|
||||||
}
|
}
|
||||||
if (active) {
|
return defaultValue()
|
||||||
return active
|
|
||||||
}
|
|
||||||
var list = itemList()
|
|
||||||
return list.length ? valueOf(list[0], 0) : ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The selection this instance started with. The runtime falls back to it
|
// The selection this instance started with. The runtime falls back to it
|
||||||
@@ -67,34 +75,6 @@ component Tabs {
|
|||||||
return orientation === "vertical" ? "vertical" : "horizontal"
|
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) {
|
client function selectTab(item, index, sourceEvent) {
|
||||||
if (item.disabled) {
|
if (item.disabled) {
|
||||||
return
|
return
|
||||||
@@ -123,9 +103,6 @@ component Tabs {
|
|||||||
data-size='{size}'
|
data-size='{size}'
|
||||||
data-mode='{mode}'
|
data-mode='{mode}'
|
||||||
data-param='{param}'
|
data-param='{param}'
|
||||||
data-wrn-tabs-param='{mode === "url" ? param : ""}'
|
|
||||||
data-wrn-tabs-default='{defaultValue()}'
|
|
||||||
@wrnexus:tabs:restore='applyUrlValue(event)'
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="wire-tabs__list"
|
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;
|
const root = dom.querySelector(".wire-tabs") as HTMLElement;
|
||||||
expect(root.getAttribute("data-mode")).toBe("url");
|
expect(root.getAttribute("data-mode")).toBe("url");
|
||||||
expect(root.getAttribute("data-param")).toBe("tab");
|
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 () => {
|
test("tabs vertical orientation switches the roving axis", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user