From 2b76592134f6ee6798c22825032ec54d05c7586a Mon Sep 17 00:00:00 2001 From: tsouth89 Date: Thu, 16 Jul 2026 18:48:21 -0400 Subject: [PATCH] Replace native popups on Windows are drawn by WebView2 and ignore the app's color-scheme / Tauri window theme, so they render in the OS theme: a light popup inside our dark UI. Replace every native select with a self-drawn, fully CSS-themed listbox that matches dark and light exactly. - Add components/Dropdown.tsx implementing the ARIA listbox keyboard model (Up/Down/Home/End move, Enter/Space commit, Escape closes, outside pointerdown dismisses) plus Dropdown.test.tsx. - Route FormControls.Select, MenuBarMetricSection, and RegionSection through Dropdown; drop the now-dead .provider-detail-select CSS. - Add .dropdown styles with dark defaults and [data-theme="light"] overrides, including a provider-detail variant. Note: the visual dark/light pass still needs a Windows eyeball since pixels can't be verified headlessly. --- .../src/components/Dropdown.test.tsx | 60 +++++++ .../desktop-tauri/src/components/Dropdown.tsx | 159 ++++++++++++++++++ .../src/components/FormControls.tsx | 18 +- apps/desktop-tauri/src/styles.css | 130 ++++++++++++-- .../sections/MenuBarMetricSection.test.tsx | 6 +- .../sections/MenuBarMetricSection.tsx | 17 +- .../providers/sections/RegionSection.tsx | 17 +- 7 files changed, 359 insertions(+), 48 deletions(-) create mode 100644 apps/desktop-tauri/src/components/Dropdown.test.tsx create mode 100644 apps/desktop-tauri/src/components/Dropdown.tsx diff --git a/apps/desktop-tauri/src/components/Dropdown.test.tsx b/apps/desktop-tauri/src/components/Dropdown.test.tsx new file mode 100644 index 00000000..d57f82d8 --- /dev/null +++ b/apps/desktop-tauri/src/components/Dropdown.test.tsx @@ -0,0 +1,60 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +import { Dropdown } from "./Dropdown"; + +const options = [ + { value: "a", label: "Alpha" }, + { value: "b", label: "Beta" }, + { value: "c", label: "Gamma" }, +]; + +describe("Dropdown", () => { + it("shows the selected label and keeps the popup closed by default", () => { + render( {}} ariaLabel="Metric" />); + expect(screen.getByRole("button", { name: "Metric" }).textContent).toContain("Beta"); + expect(screen.queryByRole("listbox")).toBeNull(); + }); + + it("opens on click and commits the clicked option", () => { + const onChange = vi.fn(); + render(); + fireEvent.click(screen.getByRole("button")); + expect(screen.getByRole("listbox")).toBeTruthy(); + fireEvent.click(screen.getByRole("option", { name: "Gamma" })); + expect(onChange).toHaveBeenCalledWith("c"); + expect(screen.queryByRole("listbox")).toBeNull(); + }); + + it("marks the current value as the selected option", () => { + render( {}} ariaLabel="Metric" />); + fireEvent.click(screen.getByRole("button")); + expect(screen.getByRole("option", { name: "Beta" }).getAttribute("aria-selected")).toBe("true"); + expect(screen.getByRole("option", { name: "Alpha" }).getAttribute("aria-selected")).toBe("false"); + }); + + it("commits with the keyboard (open, ArrowDown, Enter)", () => { + const onChange = vi.fn(); + render(); + fireEvent.keyDown(screen.getByRole("button"), { key: "ArrowDown" }); + const list = screen.getByRole("listbox"); + fireEvent.keyDown(list, { key: "ArrowDown" }); + fireEvent.keyDown(list, { key: "Enter" }); + expect(onChange).toHaveBeenCalledWith("b"); + }); + + it("closes on Escape without changing the value", () => { + const onChange = vi.fn(); + render(); + fireEvent.click(screen.getByRole("button")); + fireEvent.keyDown(screen.getByRole("listbox"), { key: "Escape" }); + expect(screen.queryByRole("listbox")).toBeNull(); + expect(onChange).not.toHaveBeenCalled(); + }); + + it("does not open when disabled", () => { + render( {}} ariaLabel="Metric" disabled />); + fireEvent.click(screen.getByRole("button")); + expect(screen.queryByRole("listbox")).toBeNull(); + }); +}); diff --git a/apps/desktop-tauri/src/components/Dropdown.tsx b/apps/desktop-tauri/src/components/Dropdown.tsx new file mode 100644 index 00000000..7eb1ccdc --- /dev/null +++ b/apps/desktop-tauri/src/components/Dropdown.tsx @@ -0,0 +1,159 @@ +import { useEffect, useId, useRef, useState } from "react"; + +export type DropdownOption = { value: string; label: string }; + +/** + * A fully CSS-themed listbox dropdown that replaces the native `` popups on Windows are drawn by WebView2 and do not reliably + * honor the app's `color-scheme` or the Tauri window theme, so they render in + * the OS theme (a light popup inside our dark UI). This control draws its own + * popup, so it matches dark and light exactly (SOU-224). It implements the ARIA + * listbox keyboard model: Up/Down/Home/End move the active option, Enter/Space + * commit, Escape closes, and clicking outside dismisses. + */ +export function Dropdown({ + value, + options, + onChange, + disabled, + className, + buttonClassName, + ariaLabel, + style, +}: { + value: string; + options: DropdownOption[]; + onChange: (value: string) => void; + disabled?: boolean; + className?: string; + buttonClassName?: string; + ariaLabel?: string; + style?: React.CSSProperties; +}) { + const [open, setOpen] = useState(false); + const [activeIndex, setActiveIndex] = useState(-1); + const rootRef = useRef(null); + const buttonRef = useRef(null); + const listRef = useRef(null); + const baseId = useId(); + + const selectedIndex = options.findIndex((option) => option.value === value); + const selectedLabel = options[selectedIndex]?.label ?? value; + + // Dismiss on outside pointer press. + useEffect(() => { + if (!open) return; + const onPointerDown = (event: PointerEvent) => { + if (rootRef.current && !rootRef.current.contains(event.target as Node)) { + setOpen(false); + } + }; + document.addEventListener("pointerdown", onPointerDown); + return () => document.removeEventListener("pointerdown", onPointerDown); + }, [open]); + + // On open, focus the list and start on the selected option. + useEffect(() => { + if (!open) return; + setActiveIndex(selectedIndex >= 0 ? selectedIndex : 0); + listRef.current?.focus(); + }, [open, selectedIndex]); + + const commit = (index: number) => { + const option = options[index]; + if (option) onChange(option.value); + setOpen(false); + buttonRef.current?.focus(); + }; + + const onButtonKeyDown = (event: React.KeyboardEvent) => { + if (disabled) return; + if (event.key === "ArrowDown" || event.key === "ArrowUp" || event.key === "Enter" || event.key === " ") { + event.preventDefault(); + setOpen(true); + } + }; + + const onListKeyDown = (event: React.KeyboardEvent) => { + switch (event.key) { + case "ArrowDown": + event.preventDefault(); + setActiveIndex((index) => Math.min(options.length - 1, index + 1)); + break; + case "ArrowUp": + event.preventDefault(); + setActiveIndex((index) => Math.max(0, index - 1)); + break; + case "Home": + event.preventDefault(); + setActiveIndex(0); + break; + case "End": + event.preventDefault(); + setActiveIndex(options.length - 1); + break; + case "Enter": + case " ": + event.preventDefault(); + commit(activeIndex); + break; + case "Escape": + event.preventDefault(); + setOpen(false); + buttonRef.current?.focus(); + break; + case "Tab": + setOpen(false); + break; + default: + break; + } + }; + + return ( +
+ + {open && ( +
    = 0 ? `${baseId}-option-${activeIndex}` : undefined} + onKeyDown={onListKeyDown} + > + {options.map((option, index) => ( +
  • setActiveIndex(index)} + onClick={() => commit(index)} + > + {option.label} +
  • + ))} +
+ )} +
+ ); +} diff --git a/apps/desktop-tauri/src/components/FormControls.tsx b/apps/desktop-tauri/src/components/FormControls.tsx index 5c7ac4af..35883972 100644 --- a/apps/desktop-tauri/src/components/FormControls.tsx +++ b/apps/desktop-tauri/src/components/FormControls.tsx @@ -1,5 +1,7 @@ import type React from "react"; +import { Dropdown } from "./Dropdown"; + // ── tiny reusable controls ────────────────────────────────────────── export function Toggle({ @@ -63,19 +65,13 @@ export function Select({ const width = Math.min(190, Math.max(78, Math.ceil(longestLabelUnits * 7.2) + 34)); return ( - + style={{ width }} + /> ); } diff --git a/apps/desktop-tauri/src/styles.css b/apps/desktop-tauri/src/styles.css index c1fcc4d3..fb6e04c1 100644 --- a/apps/desktop-tauri/src/styles.css +++ b/apps/desktop-tauri/src/styles.css @@ -2877,21 +2877,6 @@ body:has(.taskbar-flyout-frame) #root { color: var(--provider-row-text-secondary); } -.provider-detail-select { - appearance: none; - background: var(--provider-row-bg); - color: var(--provider-row-text-primary); - border: 1px solid var(--provider-icon-ring); - border-radius: 6px; - padding: 4px 10px; - font-size: 0.82rem; - min-width: 220px; -} - -.provider-detail-select:disabled { - opacity: 0.55; -} - .btn.btn--ghost { background: var(--provider-row-bg); color: var(--provider-row-text-primary); @@ -8189,3 +8174,118 @@ body:has(.dashboard-shell) #root { [data-theme="light"] .provider-detail-pace__stage[data-stage="ahead"] { color: var(--status-warning-text); } + +/* ── Themed dropdown — replaces native handleChange(next as MetricPreference)} disabled={disabled} - onChange={(e) => handleChange(e.target.value as MetricPreference)} - > - {options.map((option) => ( - - ))} - + ariaLabel={t("MenuBarMetric")} + />

{t("MenuBarMetricHelper")}

{error &&

{error}

} diff --git a/apps/desktop-tauri/src/surfaces/settings/providers/sections/RegionSection.tsx b/apps/desktop-tauri/src/surfaces/settings/providers/sections/RegionSection.tsx index f32b2475..39f7bae3 100644 --- a/apps/desktop-tauri/src/surfaces/settings/providers/sections/RegionSection.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/providers/sections/RegionSection.tsx @@ -1,4 +1,5 @@ import { useState } from "react"; +import { Dropdown } from "../../../../components/Dropdown"; import type { RegionOption } from "../../../../types/bridge"; import type { LocaleKey } from "../../../../i18n/keys"; import { setProviderRegion } from "../../../../lib/tauri"; @@ -49,18 +50,14 @@ export function RegionSection({ return (

{t("ProviderRegion")}

- + ariaLabel={t("ProviderRegion")} + /> {error &&

{error}

}
);