diff --git a/apps/presentation/dashboard/smoke/workspace-locale-browser-smoke.mjs b/apps/presentation/dashboard/smoke/workspace-locale-browser-smoke.mjs index f8c5ef92f4..91abd4525d 100644 --- a/apps/presentation/dashboard/smoke/workspace-locale-browser-smoke.mjs +++ b/apps/presentation/dashboard/smoke/workspace-locale-browser-smoke.mjs @@ -110,6 +110,41 @@ export async function verifyWorkspaceLocales({ browser, url, installApi, outputD await page.getByTestId("personal-goal-home").waitFor(); assert.equal(await page.locator("html").getAttribute("lang"), "en"); assert.equal(await page.evaluate(() => localStorage.getItem("loopx-pw-locale")), "en"); + await page.getByRole("button", {name: "Settings", exact: true}).click(); + await page.getByRole("button", {name: /Appearance/}).click(); + const themes = page.locator('.personal-settings-choice-group'); + const themeOptions = themes.getByRole('radio'); + await page.screenshot({path: resolve(outputDir, 'settings-choice-desktop.png'), animations: 'disabled'}); + assert.equal(await themes.locator('[role="radio"][tabindex="0"]').count(), 1, 'Only the selected theme is a Tab stop'); + await themeOptions.nth(0).focus(); await page.keyboard.press('ArrowRight'); + assert.equal(await themeOptions.nth(1).getAttribute('aria-checked'), 'true'); + assert(await themeOptions.nth(1).evaluate(element => element === document.activeElement)); + await page.keyboard.press('End'); + assert.equal(await themeOptions.nth(2).getAttribute('aria-checked'), 'true'); + await page.keyboard.press('ArrowRight'); + assert.equal(await themeOptions.nth(0).getAttribute('aria-checked'), 'true', 'Arrow navigation wraps'); + await page.keyboard.press('ArrowLeft'); + assert.equal(await themeOptions.nth(2).getAttribute('aria-checked'), 'true'); + await page.keyboard.press('Home'); + assert.equal(await themeOptions.nth(0).getAttribute('aria-checked'), 'true'); + await page.keyboard.press('Tab'); + assert(!await themes.evaluate(element => element.contains(document.activeElement)), 'Tab leaves the group'); + await page.setViewportSize({width: 960, height: 640}); + await themeOptions.nth(0).focus(); + assert(await themeOptions.nth(0).evaluate(element => getComputedStyle(element).outlineStyle !== 'none')); + await page.screenshot({path: resolve(outputDir, 'settings-choice-minimum.png'), animations: 'disabled'}); + await page.getByRole('button', {name: /Language/}).click(); + const languages = page.locator('.personal-language-options'); + const english = languages.getByRole('radio', {name: /English/}); + await english.focus(); await page.keyboard.press('ArrowDown'); + assert.equal(await page.evaluate(() => localStorage.getItem('loopx-pw-locale')), 'zh-CN'); + assert.equal(await languages.locator('[role="radio"][tabindex="0"]').count(), 1); + assert(await languages.locator('[aria-checked="true"]').evaluate(element => element === document.activeElement)); + await page.keyboard.press('ArrowUp'); + assert.equal(await page.evaluate(() => localStorage.getItem('loopx-pw-locale')), 'en'); + await page.reload({waitUntil: 'networkidle'}); + assert.equal(await english.getAttribute('aria-checked'), 'true', 'Keyboard selection survives reload'); + assert.equal(api.turnRequests.length, 0, 'Preferences never start an Agent'); console.log("workspace locale: Settings choices survive reload in both directions: ok"); } finally { await page.close(); diff --git a/apps/presentation/dashboard/src/features/personal-workspace/workspace-settings-page.tsx b/apps/presentation/dashboard/src/features/personal-workspace/workspace-settings-page.tsx index 37eeb0e0f9..04ca0f06e5 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/workspace-settings-page.tsx +++ b/apps/presentation/dashboard/src/features/personal-workspace/workspace-settings-page.tsx @@ -2,7 +2,7 @@ import {ContextHelp} from "./context-help"; import {GoalAcceptanceContractSection} from "./goal-acceptance-contract"; import {GoalObjectiveSettings} from "./goal-objective-settings"; import {goalAcceptanceContractCopy} from "./delivery-review-copy"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState, type KeyboardEvent } from "react"; import type {SettingsTarget} from "./workspace-settings-route"; import { ArrowLeft, Bot, Check, Clock3, KeyRound, Languages, Palette, ServerCog, Settings2 } from "lucide-react"; @@ -24,6 +24,21 @@ import type { PersonalWorkspaceCallbacks, WorkspaceGoal, WorkspaceGoalNotificati import type { WorkspaceTheme } from "./workspace-theme"; import type { AttentionNotificationSupport } from "./attention-notifications"; +// Preference cards share a single Tab stop and radio-group keyboard behavior. +function movePreferenceSelection(event: KeyboardEvent) { + if (event.altKey || event.ctrlKey || event.metaKey) return; + const options = [...event.currentTarget.querySelectorAll('button[role="radio"]:not(:disabled)')]; + const index = options.indexOf(event.target as HTMLButtonElement); + if (index < 0) return; + const next = event.key === "Home" ? 0 : event.key === "End" ? options.length - 1 + : event.key === "ArrowRight" || event.key === "ArrowDown" ? (index + 1) % options.length + : event.key === "ArrowLeft" || event.key === "ArrowUp" ? (index + options.length - 1) % options.length : -1; + if (next < 0) return; + event.preventDefault(); + options[next].focus(); + options[next].click(); +} + const attentionNotificationSupportCopy = { default: "settings.attentionNotificationsDefault", denied: "settings.attentionNotificationsDenied", @@ -274,16 +289,16 @@ export function WorkspaceSettingsPage({ {tab === "appearance" ? (
-
- - - @@ -311,10 +326,11 @@ export function WorkspaceSettingsPage({ {tab === "language" ? (
-
+
{localeOptions.map((option) => (