feat(ui): build the Stepper component

Replaces a scaffold that rendered bare anchors with ordered steps: complete,
current and upcoming status derived from the active index, horizontal or
vertical, optional icons, and indexed named slots (step-0, step-1, ...) for
authoring a step body by hand.

Also tightens the roving contract from the previous commit. A template writes
data-wrn-roving="" or data-wrn-roving-item="false" to mean not this time, but
a bare [attr] selector matches either, so a read-only stepper would still have
taken arrow-key focus. The container now requires a named axis, while a bare
item marker still counts as opted in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 16:44:12 +05:30
co-authored by Claude Opus 5
parent b67a5e43eb
commit 45ef035bae
5 changed files with 337 additions and 13 deletions
+47
View File
@@ -2493,3 +2493,50 @@ test("pagination survives an empty dataset", async () => {
const dom = mountHtml(html);
expect(dom.querySelector(".wire-pagination")).not.toBeNull();
});
test("stepper marks complete, current and upcoming steps", async () => {
const source = readFileSync(uiComponentPath("Stepper"), "utf8");
const html = await renderComponent(source, {
steps: [
{ label: "Account", description: "Your details" },
{ label: "Billing", description: "Payment method" },
{ label: "Confirm", description: "Review and submit" },
],
active: 1,
});
const dom = mountHtml(html);
const items = [...dom.querySelectorAll(".wire-stepper__step")];
expect(items).toHaveLength(3);
expect(items[0]!.getAttribute("data-status")).toBe("complete");
expect(items[1]!.getAttribute("data-status")).toBe("current");
expect(items[2]!.getAttribute("data-status")).toBe("upcoming");
expect(items[1]!.getAttribute("aria-current")).toBe("step");
expect(dom.querySelector(".wire-stepper")!.tagName.toLowerCase()).toBe("ol");
});
test("stepper renders vertically and clamps an out-of-range active index", async () => {
const source = readFileSync(uiComponentPath("Stepper"), "utf8");
const html = await renderComponent(source, {
steps: [{ label: "One" }, { label: "Two" }],
active: 99,
orientation: "vertical",
});
const dom = mountHtml(html);
const root = dom.querySelector(".wire-stepper") as HTMLElement;
expect(root.getAttribute("data-orientation")).toBe("vertical");
const items = [...dom.querySelectorAll(".wire-stepper__step")];
expect(items[1]!.getAttribute("data-status")).toBe("current");
});
test("clickable stepper opts into roving focus", async () => {
const source = readFileSync(uiComponentPath("Stepper"), "utf8");
const html = await renderComponent(source, {
steps: [{ label: "One" }, { label: "Two" }],
active: 0,
clickable: true,
});
const dom = mountHtml(html);
expect(dom.querySelector('[data-wrn-roving="horizontal"]')).not.toBeNull();
expect(dom.querySelectorAll("[data-wrn-roving-item]").length).toBe(2);
});