From 52660cbb8efad1e536a5a0870928788cf2a316fb Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Fri, 7 Aug 2026 19:18:25 +0530 Subject: [PATCH] feat(ui): build Scrollspy, fix aria-current across the navigation group Scrollspy replaces a scaffold that rendered bare anchors. The runtime observes the sections the links point at and writes the marker straight onto the links: an IntersectionObserver callback fires long after the client function that registered it returned, so a state write there would be dropped. Navbar and Breadcrumb both emitted aria-current="" for every inactive link. That is not a valid value -- the attribute takes a token or must be absent -- so every link claimed a state it did not have. Breadcrumb had it too, despite being the strongest component in the group. Navbar also takes roving arrow-key focus across its menu bar. Co-Authored-By: Claude Opus 5 --- docs/ui-visual-contract-0.8.json | 6 +- .../app/pages/components/scrollspy.wrn | 398 ++---------------- .../component-showcase/app/pages/index.wrn | 4 +- .../app/pages/navigation.wrn | 4 +- .../scripts/showcase-profiles.mjs | 32 ++ .../component-showcase/showcase-manifest.json | 6 +- packages/csr/src/reactive-runtime.ts | 139 ++++-- packages/ui/COMPONENTS.md | 4 +- packages/ui/component-reference.json | 24 +- packages/ui/components/Breadcrumb.wrn | 2 +- packages/ui/components/Navbar.wrn | 10 +- packages/ui/components/Scrollspy.wrn | 163 ++++++- packages/ui/test/ui.test.ts | 81 ++++ 13 files changed, 443 insertions(+), 430 deletions(-) diff --git a/docs/ui-visual-contract-0.8.json b/docs/ui-visual-contract-0.8.json index dfafe286..7fdf5602 100644 --- a/docs/ui-visual-contract-0.8.json +++ b/docs/ui-visual-contract-0.8.json @@ -13,7 +13,7 @@ "packages/ui/components/BackToTop.wrn": "b12c56ec8b7aabad68cbdc47f4b3237e2c21a61398f0aa51f9add6229b7357cd", "packages/ui/components/Badge.wrn": "6b55b4d85100d605cf168857aec1dc57da62c1f6a0a5cd6dd877023f24b189cc", "packages/ui/components/Blockquote.wrn": "6016dd4450de7cbf5c3cb413599baa2e502321e8eedef54439a0df6d0aae8bcb", - "packages/ui/components/Breadcrumb.wrn": "8bf6239b95b382ce68f8411aac0d364f67a1c278dfbbf08f1501cd0482751e30", + "packages/ui/components/Breadcrumb.wrn": "df27101d41cf018d55b6909e0399286a4661637140e341c8624615051d485142", "packages/ui/components/ButtonGroup.wrn": "0d97fbeed531d1d8ef4b59531923b47b34100d0de71d7bf524b02c668920b3d8", "packages/ui/components/CTASection.wrn": "2d2241d4f1018cfd2f8ea1ddd5fa8c8fcb8d1bbb2d4e4498cb9f39ff47a3801c", "packages/ui/components/Card.wrn": "4370df341235819b8a968a3f812209c2e6c29e4471d06578aaa5b577fb6e2dc8", @@ -66,7 +66,7 @@ "packages/ui/components/MetricGrid.wrn": "6018a98c10628ed240ed236ed916c0ff60994d192c3aadafd10bc876be0f9364", "packages/ui/components/Modal.wrn": "59d4ae9d6dd2700692d9edecfe53ee868e9f864363a4885961d1813cb2bee51f", "packages/ui/components/Nav.wrn": "7dc456b879e9248bde267b986cdcb138b8f3127d98b51e27bc9708129953f5e2", - "packages/ui/components/Navbar.wrn": "e68f9d3643e500e43124e9c7a6d4c7f6722657377ea7313f3e3cf5093a81b360", + "packages/ui/components/Navbar.wrn": "cf28cf4cdd23c85821d3302e6881115c2c14977e7cb7142d306cf559fea2a518", "packages/ui/components/PageHeader.wrn": "0761235a4924eec09877be40b292d27b70db22d210be7d8e989493165c20df86", "packages/ui/components/Pagination.wrn": "9ceb74745b159150b015bbbb1974c68e14a7d4b92bd03491cb23b8855aa07983", "packages/ui/components/PinInput.wrn": "5196f584de8d548a5dfa03688c3c95da3c948cead2662b05e927386ccea74299", @@ -77,7 +77,7 @@ "packages/ui/components/Radio.wrn": "0b2d38a5aee3e859280e4590fa1d509d789cc2239082054872fa342817735f1b", "packages/ui/components/RangeSlider.wrn": "21eb7a5df0ee2cb5e78f628eb920265998eb9f1a4933d4e5c57db0323fd66b41", "packages/ui/components/Rating.wrn": "a2d74dc748fc7d98892684c760518b87fa65411b1102e3611252b87245b3ffcb", - "packages/ui/components/Scrollspy.wrn": "bbe0788f63c2bc8850649c1dee78391d485a1c93876015bc536d838273d5fae4", + "packages/ui/components/Scrollspy.wrn": "76d1c17580c8d460f1222ce97585bdcac9779141f2f30d5460c84ff9f75413d8", "packages/ui/components/SearchBox.wrn": "315f54f1feaaa47a815d1e2d7a35b492f09fbfadfc6d37228e1009e19e0652ad", "packages/ui/components/Section.wrn": "21485de07d8bb986837a65c61c5dd01ba6c2f3d4e37d58c154a584c7b58de0ed", "packages/ui/components/SectionHeader.wrn": "512cb96636ee1f0540a0f4b4c75603fc0fe7536e7ed7ed7dc82173a16f48a4d3", diff --git a/examples/component-showcase/app/pages/components/scrollspy.wrn b/examples/component-showcase/app/pages/components/scrollspy.wrn index 9845036e..66c40564 100644 --- a/examples/component-showcase/app/pages/components/scrollspy.wrn +++ b/examples/component-showcase/app/pages/components/scrollspy.wrn @@ -1,12 +1,12 @@ // Generated by scripts/generate-showcase.mjs. Do not edit directly. page ScrollspyDetail { layout = "showcase" - state playground_size = ctx.url.searchParams.get("pg_size") ?? "default" state playground_color = ctx.url.searchParams.get("pg_color") ?? "primary" - state playground_label = ctx.url.searchParams.get("pg_label") ?? "Scrollspy" + state playground_size = ctx.url.searchParams.get("pg_size") ?? "default" state playground_items = JSON.parse(ctx.url.searchParams.get("pg_items") ?? "[{\"id\":\"scrollspy-0-1\",\"key\":\"scrollspy-0-1\",\"value\":\"primary\",\"label\":\"Primary workflow\",\"title\":\"Primary workflow\",\"description\":\"A clean default configuration for everyday product interfaces.\",\"text\":\"A configurable sample message.\",\"href\":\"#scrollspy-demo\",\"actionHref\":\"#scrollspy-demo\",\"actionLabel\":\"View details\",\"icon\":\"icon-[lucide--sparkles]\",\"iconClass\":\"icon-[lucide--sparkles]\",\"variant\":\"primary\",\"status\":\"Active\",\"time\":\"09:30\",\"current\":true,\"selected\":true,\"checked\":true,\"disabled\":false,\"outgoing\":false,\"open\":true,\"number\":1,\"count\":16,\"percentage\":72,\"color\":\"#7c3aed\",\"target\":\"_self\",\"ariaLabel\":\"Open primary workflow 1\",\"items\":[{\"label\":\"Nested option A\",\"value\":\"nested-a\"},{\"label\":\"Nested option B\",\"value\":\"nested-b\"}],\"links\":[{\"label\":\"Documentation\",\"href\":\"#documentation\"},{\"label\":\"API reference\",\"href\":\"#api-reference\"}],\"values\":[\"Included\",\"Standard\"]},{\"id\":\"scrollspy-0-2\",\"key\":\"scrollspy-0-2\",\"value\":\"secondary\",\"label\":\"Additional option\",\"title\":\"Supporting example\",\"description\":\"A clean default configuration for everyday product interfaces.\",\"text\":\"Another configurable message.\",\"href\":\"#scrollspy-demo\",\"actionHref\":\"#scrollspy-demo\",\"actionLabel\":\"View details\",\"icon\":\"icon-[lucide--sparkles]\",\"iconClass\":\"icon-[lucide--sparkles]\",\"variant\":\"primary\",\"status\":\"Active\",\"time\":\"09:30\",\"current\":false,\"selected\":false,\"checked\":false,\"disabled\":false,\"outgoing\":true,\"open\":true,\"number\":2,\"count\":32,\"percentage\":72,\"color\":\"#7c3aed\",\"target\":\"_self\",\"ariaLabel\":\"Open primary workflow 2\",\"items\":[{\"label\":\"Nested option A\",\"value\":\"nested-a\"},{\"label\":\"Nested option B\",\"value\":\"nested-b\"}],\"links\":[{\"label\":\"Documentation\",\"href\":\"#documentation\"},{\"label\":\"API reference\",\"href\":\"#api-reference\"}],\"values\":[\"Included\",\"Standard\"]}]") state playground_active = ctx.url.searchParams.get("pg_active") ?? "" - state playground_orientation = ctx.url.searchParams.get("pg_orientation") ?? "horizontal" + state playground_label = ctx.url.searchParams.get("pg_label") ?? "Scrollspy" + state playground_heading = ctx.url.searchParams.get("pg_heading") ?? "" state playground_class = ctx.url.searchParams.get("pg_class") ?? "showcase-instance showcase-instance--1" seo { title = "Scrollspy" @@ -30,7 +30,7 @@ page ScrollspyDetail {
Live preview
-
+
Component code
<Scrollspy
@@ -142,6 +142,7 @@ page ScrollspyDetail {
       ]
     }
   ]'
