From 9bda0b30db80fdbeb300a552fb4f086d0caf3699 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 03:54:21 +0100 Subject: [PATCH] Fix shell and settings accessibility regressions Keyboard and screen-reader users could not reliably bypass the shell or identify two General Settings toggles, while the RC badge failed the maintained contrast contract. Restoring explicit focus transfer, accessible control names, and compliant badge contrast keeps core navigation and settings usable without changing product scope. Change-source: pulse-maintainer Contract-Neutral: Accessibility regression fix only; no public contract or subsystem boundary changes. --- frontend-modern/browser-verification.json | 64 +++++++------------ frontend-modern/src/AppLayout.tsx | 6 +- .../src/__tests__/AppLayout.test.tsx | 11 +++- .../Settings/GeneralSettingsPanel.tsx | 2 + ...GeneralSettingsPanel.localization.test.tsx | 8 +++ .../83-product-trust-accessibility.spec.ts | 13 +++- 6 files changed, 60 insertions(+), 44 deletions(-) diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 0ab28a8fd..dfb21230c 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,42 +1,23 @@ { "version": 1, - "base_sha": "2f8a4ec629b75c7ecefd38a9abc4b1511bc9a891", - "verified_at": "2026-09-01T19:54:19Z", + "base_sha": "44e274e5b386e5e990e9bf09b85b6258043487ba", + "verified_at": "2026-09-02T02:59:02Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/App.tsx", "frontend-modern/src/AppLayout.tsx", - "frontend-modern/src/components/shared/MobileNavBar.tsx", - "frontend-modern/src/components/shared/mobileNavBarModel.ts", - "frontend-modern/src/features/home/HomePageSurface.tsx", - "frontend-modern/src/features/home/homePageModel.ts", - "frontend-modern/src/i18n/messages.de.ts", - "frontend-modern/src/i18n/messages.es.ts", - "frontend-modern/src/i18n/messages.ts", - "frontend-modern/src/routing/navigation.ts", - "frontend-modern/src/routing/resourceLinks.ts", - "frontend-modern/src/routing/routePreload.ts", - "frontend-modern/src/utils/assistantPageContext.ts" + "frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx" ], "content_sha256": { - "frontend-modern/src/App.tsx": "d5473f7838148edeaaf5564f01c166d97b6086232dcba70f6296b5c58e563a4b", - "frontend-modern/src/AppLayout.tsx": "db685903fef2a1509edd812acb3904f7ff3ffab0bbd1af2bff183c1b3ae8e373", - "frontend-modern/src/components/shared/MobileNavBar.tsx": "27358f3e77267cc111ce97b32d715b0069e2d1370ddd2b3ecc4f2d34a3e6dac5", - "frontend-modern/src/components/shared/mobileNavBarModel.ts": "ab9e69d379579d02e33a2abd1224d8c13e6543dd6aa0ad40cc1f65697a289d04", - "frontend-modern/src/features/home/HomePageSurface.tsx": "deleted", - "frontend-modern/src/features/home/homePageModel.ts": "deleted", - "frontend-modern/src/i18n/messages.de.ts": "602246d3d4ce11a1a8a914027d950a30ba1f2f25850c385836013853957c2d0c", - "frontend-modern/src/i18n/messages.es.ts": "ed5a749603efad29293cd6964215f6edc21a4ce14bca99a48429ddd120770263", - "frontend-modern/src/i18n/messages.ts": "43a757e00eaa7879c400895c9c59a930e31513dbf03e0ae6d60f7e72a6ad9962", - "frontend-modern/src/routing/navigation.ts": "8ae1ad012e60ef345ffb3d18bb66f5d8af056758109655a8ec362ebf4c7b9556", - "frontend-modern/src/routing/resourceLinks.ts": "dee9a426de785e23390ba49c9067f55c8f923cecfce1247ff18f9b1004e0cc90", - "frontend-modern/src/routing/routePreload.ts": "ee79d423db0afcf8d76d1d39da59a13cb2c98e908e516a016cbb425982b25873", - "frontend-modern/src/utils/assistantPageContext.ts": "bfca19b4e183777ee1535073d48cce92ce31980f4105bab337f33a28538a6a35" + "frontend-modern/src/AppLayout.tsx": "be386bcaa58656a9397fed71a9d70147e2540ff6cb4aff53261d9c03e6ca3834", + "frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx": "f1e1c0630cc841ae2820b15130a887aeef0ec7838cb93f652213ebb8fb278199" }, "routes": [ - "/", - "/home", - "/proxmox/overview" + "/actions", + "/alerts/overview", + "/settings/infrastructure", + "/settings/system-general", + "/patrol", + "/" ], "viewports": [ { @@ -44,21 +25,22 @@ "height": 720 }, { - "width": 375, - "height": 812 + "width": 390, + "height": 844 } ], "states": [ - "signed-in desktop shell with primary tabs Proxmox, Docker, Kubernetes, TrueNAS, vSphere, Machines and no Home tab", - "/home renders the Page Not Found surface with 'No route matched /home' and a Go to workspace button", - "signed-in narrow shell on /proxmox/overview with the bottom navigation bar showing Proxmox, Alerts, Patrol, Actions, More and no Home entry", - "More navigation sheet open at narrow width listing Settings only", - "no horizontal page overflow at 375px; no console errors at either width" + "authenticated empty Actions surface at phone width with the RC Preview badge visible, no horizontal overflow, and the skip link focused", + "authenticated General Settings surface with Full-width mode off and Outbound usage telemetry on, each exposed as a named pressed-state button", + "authenticated Alerts, Infrastructure, General Settings, and Patrol surfaces with reduced motion enabled and no automatically detectable WCAG A/AA violations", + "Add infrastructure dialog open with its accessible description, close control focused, and underlying shell retained", + "logged-out welcome surface with reduced motion enabled and no automatically detectable WCAG A/AA violations" ], "interactions": [ - "navigated to / and /home at desktop width and read the rendered nav and main headings", - "pressed Go to workspace on the /home not-found page and landed on /proxmox/overview", - "resized to 375x812, loaded /proxmox/overview, opened the More navigation sheet, closed it with Escape", - "confirmed the default landing route and nav order are unchanged from the parent revision apart from the removed Home entry" + "tabbed from the document start to Skip to main content at phone width, activated it with Enter, and confirmed focus moved to the main landmark", + "inspected the rendered 390x844 Actions screenshot for badge contrast, placement, clipping, scrolling, and bottom-navigation coherence", + "navigated the authenticated desktop routes and scanned their rendered states for WCAG A/AA violations and unexpected reduced-motion effects", + "opened Add infrastructure, verified initial close-control focus and dialog description, dismissed it with Escape, and confirmed focus returned to Add infrastructure", + "opened the logged-out entry surface and verified its heading, disabled welcome/form motion, and accessibility scan" ] } diff --git a/frontend-modern/src/AppLayout.tsx b/frontend-modern/src/AppLayout.tsx index 2fae9e46d..3d4ea0def 100644 --- a/frontend-modern/src/AppLayout.tsx +++ b/frontend-modern/src/AppLayout.tsx @@ -258,6 +258,7 @@ export function AppLayout(props: AppLayoutProps) { const [skipLinkFocused, setSkipLinkFocused] = createSignal(false); const [primaryRouteMemoryVersion, setPrimaryRouteMemoryVersion] = createSignal(0); let headerEl: HTMLDivElement | undefined; + let mainContentEl: HTMLElement | undefined; let assistantLauncherEl: HTMLButtonElement | undefined; let restoreAssistantLauncherFocus = false; let headerHideTimeout: ReturnType | undefined; @@ -730,6 +731,7 @@ export function AppLayout(props: AppLayoutProps) { jump past the chrome straight into the page content. */} mainContentEl?.focus()} onFocus={() => setSkipLinkFocused(true)} onBlur={() => setSkipLinkFocused(false)} class={ @@ -814,7 +816,7 @@ export function AppLayout(props: AppLayoutProps) { - + Preview @@ -980,7 +982,9 @@ export function AppLayout(props: AppLayoutProps) {
diff --git a/frontend-modern/src/__tests__/AppLayout.test.tsx b/frontend-modern/src/__tests__/AppLayout.test.tsx index 821a963de..d097bb98a 100644 --- a/frontend-modern/src/__tests__/AppLayout.test.tsx +++ b/frontend-modern/src/__tests__/AppLayout.test.tsx @@ -174,7 +174,16 @@ describe('AppLayout navigation icons', () => { expect(container.querySelector('.pulse-shell')).toHaveClass('pb-safe-or-14'); expect(container.querySelector('.pulse-shell')).not.toHaveClass('pb-safe-or-16'); expect(container.querySelector('.header')).toHaveClass('mb-1', 'sm:mb-3'); - expect(container.querySelector('main')).toHaveClass('mb-1', 'sm:mb-2'); + const main = container.querySelector('main'); + expect(main).toHaveClass('mb-1', 'sm:mb-2'); + expect(main).toHaveAttribute('id', 'main'); + expect(main).toHaveAttribute('tabindex', '-1'); + const skipLink = screen.getByRole('link', { name: 'Skip to main content' }); + expect(skipLink).toHaveAttribute('href', '#main'); + fireEvent.click(skipLink); + expect(main).toHaveFocus(); + expect(screen.getByText('Preview')).toHaveClass('bg-orange-700', 'text-white'); + expect(screen.getByText('Preview')).not.toHaveClass('bg-orange-500'); expect(container.querySelector('footer')).toHaveClass('pulse-footer', 'px-2', 'sm:px-4'); const desktopNav = screen.getByRole('navigation', { name: 'Primary navigation' }); diff --git a/frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx b/frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx index 0f3b9ad5f..d6f00707e 100644 --- a/frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx +++ b/frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx @@ -223,6 +223,7 @@ export const GeneralSettingsPanel: Component = (props layoutStore.toggle()} />
@@ -276,6 +277,7 @@ export const GeneralSettingsPanel: Component = (props props.handleTelemetryEnabledChange(!props.telemetryEnabled())} /> diff --git a/frontend-modern/src/components/Settings/__tests__/GeneralSettingsPanel.localization.test.tsx b/frontend-modern/src/components/Settings/__tests__/GeneralSettingsPanel.localization.test.tsx index d37365766..3cd87ddc7 100644 --- a/frontend-modern/src/components/Settings/__tests__/GeneralSettingsPanel.localization.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/GeneralSettingsPanel.localization.test.tsx @@ -86,6 +86,14 @@ describe('GeneralSettingsPanel localization', () => { ).toBeInTheDocument(); expect(screen.getByText('Usage data and privacy')).toBeInTheDocument(); expect(screen.getByText('Outbound usage telemetry')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Full-width mode' })).toHaveAttribute( + 'aria-pressed', + 'false', + ); + expect(screen.getByRole('button', { name: 'Outbound usage telemetry' })).toHaveAttribute( + 'aria-pressed', + 'true', + ); expect(screen.getByRole('button', { name: 'Preview payload' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Reset ID' })).toBeInTheDocument(); expect(screen.getByText('Monitoring cadence')).toBeInTheDocument(); diff --git a/tests/integration/tests/83-product-trust-accessibility.spec.ts b/tests/integration/tests/83-product-trust-accessibility.spec.ts index 553474bb9..48b74f8d7 100644 --- a/tests/integration/tests/83-product-trust-accessibility.spec.ts +++ b/tests/integration/tests/83-product-trust-accessibility.spec.ts @@ -137,8 +137,19 @@ test("Actions remains named, directly reachable, keyboard accessible, and free o document.documentElement.clientWidth, ); expect(overflow).toBeFalsy(); + + const skipLink = page.getByRole("link", { name: "Skip to main content" }); + await page.evaluate(() => { + document.body.tabIndex = -1; + document.body.focus(); + document.body.removeAttribute("tabindex"); + }); await page.keyboard.press("Tab"); - await expect(page.locator(":focus")).toBeVisible(); + await expect(skipLink).toBeFocused(); + await expect(skipLink).toBeVisible(); + await page.keyboard.press("Enter"); + await expect(page.locator("#main")).toBeFocused(); + await testInfo.attach("actions-phone-width", { body: await page.screenshot(), contentType: "image/png",