From 6eeb459dd835fe0e66634d4a865a52ff3e7d10d7 Mon Sep 17 00:00:00 2001 From: Lars Francke Date: Thu, 3 Sep 2026 12:37:05 +0200 Subject: [PATCH 1/2] fix: Make keyboard focus visible and name the unnamed controls Seven controls in the chrome carried `outline: none` with no replacement, so keyboard focus was invisible on the burger menu, the nav toggles, the TOC links, the copy button and every `summary`. asciidoctor-tabs does the same to its tab list items; that one is overridden here because its stylesheet is imported first. The rings use `currentcolor` rather than a new variable, so the design token work has nothing extra to reconcile and contrast stays out of scope. The search opener was a `div`, so it was neither focusable nor named and `Ctrl+K` was the only way in. Making it a `button` requires the popover and its results list to become siblings, because a button may not contain interactive content, which also stops a click inside the popover bubbling back to the opener and reopening it. The two logos, the four social links, the toolbar home link and Asciidoctor's per-heading section anchors were all reachable with no accessible name. The anchors are empty by construction, with the glyph coming from CSS, so they are named from their heading at runtime. --- ui/src/css/base.css | 6 +++++- ui/src/css/doc.css | 6 +++++- ui/src/css/header.css | 11 ++++++++++- ui/src/css/nav.css | 12 ++++++++++-- ui/src/css/tabs.css | 7 +++++++ ui/src/css/toc.css | 6 +++++- ui/src/css/toolbar.css | 6 +++++- ui/src/js/08-search.js | 7 +++++++ ui/src/js/09-heading-anchors.js | 16 ++++++++++++++++ ui/src/partials/footer-content.hbs | 10 +++++----- ui/src/partials/header.hbs | 14 +++++++------- ui/src/partials/toolbar.hbs | 2 +- 12 files changed, 83 insertions(+), 20 deletions(-) create mode 100644 ui/src/js/09-heading-anchors.js diff --git a/ui/src/css/base.css b/ui/src/css/base.css index 70b28f1ad..2418b1fb8 100644 --- a/ui/src/css/base.css +++ b/ui/src/css/base.css @@ -88,7 +88,11 @@ button::-moz-focus-inner { summary { cursor: pointer; -webkit-tap-highlight-color: transparent; - outline: none; +} + +summary:focus-visible { + outline: 2px solid currentcolor; + outline-offset: 2px; } table { diff --git a/ui/src/css/doc.css b/ui/src/css/doc.css index 81babc982..6ff146206 100644 --- a/ui/src/css/doc.css +++ b/ui/src/css/doc.css @@ -863,7 +863,6 @@ background: none; border: none; color: inherit; - outline: none; padding: 0; font-size: inherit; line-height: inherit; @@ -871,6 +870,11 @@ height: 1em; } +.doc .source-toolbox .copy-button:focus-visible { + outline: 2px solid currentcolor; + outline-offset: 2px; +} + .doc .source-toolbox .copy-icon { flex: none; width: inherit; diff --git a/ui/src/css/header.css b/ui/src/css/header.css index 33ee316e3..55dbf8260 100644 --- a/ui/src/css/header.css +++ b/ui/src/css/header.css @@ -189,8 +189,13 @@ body { color: var(--navbar-menu-font-color-hover); } +/* font: inherit is required, not tidiness: .svg-icon sizes the magnifier in em, + * so the UA button font would shrink it from 16px to 13px. */ #search-button { + background: none; + border: none; cursor: pointer; + font: inherit; padding-right: 2rem; } @@ -201,7 +206,6 @@ body { .navbar-burger { background: none; border: none; - outline: none; line-height: 1; position: relative; padding: 0; @@ -213,6 +217,11 @@ body { min-width: 0; } +.navbar-burger:focus-visible { + outline: 2px solid currentcolor; + outline-offset: 2px; +} + .navbar-burger span { background-color: var(--navbar-font-color); height: 1.5px; diff --git a/ui/src/css/nav.css b/ui/src/css/nav.css index 1c039cb33..e30cdddbc 100644 --- a/ui/src/css/nav.css +++ b/ui/src/css/nav.css @@ -117,7 +117,6 @@ height: 1em; margin-right: -0.5rem; opacity: 0.75; - outline: none; padding: 0; position: sticky; top: calc((var(--nav-line-height) - 1 + 0.5) * 1rem); @@ -125,6 +124,11 @@ width: 1em; } +.nav-menu-toggle:focus-visible { + outline: 2px solid currentcolor; + outline-offset: 2px; +} + .nav-menu-toggle.is-active { background-image: url(../img/octicons-16.svg#view-fold); } @@ -173,7 +177,6 @@ .nav-item-toggle { background: transparent url(../img/caret.svg) no-repeat center / 50%; border: none; - outline: none; line-height: inherit; padding: 0; position: absolute; @@ -183,6 +186,11 @@ margin-left: calc(var(--nav-line-height) * -1em); } +.nav-item-toggle:focus-visible { + outline: 2px solid currentcolor; + outline-offset: 2px; +} + .nav-item.is-active > .nav-item-toggle { transform: rotate(90deg); } diff --git a/ui/src/css/tabs.css b/ui/src/css/tabs.css index 215f17c18..94b1c63ba 100644 --- a/ui/src/css/tabs.css +++ b/ui/src/css/tabs.css @@ -2,3 +2,10 @@ .tabs { margin-top: 1rem; } + +/* asciidoctor-tabs sets `outline: none` on its tab list items, so keyboard users + * cannot see which tab they are on. Imported before this file, so this wins. */ +.tablist > ul li:focus-visible { + outline: 2px solid currentcolor; + outline-offset: 2px; +} diff --git a/ui/src/css/toc.css b/ui/src/css/toc.css index 01a77c2aa..94f93d3fd 100644 --- a/ui/src/css/toc.css +++ b/ui/src/css/toc.css @@ -76,7 +76,11 @@ .sidebar.toc .toc-menu a { display: block; - outline: none; +} + +.sidebar.toc .toc-menu a:focus-visible { + outline: 2px solid currentcolor; + outline-offset: 2px; } .toc .toc-menu a:hover { diff --git a/ui/src/css/toolbar.css b/ui/src/css/toolbar.css index f545901d4..d3470e3f2 100644 --- a/ui/src/css/toolbar.css +++ b/ui/src/css/toolbar.css @@ -30,7 +30,6 @@ background: url(../img/menu.svg) no-repeat 50% 47.5%; background-size: 49%; border: none; - outline: none; line-height: inherit; padding: 0; height: var(--toolbar-height); @@ -38,6 +37,11 @@ margin-right: -0.25rem; } +.nav-toggle:focus-visible { + outline: 2px solid currentcolor; + outline-offset: 2px; +} + @media screen and (min-width: 1024px) { .nav-toggle { display: none; diff --git a/ui/src/js/08-search.js b/ui/src/js/08-search.js index 1e79b397b..d9feb140a 100644 --- a/ui/src/js/08-search.js +++ b/ui/src/js/08-search.js @@ -4,6 +4,7 @@ function openSearchPopover () { document.getElementById('search-background').style.display = 'block' document.getElementById('search').style.display = 'block' + setSearchExpanded(true) // the search assets load on first use (see footer-scripts.hbs) window.loadSearch().then(function () { @@ -17,6 +18,12 @@ function closeSearchPopover () { document.getElementById('search-background').style.display = 'none' document.getElementById('search').style.display = 'none' + setSearchExpanded(false) + } + + function setSearchExpanded (expanded) { + var button = document.getElementById('search-button') + if (button) button.setAttribute('aria-expanded', String(expanded)) } function focusSearchInput () { diff --git a/ui/src/js/09-heading-anchors.js b/ui/src/js/09-heading-anchors.js new file mode 100644 index 000000000..edc28f0d7 --- /dev/null +++ b/ui/src/js/09-heading-anchors.js @@ -0,0 +1,16 @@ +;(function () { + 'use strict' + + // Asciidoctor emits section anchors as an empty inside the + // heading, so they are links with no accessible name -- one per section, and + // axe flags every one. The glyph comes from CSS, so there is no text to find: + // name them after the heading they link to. + var anchors = document.querySelectorAll('.doc a.anchor') + Array.prototype.forEach.call(anchors, function (anchor) { + if (anchor.getAttribute('aria-label')) return + var heading = anchor.parentNode + if (!heading) return + var text = heading.textContent.trim() + if (text) anchor.setAttribute('aria-label', 'Link to ' + text) + }) +})() diff --git a/ui/src/partials/footer-content.hbs b/ui/src/partials/footer-content.hbs index b25259f7f..169d3b1ba 100644 --- a/ui/src/partials/footer-content.hbs +++ b/ui/src/partials/footer-content.hbs @@ -4,7 +4,7 @@