+  label="Scrollspy"
 />
@@ -149,7 +150,7 @@ page ScrollspyDetail {
Component props7 controls
-
+]
@@ -265,382 +266,75 @@ page ScrollspyDetail {
Live examples

Designed for real product surfaces

Compare configurations and resize the browser to check responsive behavior.

-
Recommended

Production default

Balanced spacing, hierarchy, and content for the most common product workflow.

+
Recommended

Table of contents

Each href points at an element on the page. The runtime observes those elements and moves aria-current to the link for whichever one is in view, so the marker follows the reader without any component state.

01
Live preview
-
+
-
+
Component usage.wrn
<Scrollspy
   items='[
     {
-      "id": "scrollspy-0-1",
-      "key": "scrollspy-0-1",
-      "value": "primary",
-      "label": "Primary workflow",
-      "title": "Primary workflow",
-      "description": "A clean default configuration for everyday product interfaces.",
-      "text": "A configurable sample message.",
-      "href": "#scrollspy-demo",
-      "actionHref": "#scrollspy-demo",
-      "actionLabel": "View details",
-      "icon": "icon-[lucide--sparkles]",
-      "iconClass": "icon-[lucide--sparkles]",
-      "variant": "primary",
-      "status": "Active",
-      "time": "09:30",
-      "current": true,
-      "selected": true,
-      "checked": true,
-      "disabled": false,
-      "outgoing": false,
-      "open": true,
-      "number": 1,
-      "count": 16,
-      "percentage": 72,
-      "color": "#7c3aed",
-      "target": "_self",
-      "ariaLabel": "Open primary workflow 1",
-      "items": [
-        {
-          "label": "Nested option A",
-          "value": "nested-a"
-        },
-        {
-          "label": "Nested option B",
-          "value": "nested-b"
-        }
-      ],
-      "links": [
-        {
-          "label": "Documentation",
-          "href": "#documentation"
-        },
-        {
-          "label": "API reference",
-          "href": "#api-reference"
-        }
-      ],
-      "values": [
-        "Included",
-        "Standard"
-      ]
+      "label": "Overview",
+      "href": "#overview"
     },
     {
-      "id": "scrollspy-0-2",
-      "key": "scrollspy-0-2",
-      "value": "secondary",
-      "label": "Additional option",
-      "title": "Supporting example",
-      "description": "A clean default configuration for everyday product interfaces.",
-      "text": "Another configurable message.",
-      "href": "#scrollspy-demo",
-      "actionHref": "#scrollspy-demo",
-      "actionLabel": "View details",
-      "icon": "icon-[lucide--sparkles]",
-      "iconClass": "icon-[lucide--sparkles]",
-      "variant": "primary",
-      "status": "Active",
-      "time": "09:30",
-      "current": false,
-      "selected": false,
-      "checked": false,
-      "disabled": false,
-      "outgoing": true,
-      "open": true,
-      "number": 2,
-      "count": 32,
-      "percentage": 72,
-      "color": "#7c3aed",
-      "target": "_self",
-      "ariaLabel": "Open primary workflow 2",
-      "items": [
-        {
-          "label": "Nested option A",
-          "value": "nested-a"
-        },
-        {
-          "label": "Nested option B",
-          "value": "nested-b"
-        }
-      ],
-      "links": [
-        {
-          "label": "Documentation",
-          "href": "#documentation"
-        },
-        {
-          "label": "API reference",
-          "href": "#api-reference"
-        }
-      ],
-      "values": [
-        "Included",
-        "Standard"
-      ]
+      "label": "Installation",
+      "href": "#installation"
+    },
+    {
+      "label": "Usage",
+      "href": "#usage"
+    },
+    {
+      "label": "API",
+      "href": "#api"
     }
   ]'
