From 47496ac4320f81e95bc4a09bb57cf13b1d696880 Mon Sep 17 00:00:00 2001 From: Mike Swanson Date: Fri, 5 Jun 2026 17:42:46 -0700 Subject: [PATCH] =?UTF-8?q?fix(radio):=20keyboard=20a11y=20=E2=80=94=20ski?= =?UTF-8?q?p=20link,=20focus-visible,=20mobile-menu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit human-flow P0-P1 fixes for radio.azcomputerguru.com: - K1: skip-to-content link (first tab stop) + id/tabindex on
. - K2: global :focus-visible ring (accent outline) across links, buttons, inputs and player controls; reveal the seek-bar handle on focus. - K3: mobile menu a11y — aria-expanded/aria-controls, Escape closes and restores focus to the toggle, focus moves to first link on open. All token-based, no emojis. Not built (node_modules absent on this host). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../components/global/PersistentPlayer.tsx | 20 +++++++++++ .../website/src/layouts/BaseLayout.astro | 36 +++++++++++++++---- .../radio-show/website/src/styles/global.css | 34 ++++++++++++++++++ 3 files changed, 84 insertions(+), 6 deletions(-) diff --git a/projects/radio-show/website/src/components/global/PersistentPlayer.tsx b/projects/radio-show/website/src/components/global/PersistentPlayer.tsx index cff4b22..24cb0cf 100644 --- a/projects/radio-show/website/src/components/global/PersistentPlayer.tsx +++ b/projects/radio-show/website/src/components/global/PersistentPlayer.tsx @@ -644,6 +644,15 @@ export default function PersistentPlayer() { opacity: 1; } + .persistent-player__progress-bar:focus-visible { + outline: 2px solid var(--color-accent, hsl(200 85% 55%)); + outline-offset: 2px; + } + + .persistent-player__progress-bar:focus-visible .persistent-player__progress-handle { + opacity: 1; + } + [data-theme="light"] .persistent-player__progress-bar { background: hsl(220 12% 85% / 0.5); } @@ -755,6 +764,17 @@ export default function PersistentPlayer() { background: hsl(220 16% 92% / 0.6); } + .persistent-player__btn:focus-visible { + outline: 2px solid var(--color-accent, hsl(200 85% 55%)); + outline-offset: 2px; + color: var(--color-text-primary, hsl(220 10% 92%)); + } + + .persistent-player__btn--play:focus-visible { + color: hsl(220 20% 8%); + box-shadow: 0 0 16px hsl(200 85% 55% / 0.3); + } + .persistent-player__btn--play { width: 40px; height: 40px; diff --git a/projects/radio-show/website/src/layouts/BaseLayout.astro b/projects/radio-show/website/src/layouts/BaseLayout.astro index 7b9f771..d00ed63 100644 --- a/projects/radio-show/website/src/layouts/BaseLayout.astro +++ b/projects/radio-show/website/src/layouts/BaseLayout.astro @@ -51,12 +51,13 @@ const canonicalURL = new URL(Astro.url.pathname, Astro.site); + -
+
@@ -134,9 +135,32 @@ const canonicalURL = new URL(Astro.url.pathname, Astro.site); }); // Mobile menu toggle - document.getElementById('mobile-menu-toggle').addEventListener('click', () => { - document.querySelector('.nav-links').classList.toggle('open'); - document.getElementById('mobile-menu-toggle').classList.toggle('open'); + const menuToggle = document.getElementById('mobile-menu-toggle'); + const navLinks = document.querySelector('.nav-links'); + + function closeMobileMenu(returnFocus) { + navLinks.classList.remove('open'); + menuToggle.classList.remove('open'); + menuToggle.setAttribute('aria-expanded', 'false'); + if (returnFocus) { + menuToggle.focus(); + } + } + + menuToggle.addEventListener('click', () => { + const isOpen = navLinks.classList.toggle('open'); + menuToggle.classList.toggle('open', isOpen); + menuToggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); + if (isOpen) { + const firstLink = navLinks.querySelector('a'); + if (firstLink) firstLink.focus(); + } + }); + + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && navLinks.classList.contains('open')) { + closeMobileMenu(true); + } }); // Fade-in on scroll diff --git a/projects/radio-show/website/src/styles/global.css b/projects/radio-show/website/src/styles/global.css index eec8b61..1a47876 100644 --- a/projects/radio-show/website/src/styles/global.css +++ b/projects/radio-show/website/src/styles/global.css @@ -31,6 +31,40 @@ a:hover { color: var(--color-accent-hover); } +/* Global keyboard focus styles — strong, consistent ring on the dark theme. + Uses outline so it layers over any existing box-shadow focus rings. */ +a:focus-visible, +button:focus-visible, +[tabindex]:focus-visible, +input:focus-visible, +select:focus-visible, +textarea:focus-visible { + outline: 2px solid var(--color-accent); + outline-offset: 2px; + border-radius: var(--radius-sm); +} + +/* Skip to content link — keyboard-reachable first tab stop, hidden until focused */ +.skip-link { + position: absolute; + left: -9999px; + top: 0; + z-index: 1001; + padding: var(--space-3) var(--space-6); + background: var(--color-bg-secondary); + color: var(--color-accent); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + font-size: var(--text-sm); + font-weight: 600; +} +.skip-link:focus { + left: var(--space-4); + top: var(--space-4); + outline: 2px solid var(--color-accent); + outline-offset: 2px; +} + img, video { max-width: 100%; height: auto;