From 8423ddde736ae5553c1458638a51deb6ecfd1c26 Mon Sep 17 00:00:00 2001 From: Mike Binder Date: Wed, 30 Sep 2026 03:23:52 +0000 Subject: [PATCH] Add a phone menu and fix the second audit's findings - Under 768px the nav and account links fold into a Menu button (aria-expanded, aria-controls, Escape closes and returns focus, closes on navigation). - Dialog triggers drop aria-expanded, so checkers stop reading them as broken menu toggles. - Open net form shows errors in a role=alert message. - 2px focus outline on links and focusable regions. - New-tab links use rel="noopener noreferrer". --- CHANGELOG.md | 2 +- web/src/App.tsx | 81 ++++++++++++++++---------- web/src/components/PasswordDialog.tsx | 2 +- web/src/components/ui/alert-dialog.tsx | 4 +- web/src/components/ui/dialog.tsx | 4 +- web/src/index.css | 6 ++ web/src/pages/Dashboard.tsx | 15 ++++- web/src/pages/Health.tsx | 2 +- 8 files changed, 76 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed69a37..37397bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ First public release. - Nets page: every past net with roster, searchable transcript, CSV and transcript downloads. - Stats page: check-ins per net over time (returning vs first time), typical check-ins vs the previous period, stations heard, first-timers, regulars with attendance. Filter by range and scheduled net. - Health page (admins): live health checks, plus an AllStar setup guide with your values filled in that opens itself when something breaks. -- Works on a phone, light and dark mode, screen reader and keyboard friendly (WCAG 2.2 AA). +- Works on a phone (navigation folds into a Menu button), light and dark mode, screen reader and keyboard friendly (WCAG 2.2 AA). ### Accounts and security - Accounts for licensed hams only: callsign checked against the FCC database (callook.info) at setup and when adding users, re-checked every 30 days. Admins confirm licenses from other countries; a first admin outside the US is accepted as entered. `REQUIRE_LICENSE=0` turns it off. diff --git a/web/src/App.tsx b/web/src/App.tsx index 354572c..fd9bf7e 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useRef, useState } from "react"; -import { Radio } from "lucide-react"; +import { Menu as MenuIcon, Radio, X } from "lucide-react"; import { get, post, setOnUnauthorized, type User } from "@/lib/api"; import { Button } from "@/components/ui/button"; import { AuthScreen, VerifyLicense } from "@/pages/AuthScreen"; @@ -27,6 +27,15 @@ export default function App() { const [status, setStatus] = useState(null); const [error, setError] = useState(""); const [view, setView] = useState(viewFromHash); + const [menuOpen, setMenuOpen] = useState(false); + const menuButton = useRef(null); + // Escape closes the phone menu and puts focus back on its button + const onMenuKey = (e: React.KeyboardEvent) => { + if (e.key === "Escape" && menuOpen) { + setMenuOpen(false); + menuButton.current?.focus(); + } + }; const load = useCallback(async () => { try { @@ -44,7 +53,7 @@ export default function App() { // Hash routing so the back button works useEffect(() => { - const on = () => setView(viewFromHash()); + const on = () => { setView(viewFromHash()); setMenuOpen(false); }; window.addEventListener("hashchange", on); return () => window.removeEventListener("hashchange", on); }, []); @@ -108,34 +117,42 @@ export default function App() {