From 5c9307015c4f84b371d24e25666f76bf293c91b4 Mon Sep 17 00:00:00 2001 From: James Maes Date: Fri, 16 Oct 2026 09:20:21 -0500 Subject: [PATCH 1/2] fix(a11y): readable link text, 24px clear target, h2 widget titles, skip link focus App homes failed axe "serious" checks on 18 kof22 pages (A10/L10 = 0): - Accent-colored text (the table widget link row, cell links, widget links) used text-primary, which fails 4.5:1 for light brand colors such as #059669. Add a --color-link token (text-link utility) derived per theme from the accent: applyBrandingTheme darkens (light theme) or lightens (dark theme) the accent until it reaches 4.5:1 on the card and muted surfaces; without branding the stylesheet derives it from --color-primary. - The widget dropdown icon buttons (Clear range, previous/next) are at least 24 x 24 px (WCAG 2.2 target size). - Widget titles on an app home render as h2 under the page h1 (a nested widget stays h3); no visual change. - main is focusable (tabIndex -1) and the skip link moves focus to the first widget of the page, else to main. Adds unit tests and a Playwright axe spec (light and dark, 390 and 1440 px). Co-Authored-By: Claude Sonnet 5.5 --- src/app/(dashboard)/layout.tsx | 4 +- src/components/records/FieldValue.tsx | 16 +-- .../widgets/AppHome.headings.test.tsx | 53 ++++++++ src/components/widgets/AppHome.tsx | 6 +- .../widgets/ChildListRelatedPickers.tsx | 2 +- .../widgets/ChildRecordListEditor.tsx | 2 +- .../widgets/ChildRecordListWidget.tsx | 8 +- src/components/widgets/EsbOverviewWidget.tsx | 2 +- .../widgets/FilterAndColumnsSetupEditor.tsx | 6 +- .../widgets/PivotTableSetupWidget.tsx | 2 +- .../widgets/ProcessRowBuilderEditor.tsx | 2 +- src/components/widgets/QqqDisplayWidgets.tsx | 2 +- src/components/widgets/QqqTableWidget.tsx | 2 +- src/components/widgets/RecordGridWidget.tsx | 2 +- src/components/widgets/WidgetBlock.test.tsx | 14 +- src/components/widgets/WidgetBlock.tsx | 10 +- .../widgets/WidgetDropdownMenu.test.tsx | 6 + src/components/widgets/WidgetDropdownMenu.tsx | 4 +- .../widgets/WidgetHeaderControls.tsx | 2 +- src/components/widgets/qfmd-bridge.tsx | 2 +- src/components/widgets/widget-context.ts | 21 ++- src/lib/theme/apply-branding.test.ts | 33 ++++- src/lib/theme/apply-branding.ts | 91 +++++++++++++ src/lib/utils/skip-link.test.ts | 56 ++++++++ src/lib/utils/skip-link.ts | 53 ++++++++ src/styles/globals.css | 2 + src/styles/qqq-theme.css | 5 + tests/e2e/dashboard-a11y.spec.ts | 125 ++++++++++++++++++ 28 files changed, 498 insertions(+), 35 deletions(-) create mode 100644 src/components/widgets/AppHome.headings.test.tsx create mode 100644 src/lib/utils/skip-link.test.ts create mode 100644 src/lib/utils/skip-link.ts create mode 100644 tests/e2e/dashboard-a11y.spec.ts diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx index 916b404..53f878a 100644 --- a/src/app/(dashboard)/layout.tsx +++ b/src/app/(dashboard)/layout.tsx @@ -34,6 +34,7 @@ import { useAppTreeRoutes } from '@/lib/hooks/use-routes' import { useDocumentTitle } from '@/lib/hooks/use-document-title' import { loadMetaData } from '@/lib/api/metadata' import { applyBrandingTheme } from '@/lib/theme/apply-branding' +import { skipToFirstZone } from '@/lib/utils/skip-link' import { readMaterialTheme } from '@/lib/theme/material-theme' import { useTheme } from '@/lib/theme/theme-provider' import { recordAnalytics } from '@/lib/analytics' @@ -308,6 +309,7 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) { href="#main-content" className="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-[9999] focus:inline-flex focus:items-center pointer-coarse:focus:min-h-11 focus:rounded-md focus:bg-primary focus:px-4 focus:py-2 focus:text-sm focus:font-medium focus:text-primary-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2" data-qqq-id="skip-to-content" + onClick={(event) => { if (skipToFirstZone()) event.preventDefault() }} > Skip to main content @@ -372,7 +374,7 @@ function DashboardLayoutContent({ children }: { children: React.ReactNode }) { /> {/* Page content */} -
+
{metaLoading ? (
{String(value)} {target.target === '_blank' &&