From 2e5061337b7bd33af415f549aa6b12e2e4084227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 8 Aug 2026 20:53:35 +0200 Subject: [PATCH] fix(ui): give date and time fields the standard focus ring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DateField and TimeField carried a divergent focus vocabulary — a hard `ring-2 ring-offset-2` that painted a 4 px ring outside the field's border box, so the highlight was visibly wider than the div and shifted on focus. Pull both onto the Input vocabulary: a soft 3 px ring hugging the box, no offset. Also match Input's `text-base md:text-sm`, so a sub-16 px input no longer triggers iOS Safari's focus auto-zoom. Every DateField and TimeField in the app inherits the fix. --- src/components/ui/__tests__/date-field.test.tsx | 11 +++++++++++ src/components/ui/__tests__/time-field.test.tsx | 9 +++++++++ src/components/ui/date-field.tsx | 9 ++++++++- src/components/ui/time-field.tsx | 6 +++++- 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/components/ui/__tests__/date-field.test.tsx b/src/components/ui/__tests__/date-field.test.tsx index 53a429e6b..e93dd48e8 100644 --- a/src/components/ui/__tests__/date-field.test.tsx +++ b/src/components/ui/__tests__/date-field.test.tsx @@ -97,4 +97,15 @@ describe("", () => { const html = render(); expect(html).toContain('aria-label="Open date picker"'); }); + + it("uses the Input focus vocabulary — a ring that hugs the box, no offset", () => { + // Drift guard: the field must share the app's focus ring (soft 3 px, no + // offset) so the highlight never paints wider than the div or jumps on + // focus. The old hard `ring-offset-2` ring is banned here. + const html = render(); + expect(html).toContain("focus-within:ring-ring/50"); + expect(html).toContain("focus-within:ring-[3px]"); + expect(html).not.toContain("ring-offset-2"); + expect(html).not.toContain("ring-offset-background"); + }); }); diff --git a/src/components/ui/__tests__/time-field.test.tsx b/src/components/ui/__tests__/time-field.test.tsx index f50b93c80..fb031e85d 100644 --- a/src/components/ui/__tests__/time-field.test.tsx +++ b/src/components/ui/__tests__/time-field.test.tsx @@ -120,4 +120,13 @@ describe(" SSR", () => { expect(html).toContain("min-h-11"); expect(html).toContain("sm:h-10"); }); + it("uses the Input focus vocabulary — a ring that hugs the box, no offset", () => { + // Drift guard shared with : soft 3 px ring, no offset, so the + // highlight never overflows the div or jumps on focus. + const html = render(, "de"); + expect(html).toContain("focus-within:ring-ring/50"); + expect(html).toContain("focus-within:ring-[3px]"); + expect(html).not.toContain("ring-offset-2"); + expect(html).not.toContain("ring-offset-background"); + }); }); diff --git a/src/components/ui/date-field.tsx b/src/components/ui/date-field.tsx index 9eaa01f59..0d9edae3d 100644 --- a/src/components/ui/date-field.tsx +++ b/src/components/ui/date-field.tsx @@ -56,8 +56,15 @@ const FIELD_HEIGHT_CLASSES = "min-h-11 h-11 sm:min-h-10 sm:h-10"; // ~230 px no matter what width the parent hands it, and the surplus spills out // of whatever dialog or sheet it sits in. See the matching note on the overlay // input below; both ends are needed, one alone still floors the box. +// The focus vocabulary matches `` exactly: a soft translucent 3 px ring +// that hugs the border box with no offset. The earlier `ring-2 ring-offset-2` +// painted a hard 4 px ring OUTSIDE the box — wider than the field's div and +// visibly mismatched next to the app's other inputs, and it shifted the +// highlight box on focus. `text-base md:text-sm` mirrors `` too: a +// sub-16 px input triggers iOS Safari's focus auto-zoom, which reads as a +// layout jump; 16 px below `md:` suppresses it. const FIELD_BASE_CLASSES = - "border-input bg-background text-foreground ring-offset-background placeholder:text-muted-foreground focus-within:ring-ring relative flex w-full min-w-0 items-center rounded-md border ps-3 pe-2 text-sm shadow-xs transition-[color,box-shadow] focus-within:ring-2 focus-within:ring-offset-2 focus-within:outline-none"; + "border-input bg-background text-foreground placeholder:text-muted-foreground relative flex w-full min-w-0 items-center rounded-md border ps-3 pe-2 text-base md:text-sm shadow-xs transition-[color,box-shadow] focus-within:border-ring focus-within:ring-ring/50 focus-within:ring-[3px] focus-within:outline-none"; export interface DateFieldProps { id?: string; diff --git a/src/components/ui/time-field.tsx b/src/components/ui/time-field.tsx index b1c48822c..432d07163 100644 --- a/src/components/ui/time-field.tsx +++ b/src/components/ui/time-field.tsx @@ -42,8 +42,12 @@ const FIELD_HEIGHT_CLASSES = "min-h-11 h-11 sm:min-h-10 sm:h-10"; // automatic minimum size would otherwise be the 20-character intrinsic width of // the text input inside, which floors the whole field at ~230 px and overflows // the dialog that hosts it. See the note in `date-field.tsx`. +// Focus vocabulary and font-size parity with `` / ``: a soft +// 3 px ring hugging the box (not the old hard `ring-2 ring-offset-2`, which +// painted 4 px outside the div and jumped on focus) and `text-base md:text-sm` +// to keep iOS Safari from auto-zooming a sub-16 px input. const FIELD_BASE_CLASSES = - "border-input bg-background text-foreground ring-offset-background placeholder:text-muted-foreground focus-within:ring-ring relative flex w-full min-w-0 items-center rounded-md border ps-3 pe-2 text-sm shadow-xs transition-[color,box-shadow] focus-within:ring-2 focus-within:ring-offset-2 focus-within:outline-none"; + "border-input bg-background text-foreground placeholder:text-muted-foreground relative flex w-full min-w-0 items-center rounded-md border ps-3 pe-2 text-base md:text-sm shadow-xs transition-[color,box-shadow] focus-within:border-ring focus-within:ring-ring/50 focus-within:ring-[3px] focus-within:outline-none"; const TIME_RE = /^([01]\d|2[0-3]):([0-5]\d)$/;