Skip link and main landmark on every route with TopNav #224
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "no-skip-link"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #223.
TopNav renders at the top of all eight authenticated routes, so a keyboard or screen-reader user tabbed through the whole navigation block again on every page. That is WCAG 2.2 SC 2.4.1 (Bypass Blocks, level A), and thereby EN 301 549 clause 9.2.4.1.
What changed
A shared
PageShell.vuewrapsTopNavinnldd-skip-link("Direct naar de inhoud") and puts the page content in a<main>:The seven plain routes (Inbox, Leeslijst, Instellingen, Concepten, Uitgaande post, Notities, Voorwaarden) go through it. Welkom keeps
nldd-page, which supplies its own<main>from its shadow root, and carries the wrapper in itsheaderslot; activating it focuses the slotted content, verified in the browser.Extracting the shell also removes the seven copies of
<div class="page-shell">and makes the skip link something a new route gets by construction.Login and AccountVerwijderd render no TopNav, so they need no skip link - but an axe scan showed both reporting
landmark-one-mainand tworegionviolations, with their heading and body text outside any landmark. Each is a single card, so the card became the<main>.Two constraints the shape depends on
href,nldd-skip-linkfocuses its ownnextElementSibling(addingtabindex="-1"and removing it again on blur), so<main>has to stay directly after the wrapper. On Inbox that movesSkipTourButtonandRetentionIntroBannerinside<main>.position: stickyhad nowhere left to stick. That rule is moved fromTopNav.vueto the wrapper rather than duplicated - the app's CSS shrinks by a line. Document-origin rules beat the shadow tree's:host { position: relative }, so no shadow-DOM reach-in is needed.topnav-sticky.spec.tsstill passes and now checks the wrapper as the sticky layer.nldd-page[sticky-header]would supply both the<main>and the sticky layer from the design system, but it takes over scrolling from the document, which breakswindow.scrollToinInbox.endTour(), the router scroll and drag-autoscroll. The exception is root-scroll mode, which comes fromnldd-app-view- tracked in #203, where I left a correction: on 0.8.77 a bare-page app derivesrootautomatically, so that adoption is cheaper than the issue assumes. Worth doing, but as its own change.Keeping the nav sticky is deliberate. NLDD's guidance is "de standaard is: geen sticky", so, measured: Leeslijst is 26347px over a 757px viewport - ~35 screens back to the only navigation in the app.
Verification
PageShell.test.tspins the sibling contract, so a component inserted between wrapper and<main>fails the build.skipLink.test.tsenumerates the route files that import TopNav and asserts each offers a skip link - a new route cannot reintroduce the gap silently.skip-link.spec.ts(e2e) tabs to the link, asserts it becomes visible (opacity: 1) while focused, presses Enter and assertsdocument.activeElementisMAIN; plus the sibling check on all seven routes.landmarks.spec.ts(e2e, new) runs axe-core over all nine routes forlandmark-one-mainandregion. Verified it fails when<main>is turned back into a<div>. axe goes in viaaddInitScript, since the app's CSP blocks an injected inline script.Before:
landmark-one-mainon every route. After: clean on all nine.A full axe scan still reports
page-has-heading-oneon Inbox, Leeslijst and Instellingen. That is a heading-structure question needing a design call, filed as #230;landmarks.spec.tsis scoped to the two landmark rules so that gap stays visible instead of being allowlisted away.npm run typecheck, 663 vitest tests,npm run buildand eslint (0 errors) are green. Playwright: 67 passed, 1 failed -encrypted-export-roundtrip, which fails identically on the untouched tree (a checkbox inside RecoveryKeyModal) and passes in CI, so it is pre-existing and unrelated.Note the skip link is the second tab stop rather than the first when
MmStatusBannershows its "Verbind nu" link. That banner is a single alert action, not a repeated block, so 2.4.1 is satisfied; the e2e test allows for it explicitly.3ca9a4cbec0ba2f3e52e0ba2f3e52eda0875e0f4