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 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 19:18:25 +05:30
co-authored by Claude Opus 5
parent ca2f9451ab
commit 52660cbb8e
13 changed files with 443 additions and 430 deletions
+5 -5
View File
@@ -86,11 +86,11 @@ size: string = "default"
</button>
<div class="wire-navbar__collapse {mobileOpen ? 'is-open' : ''}">
<nav class="wire-navbar__menus" aria-label="{label}">
<nav class="wire-navbar__menus" aria-label="{label}" data-wrn-roving="horizontal">
{#each items as item}
{#if item.children && item.children.length}
<details class="wire-navbar__dropdown wire-navbar__dropdown--{item.type || 'dropdown'}" name="wire-navbar-menu" @toggle="toggleDropdown(event, item)">
<summary aria-current="{isItemActive(item) ? 'page' : ''}">
<summary data-wrn-roving-item="true" aria-current="{isItemActive(item) ? 'page' : 'false'}">
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
<span>{item.label}</span>
<span class="wire-navbar__chevron" aria-hidden="true"></span>
@@ -103,13 +103,13 @@ size: string = "default"
{#if child.label}<strong class="wire-navbar__group-title">{child.label}</strong>{/if}
{#if child.description}<small>{child.description}</small>{/if}
{#each child.children as nested}
<a href="{nested.href || '#'}" target="{nested.target || ''}" rel="{nested.rel || ''}" aria-current="{nested.value === active ? 'page' : ''}" @click="selectItem(nested, 3)">
<a href="{nested.href || '#'}" target="{nested.target || ''}" rel="{nested.rel || ''}" aria-current="{nested.value === active ? 'page' : 'false'}" @click="selectItem(nested, 3)">
{#if nested.icon}<span class="{nested.icon}" aria-hidden="true"></span>{/if}
<span><strong>{nested.label}</strong>{#if nested.description}<small>{nested.description}</small>{/if}</span>
</a>
{/each}
{:else}
<a href="{child.href || '#'}" target="{child.target || ''}" rel="{child.rel || ''}" aria-current="{child.value === active ? 'page' : ''}" @click="selectItem(child, 2)">
<a href="{child.href || '#'}" target="{child.target || ''}" rel="{child.rel || ''}" aria-current="{child.value === active ? 'page' : 'false'}" @click="selectItem(child, 2)">
{#if child.icon}<span class="{child.icon}" aria-hidden="true"></span>{/if}
<span><strong>{child.label}</strong>{#if child.description}<small>{child.description}</small>{/if}</span>
</a>
@@ -119,7 +119,7 @@ size: string = "default"
</div>
</details>
{:else}
<a class="wire-navbar__menu-link" href="{item.href || '#'}" target="{item.target || ''}" rel="{item.rel || ''}" aria-current="{item.value === active ? 'page' : ''}" @click="selectItem(item, 1)">
<a class="wire-navbar__menu-link" data-wrn-roving-item="true" href="{item.href || '#'}" target="{item.target || ''}" rel="{item.rel || ''}" aria-current="{item.value === active ? 'page' : 'false'}" @click="selectItem(item, 1)">
{#if item.icon}<span class="{item.icon}" aria-hidden="true"></span>{/if}
<span>{item.label}</span>
</a>