+  active="#overview"
+  label="Scrollspy"
+  heading="On this page"
 />
-
Dense UI

Compact application

A tighter variation for dashboards, side panels, tables, and operational interfaces.

+
Advanced

Compact rail

On a phone the rail becomes a horizontal strip that scrolls, so a long contents list costs one line rather than a screenful.

02
Live preview
-
+
-
+
Component usage.wrn
<Scrollspy
   size="sm"
+  items='[
+    {
+      "label": "Getting started",
+      "href": "#getting-started"
+    },
+    {
+      "label": "Configuration",
+      "href": "#configuration"
+    },
+    {
+      "label": "Troubleshooting",
+      "href": "#troubleshooting"
+    }
+  ]'
+  active="#configuration"
   label="Compact example"
-  items='[
-    {
-      "id": "scrollspy-1-1",
-      "key": "scrollspy-1-1",
-      "value": "primary",
-      "label": "Secondary workflow",
-      "title": "Secondary workflow",
-      "description": "A compact configuration designed for dense application layouts.",
-      "text": "A configurable sample message.",
-      "href": "#scrollspy-demo",
-      "actionHref": "#scrollspy-demo",
-      "actionLabel": "View details",
-      "icon": "icon-[lucide--zap]",
-      "iconClass": "icon-[lucide--zap]",
-      "variant": "secondary",
-      "status": "Active",
-      "time": "10:15",
-      "current": true,
-      "selected": true,
-      "checked": true,
-      "disabled": false,
-      "outgoing": false,
-      "open": true,
-      "number": 1,
-      "count": 24,
-      "percentage": 48,
-      "color": "#0284c7",
-      "target": "_self",
-      "ariaLabel": "Open secondary workflow 1",
-      "items": [
-        {
-          "label": "Nested option A",
-          "value": "nested-a"
-        },
-        {
-          "label": "Nested option B",
-          "value": "nested-b"
-        }
-      ],
-      "links": [
-        {
-          "label": "Documentation",
-          "href": "#documentation"
-        },
-        {
-          "label": "API reference",
-          "href": "#api-reference"
-        }
-      ],
-      "values": [
-        "Included",
-        "Standard"
-      ]
-    },
-    {
-      "id": "scrollspy-1-2",
-      "key": "scrollspy-1-2",
-      "value": "secondary",
-      "label": "Additional option",
-      "title": "Supporting example",
-      "description": "A compact configuration designed for dense application layouts.",
-      "text": "Another configurable message.",
-      "href": "#scrollspy-demo",
-      "actionHref": "#scrollspy-demo",
-      "actionLabel": "View details",
-      "icon": "icon-[lucide--zap]",
-      "iconClass": "icon-[lucide--zap]",
-      "variant": "secondary",
-      "status": "Active",
-      "time": "10:15",
-      "current": false,
-      "selected": false,
-      "checked": false,
-      "disabled": false,
-      "outgoing": true,
-      "open": true,
-      "number": 2,
-      "count": 48,
-      "percentage": 48,
-      "color": "#0284c7",
-      "target": "_self",
-      "ariaLabel": "Open secondary workflow 2",
-      "items": [
-        {
-          "label": "Nested option A",
-          "value": "nested-a"
-        },
-        {
-          "label": "Nested option B",
-          "value": "nested-b"
-        }
-      ],
-      "links": [
-        {
-          "label": "Documentation",
-          "href": "#documentation"
-        },
-        {
-          "label": "API reference",
-          "href": "#api-reference"
-        }
-      ],
-      "values": [
-        "Included",
-        "Standard"
-      ]
-    }
-  ]'
-/>
-
-
-
-
-
-
Extended

