diff --git a/frontend/e2e/focus-rings.spec.ts b/frontend/e2e/focus-rings.spec.ts new file mode 100644 index 00000000..1fcb6d44 --- /dev/null +++ b/frontend/e2e/focus-rings.spec.ts @@ -0,0 +1,88 @@ +import { test, expect } from '@playwright/test'; + +// Expected focus ring resolved colors +// Light: --c-focus-ring: #012851 (brand-navy) +const FOCUS_RING_LIGHT = 'rgb(1, 40, 81)'; +// Dark: --c-focus-ring: #a1dcd8 (brand-mint) +const FOCUS_RING_DARK = 'rgb(161, 220, 216)'; + +test.describe('Focus ring token — CSS custom property', () => { + test('--c-focus-ring is defined in light mode', async ({ page }) => { + await page.goto('/'); + await page.waitForSelector('[data-hydrated]'); + + const value = await page.evaluate(() => + getComputedStyle(document.documentElement).getPropertyValue('--c-focus-ring').trim() + ); + expect(value).toBe('#012851'); + }); + + test('--c-focus-ring is defined in dark mode', async ({ page }) => { + await page.goto('/'); + await page.waitForSelector('[data-hydrated]'); + await page.evaluate(() => document.documentElement.setAttribute('data-theme', 'dark')); + + const value = await page.evaluate(() => + getComputedStyle(document.documentElement).getPropertyValue('--c-focus-ring').trim() + ); + expect(value).toBe('#a1dcd8'); + }); +}); + +test.describe('Focus ring — header interactive elements', () => { + test('ThemeToggle has brand-navy ring in light mode', async ({ page }) => { + await page.goto('/'); + await page.waitForSelector('[data-hydrated]'); + + await page.getByRole('button', { name: /dark mode|dunkelmodus/i }).focus(); + const boxShadow = await page.evaluate( + () => getComputedStyle(document.activeElement as HTMLElement).boxShadow + ); + expect(boxShadow).toContain(FOCUS_RING_LIGHT); + }); + + test('AppNav link has brand-mint ring in dark mode', async ({ page }) => { + await page.goto('/'); + await page.waitForSelector('[data-hydrated]'); + await page.evaluate(() => document.documentElement.setAttribute('data-theme', 'dark')); + + // Focus first desktop nav link + await page.locator('header nav').getByRole('link').first().focus(); + const boxShadow = await page.evaluate( + () => getComputedStyle(document.activeElement as HTMLElement).boxShadow + ); + expect(boxShadow).toContain(FOCUS_RING_DARK); + }); +}); + +test.describe('Focus ring — form inputs', () => { + test.use({ storageState: { cookies: [], origins: [] } }); + + test('login username input has brand-mint ring in dark mode', async ({ page }) => { + await page.goto('/login'); + await page.evaluate(() => document.documentElement.setAttribute('data-theme', 'dark')); + + await page.locator('#username').focus(); + const boxShadow = await page.evaluate( + () => getComputedStyle(document.activeElement as HTMLElement).boxShadow + ); + expect(boxShadow).toContain(FOCUS_RING_DARK); + }); +}); + +test.describe('Focus ring — PersonTypeahead', () => { + test('PersonTypeahead input has brand-navy ring in light mode', async ({ page }) => { + await page.goto('/'); + await page.waitForSelector('[data-hydrated]'); + + // Open advanced filter panel to expose the sender PersonTypeahead + await page.getByRole('button', { name: /filter/i }).click(); + await page.waitForSelector('#senderId-search'); + + await page.locator('#senderId-search').focus(); + const boxShadow = await page.evaluate( + () => getComputedStyle(document.activeElement as HTMLElement).boxShadow + ); + expect(boxShadow).toContain(FOCUS_RING_LIGHT); + }); +}); diff --git a/frontend/src/lib/components/CommentThread.svelte b/frontend/src/lib/components/CommentThread.svelte index 2bcf4418..b741c632 100644 --- a/frontend/src/lib/components/CommentThread.svelte +++ b/frontend/src/lib/components/CommentThread.svelte @@ -312,7 +312,7 @@ onMount(async () => {
{@render commentEntry(thread, thread.id, thread.replies.length === 0)} @@ -323,7 +323,7 @@ onMount(async () => {
{@render commentEntry(reply, thread.id, ri === thread.replies.length - 1)} diff --git a/frontend/src/lib/components/LanguageSwitcher.svelte b/frontend/src/lib/components/LanguageSwitcher.svelte index 314fb54c..187944af 100644 --- a/frontend/src/lib/components/LanguageSwitcher.svelte +++ b/frontend/src/lib/components/LanguageSwitcher.svelte @@ -12,7 +12,7 @@ const activeLocale = $derived(getLocale().toUpperCase());
diff --git a/frontend/src/lib/components/document/WhoWhenSection.svelte b/frontend/src/lib/components/document/WhoWhenSection.svelte index ff90efdf..d86a6743 100644 --- a/frontend/src/lib/components/document/WhoWhenSection.svelte +++ b/frontend/src/lib/components/document/WhoWhenSection.svelte @@ -71,7 +71,7 @@ $effect(() => { placeholder={m.form_placeholder_date()} maxlength="10" class="block w-full rounded border border-line p-2 text-sm shadow-sm - {dateInvalid ? 'border-red-400 focus:border-red-500 focus:ring-red-500' : 'focus:border-ink focus:ring-ink'}" + {dateInvalid ? 'border-red-400 focus:outline-none focus-visible:ring-2 focus-visible:ring-red-500' : 'focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring'}" aria-describedby={dateInvalid ? 'date-error' : undefined} /> @@ -91,7 +91,7 @@ $effect(() => { name="location" value={initialLocation} placeholder={m.form_placeholder_location()} - class="block w-full rounded border border-line p-2 text-sm shadow-sm focus:border-ink focus:ring-ink" + class="block w-full rounded border border-line p-2 text-sm shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring" />
diff --git a/frontend/src/lib/components/user/UserGroupsSection.svelte b/frontend/src/lib/components/user/UserGroupsSection.svelte index 760989f1..590c3295 100644 --- a/frontend/src/lib/components/user/UserGroupsSection.svelte +++ b/frontend/src/lib/components/user/UserGroupsSection.svelte @@ -18,7 +18,7 @@ let selected = $derived([...selectedGroupIds]); name="groupIds" value={group.id} bind:group={selected} - class="rounded border-line text-ink focus:ring-accent" + class="rounded border-line text-ink focus:ring-focus-ring" /> {group.name} diff --git a/frontend/src/lib/components/user/UserPasswordSection.svelte b/frontend/src/lib/components/user/UserPasswordSection.svelte index cf25b239..77572378 100644 --- a/frontend/src/lib/components/user/UserPasswordSection.svelte +++ b/frontend/src/lib/components/user/UserPasswordSection.svelte @@ -13,7 +13,7 @@ let { required = false }: { required?: boolean } = $props(); type="password" name="newPassword" required={required} - class="w-full rounded-sm border border-line px-3 py-2 font-serif text-sm focus:border-ink focus:outline-none" + class="w-full rounded-sm border border-line px-3 py-2 font-serif text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring" /> @@ -25,7 +25,7 @@ let { required = false }: { required?: boolean } = $props(); type="password" name="confirmPassword" required={required} - class="w-full rounded-sm border border-line px-3 py-2 font-serif text-sm focus:border-ink focus:outline-none" + class="w-full rounded-sm border border-line px-3 py-2 font-serif text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring" /> diff --git a/frontend/src/lib/components/user/UserProfileSection.svelte b/frontend/src/lib/components/user/UserProfileSection.svelte index b3b8d214..1efd502a 100644 --- a/frontend/src/lib/components/user/UserProfileSection.svelte +++ b/frontend/src/lib/components/user/UserProfileSection.svelte @@ -37,7 +37,7 @@ function handleBirthDateInput(e: Event) { type="text" name="firstName" value={firstName} - class="w-full rounded-sm border border-line px-3 py-2 font-serif text-sm focus:border-ink focus:outline-none" + class="w-full rounded-sm border border-line px-3 py-2 font-serif text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring" /> @@ -49,7 +49,7 @@ function handleBirthDateInput(e: Event) { type="text" name="lastName" value={lastName} - class="w-full rounded-sm border border-line px-3 py-2 font-serif text-sm focus:border-ink focus:outline-none" + class="w-full rounded-sm border border-line px-3 py-2 font-serif text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring" /> @@ -63,7 +63,7 @@ function handleBirthDateInput(e: Event) { placeholder="TT.MM.JJJJ" value={birthDateDisplay} oninput={handleBirthDateInput} - class="w-full rounded-sm border border-line px-3 py-2 font-serif text-sm focus:border-ink focus:outline-none" + class="w-full rounded-sm border border-line px-3 py-2 font-serif text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring" /> @@ -76,7 +76,7 @@ function handleBirthDateInput(e: Event) { type="email" name="email" value={email} - class="w-full rounded-sm border border-line px-3 py-2 font-serif text-sm focus:border-ink focus:outline-none" + class="w-full rounded-sm border border-line px-3 py-2 font-serif text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring" /> @@ -88,7 +88,7 @@ function handleBirthDateInput(e: Event) { name="contact" rows="3" placeholder={m.profile_contact_placeholder()} - class="w-full rounded-sm border border-line px-3 py-2 font-serif text-sm focus:border-ink focus:outline-none" + class="w-full rounded-sm border border-line px-3 py-2 font-serif text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring" >{contact} diff --git a/frontend/src/routes/AppNav.svelte b/frontend/src/routes/AppNav.svelte index 32fce2e3..4fe77704 100644 --- a/frontend/src/routes/AppNav.svelte +++ b/frontend/src/routes/AppNav.svelte @@ -41,7 +41,7 @@ function handleOverlayKeydown(event: KeyboardEvent) {