From b37fb5a26c78ca4a8ba3eca4c6c732c505378d6a Mon Sep 17 00:00:00 2001 From: Tyler Date: Sat, 20 Jun 2026 16:51:36 -0600 Subject: [PATCH] a11y(frontend): focus rings, ARIA labels, skip-link across ui components --- src/components/Layout.tsx | 10 ++++- src/components/ui/badge.tsx | 2 +- src/components/ui/claim-state-badge.tsx | 2 +- src/components/ui/dialog.tsx | 7 ++- src/components/ui/select.tsx | 4 +- src/components/ui/skip-link.tsx | 35 +++++++++++++++ src/components/ui/table.tsx | 5 ++- src/index.css | 58 ++++++++++++++++++++++++- 8 files changed, 113 insertions(+), 10 deletions(-) create mode 100644 src/components/ui/skip-link.tsx diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 90b02d4..92d570f 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -1,6 +1,7 @@ import { Outlet } from "react-router-dom"; import { useIsFetching } from "@tanstack/react-query"; import { Sidebar } from "./Sidebar"; +import { SkipLink } from "@/components/ui/skip-link"; export function Layout() { // useIsFetching() returns the number of in-flight queries and is 0 on @@ -11,6 +12,9 @@ export function Layout() { return (
+ {/* Skip link — first Tab stop, jumps past the sidebar. + Hidden until focused (see .skip-link in index.css). */} + {/* Scan element — always in the DOM (no layout shift), 1px tall, full width. pointer-events-none so it never blocks clicks. The inner @@ -28,7 +32,11 @@ export function Layout() { />
-
+
diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index 7f2526c..f9b0acc 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -3,7 +3,7 @@ import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; const badgeVariants = cva( - "inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:ring-offset-background", + "inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background", { variants: { variant: { diff --git a/src/components/ui/claim-state-badge.tsx b/src/components/ui/claim-state-badge.tsx index e4832f0..4d47314 100644 --- a/src/components/ui/claim-state-badge.tsx +++ b/src/components/ui/claim-state-badge.tsx @@ -75,7 +75,7 @@ export function ClaimStateBadge({ className, )} > - + {cfg.label} ); diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index 91ffca3..a74d63d 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -38,8 +38,11 @@ const DialogContent = React.forwardRef< {...props} > {children} - - + + Close diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx index 124ba90..074bdba 100644 --- a/src/components/ui/select.tsx +++ b/src/components/ui/select.tsx @@ -14,7 +14,7 @@ const SelectTrigger = React.forwardRef< span]:line-clamp-1", + "flex h-9 w-full items-center justify-between rounded-md border border-input bg-background/60 px-3 py-1 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", className )} {...props} @@ -64,7 +64,7 @@ const SelectItem = React.forwardRef< & { + /** id of the element the link should jump to (target of the focus). */ + targetId?: string; +}; + +/** + * "Skip to main content" link. + * + * Renders off-screen by default so it does not intrude on the visual + * design, and slides into view when it receives keyboard focus. Used at + * the very top of so keyboard users can bypass the sidebar and + * the global scan element on the first Tab. + * + * Voice: instrument-label eyebrow caps. Same hairline surface as the + * sidebar so it feels native when it appears. + */ +export function SkipLink({ + targetId = "main-content", + className, + children = "Skip to main content", + ...props +}: SkipLinkProps) { + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx index 194f656..4de0695 100644 --- a/src/components/ui/table.tsx +++ b/src/components/ui/table.tsx @@ -45,7 +45,7 @@ const TableRow = React.forwardRef< ->(({ className, ...props }, ref) => ( +>(({ className, scope = "col", ...props }, ref) => (