From 1dbcbb2fb07469b87ad4a63ceb0b119021737db4 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 21 Aug 2026 04:53:52 -0700 Subject: [PATCH] fix(ui): keep segmented selections visible in forced colors (#127165) * test(ui): cover segmented controls in forced colors Amp-Thread-ID: https://ampcode.com/threads/T-01a021f4-b547-7788-a916-d4a94cbd3e3b * fix(ui): preserve segmented selection in forced colors Amp-Thread-ID: https://ampcode.com/threads/T-01a021f4-b547-7788-a916-d4a94cbd3e3b * fix(ui): separate forced-color selection and focus Amp-Thread-ID: https://ampcode.com/threads/T-01a021f4-b547-7788-a916-d4a94cbd3e3b --------- Co-authored-by: Amp --- ui/src/e2e/config-controls-visual.e2e.test.ts | 108 ++++++++++++++++++ ui/src/styles/settings-controls.css | 12 ++ 2 files changed, 120 insertions(+) diff --git a/ui/src/e2e/config-controls-visual.e2e.test.ts b/ui/src/e2e/config-controls-visual.e2e.test.ts index 4104c3981991..7949bb719475 100644 --- a/ui/src/e2e/config-controls-visual.e2e.test.ts +++ b/ui/src/e2e/config-controls-visual.e2e.test.ts @@ -108,6 +108,114 @@ async function captureBrowserSettingProof( } suite.define(() => { + it("keeps selected segmented options distinct in forced colors", async () => { + const cases = [ + { + colorScheme: "dark" as const, + config: { ui: { prefs: { themeMode: "dark" } } }, + featureMethods: undefined, + route: "settings/appearance", + selected: "Dark", + unselected: "Light", + }, + { + colorScheme: "light" as const, + config: { update: { auto: { enabled: true }, channel: "beta" } }, + featureMethods: ["config.get", "config.set", "config.apply", "update.run"], + route: "settings/updates", + selected: "Beta", + unselected: "Stable", + }, + ]; + + for (const scenario of cases) { + await suite.withPage( + { + colorScheme: scenario.colorScheme, + forcedColors: "active", + locale: "en-US", + serviceWorkers: "block", + viewport: { height: 900, width: 1280 }, + }, + async ({ page }) => { + await installMockGateway(page, { + featureMethods: scenario.featureMethods, + methodResponses: { + "config.get": { + config: scenario.config, + hash: `forced-colors-${scenario.route}`, + issues: [], + raw: JSON.stringify(scenario.config), + runtimeConfig: scenario.config, + valid: true, + }, + }, + operatorScopes: ["operator.read", "operator.admin"], + }); + + const response = await page.goto(`${suite.server.baseUrl}${scenario.route}`); + expect(response?.status()).toBe(200); + expect(await page.evaluate(() => matchMedia("(forced-colors: active)").matches)).toBe( + true, + ); + + const selected = page.getByRole("radio", { name: scenario.selected, exact: true }); + const unselected = page.getByRole("radio", { name: scenario.unselected, exact: true }); + await selected.waitFor(); + expect(await selected.getAttribute("aria-checked")).toBe("true"); + expect(await unselected.getAttribute("aria-checked")).toBe("false"); + if (captureUiProofEnabled) { + await mkdir(uiProofArtifactDir, { recursive: true }); + await selected + .locator( + "xpath=ancestor::div[contains(concat(' ', normalize-space(@class), ' '), ' settings-row ')][1]", + ) + .screenshot({ + animations: "disabled", + path: path.join( + uiProofArtifactDir, + `segmented-forced-colors-${scenario.colorScheme}.png`, + ), + }); + } + expect( + await selected.evaluate((element) => { + const style = getComputedStyle(element); + return { + textDecorationLine: style.textDecorationLine, + textDecorationThickness: style.textDecorationThickness, + }; + }), + ).toEqual({ textDecorationLine: "underline", textDecorationThickness: "2px" }); + expect( + await unselected.evaluate((element) => getComputedStyle(element).textDecorationLine), + ).toBe("none"); + + await selected.focus(); + expect(await selected.evaluate((element) => element.matches(":focus-visible"))).toBe( + true, + ); + expect( + await selected.evaluate((element) => { + const style = getComputedStyle(element); + return { + outlineOffset: style.outlineOffset, + outlineStyle: style.outlineStyle, + outlineWidth: style.outlineWidth, + textDecorationLine: style.textDecorationLine, + }; + }), + ).toEqual({ + outlineOffset: "2px", + outlineStyle: "solid", + outlineWidth: "2px", + textDecorationLine: "underline", + }); + }, + ); + } + }); + it("keeps checked switches on the scene accent on the security page", async () => { await suite.withPage( { diff --git a/ui/src/styles/settings-controls.css b/ui/src/styles/settings-controls.css index 1d92008877dc..93e5b4c2ca1a 100644 --- a/ui/src/styles/settings-controls.css +++ b/ui/src/styles/settings-controls.css @@ -90,6 +90,18 @@ wa-radio-group.settings-segmented::part(radios) { cursor: not-allowed; } +@media (forced-colors: active) { + .settings-segmented__btn--active { + text-decoration: underline 2px; + text-underline-offset: 3px; + } + + .settings-segmented__btn:focus-visible { + outline: 2px solid Highlight; + outline-offset: 2px; + } +} + input.settings-input, select.settings-select:not([multiple]) { height: var(--settings-control-height);