Rich configuration

A more expressive variation using additional data, stronger emphasis, and optional states.

- 03 -
-
-
-
Live preview
- -
-
-
-
Component usage.wrn
-
<Scrollspy
-  size="lg"
-  label="Advanced example"
-  items='[
-    {
-      "id": "scrollspy-2-1",
-      "key": "scrollspy-2-1",
-      "value": "primary",
-      "label": "Advanced workflow",
-      "title": "Advanced workflow",
-      "description": "A richer configuration with more supporting information and actions.",
-      "text": "A configurable sample message.",
-      "href": "#scrollspy-demo",
-      "actionHref": "#scrollspy-demo",
-      "actionLabel": "Explore workflow",
-      "icon": "icon-[lucide--circle-check]",
-      "iconClass": "icon-[lucide--circle-check]",
-      "variant": "success",
-      "status": "Complete",
-      "time": "11:45",
-      "current": true,
-      "selected": true,
-      "checked": true,
-      "disabled": false,
-      "outgoing": false,
-      "open": true,
-      "number": 1,
-      "count": 32,
-      "percentage": 91,
-      "color": "#059669",
-      "target": "_self",
-      "ariaLabel": "Open advanced workflow 1",
-      "items": [
-        {
-          "label": "Nested option A",
-          "value": "nested-a"
-        },
-        {
-          "label": "Nested option B",
-          "value": "nested-b"
-        }
-      ],
-      "links": [
-        {
-          "label": "Documentation",
-          "href": "#documentation"
-        },
-        {
-          "label": "API reference",
-          "href": "#api-reference"
-        }
-      ],
-      "values": [
-        "Included",
-        "Unlimited"
-      ]
-    },
-    {
-      "id": "scrollspy-2-2",
-      "key": "scrollspy-2-2",
-      "value": "secondary",
-      "label": "Additional option",
-      "title": "Supporting example",
-      "description": "A richer configuration with more supporting information and actions.",
-      "text": "Another configurable message.",
-      "href": "#scrollspy-demo",
-      "actionHref": "#scrollspy-demo",
-      "actionLabel": "Explore workflow",
-      "icon": "icon-[lucide--circle-check]",
-      "iconClass": "icon-[lucide--circle-check]",
-      "variant": "success",
-      "status": "Complete",
-      "time": "11:45",
-      "current": false,
-      "selected": false,
-      "checked": false,
-      "disabled": true,
-      "outgoing": true,
-      "open": true,
-      "number": 2,
-      "count": 64,
-      "percentage": 91,
-      "color": "#059669",
-      "target": "_self",
-      "ariaLabel": "Open advanced workflow 2",
-      "items": [
-        {
-          "label": "Nested option A",
-          "value": "nested-a"
-        },
-        {
-          "label": "Nested option B",
-          "value": "nested-b"
-        }
-      ],
-      "links": [
-        {
-          "label": "Documentation",
-          "href": "#documentation"
-        },
-        {
-          "label": "API reference",
-          "href": "#api-reference"
-        }
-      ],
-      "values": [
-        "Included",
-        "Unlimited"
-      ]
-    }
-  ]'
+  heading="Sections"
 />
@@ -654,10 +348,10 @@ const component = document.querySelector("[data-ui-component=\"Scrollspy\"], [da if (component) registerOutputHandler(component, "change", (payload) => { console.log("change", payload) })
-
Component API

Props and configuration

All content and behavior shown above is supplied through these props and slots.

PropTypeDefaultRequired
sizestring"default"No
colorstring"primary"No
labelstring"Scrollspy"No
itemsunknown[][]No
activestring""No
orientationstring"horizontal"No
classstring""No
+
Component API

Props and configuration

All content and behavior shown above is supplied through these props and slots.

PropTypeDefaultRequired
colorstring"primary"No
sizestring"default"No
itemsunknown[][]No
activestring""No
labelstring"On this page"No
headingstring""No
classstring""No