fix(ui): give date and time fields the standard focus ring

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.
This commit is contained in:
Marc-André Bombeck committed 2026-08-08 20:53:35 +02:00
1 parent 271e6f5db6
commit 2e5061337b
4 files changed
+33 -2

No files matched your search

@@ -97,4 +97,15 @@ describe("<DateField>", () => {
const html = render(<DateField value="2026-02-19" />);
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(<DateField value="2026-02-19" />);
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");
});
});
@@ -120,4 +120,13 @@ describe("<TimeField> 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 <DateField>: soft 3 px ring, no offset, so the
// highlight never overflows the div or jumps on focus.
const html = render(<TimeField value="08:30" />, "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");
});
});
+8 -1
View File
@@ -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 `<Input>` 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 `<Input>` 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;
+5 -1
View File
@@ -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 `<Input>` / `<DateField>`: 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)$/;