Merge remote-tracking branch 'origin/release/v1.42.0' into v142/ui-audit

This commit is contained in:
Marc-André Bombeck committed 2026-10-08 17:44:00 +02:00
commit 492c2a4131
41 files changed
+1268 -151

No files matched your search

@@ -4,10 +4,12 @@ import { renderToStaticMarkup } from "react-dom/server";
import { Textarea } from "../textarea";
describe("<Textarea>", () => {
it("pins text-base on mobile and shrinks to text-sm on sm+ (iOS zoom defence)", () => {
it("pins text-base on touch screens and shrinks to text-sm only under a fine pointer (iOS zoom defence)", () => {
const html = renderToStaticMarkup(<Textarea id="note" />);
expect(html).toContain("text-base");
expect(html).toContain("sm:text-sm");
expect(html).toContain("sm:pointer-fine:text-sm");
// A width breakpoint alone handed 14 px to a phone held sideways.
expect(html).not.toMatch(/(^|\s)sm:text-sm(\s|")/);
});
it("floors at min-h-11 on mobile and min-h-9 on sm+ (WCAG 2.5.5 tap target)", () => {
@@ -62,7 +64,7 @@ describe("<Textarea>", () => {
expect(html).toContain("font-mono");
// iOS zoom defence still present after merge
expect(html).toContain("text-base");
expect(html).toContain("sm:text-sm");
expect(html).toContain("sm:pointer-fine:text-sm");
});
it("forwards rows / placeholder / maxLength to the underlying element", () => {
+6 -4
View File
@@ -60,11 +60,13 @@ const FIELD_HEIGHT_CLASSES = "min-h-11 h-11 sm:min-h-10 sm:h-10";
// `input-focus` tone, with no ring. 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.
// highlight box on focus. `text-base md:pointer-fine: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 on every touch screen suppresses it
// (`pointer-fine`, globals.css — a width breakpoint alone missed a phone
// held sideways and every iPad).
const FIELD_BASE_CLASSES =
"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-input-focus 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:pointer-fine:text-sm shadow-xs transition-[color,box-shadow] focus-within:border-input-focus focus-within:outline-none";
export interface DateFieldProps {
id?: string;
+1 -1
View File
@@ -76,7 +76,7 @@ function Input({
className={cn(
// v1.4.34 IW-G — floor at 44 px on mobile to clear WCAG 2.5.5;
// shrink to 40 px on sm+ where pointer precision is higher.
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-11 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 sm:h-10 md:text-sm",
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-11 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 sm:h-10 md:pointer-fine:text-sm",
"focus-visible:border-input-focus aria-invalid:focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className,
+4 -1
View File
@@ -56,8 +56,11 @@ import { cn } from "@/lib/utils";
// theme's `--muted-foreground` — same colour the shadcn `<SelectTrigger>`
// chevron uses.
// Type size: 16 px on a touch screen, 14 px only under a mouse or trackpad
// (`pointer-fine`, globals.css). iOS Safari zooms the page into a focused
// `<select>` whose text is smaller than 16 px, the same as into an input.
const NATIVE_SELECT_CLASS =
"border-input bg-background text-foreground placeholder:text-muted-foreground focus-visible:border-input-focus flex h-11 sm:h-10 w-full appearance-none rounded-md border ps-3 pe-9 py-1 text-sm shadow-xs transition-[color,box-shadow] focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 bg-[image:var(--native-select-chevron)] bg-[length:1rem] bg-[position:right_0.75rem_center] bg-no-repeat";
"border-input bg-background text-foreground placeholder:text-muted-foreground focus-visible:border-input-focus flex h-11 sm:h-10 w-full appearance-none rounded-md border ps-3 pe-9 py-1 text-base pointer-fine:text-sm shadow-xs transition-[color,box-shadow] focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 bg-[image:var(--native-select-chevron)] bg-[length:1rem] bg-[position:right_0.75rem_center] bg-no-repeat";
export type NativeSelectProps = React.SelectHTMLAttributes<HTMLSelectElement>;
+8 -2
View File
@@ -250,8 +250,14 @@ export function ResponsiveSheet({
className={cn(
// Vertical-only scroll port — see the note on the Dialog
// branch's body below for why `overflow-y-auto` alone does not
// say that.
"flex min-h-0 flex-1 flex-col gap-4 overflow-x-clip overflow-y-auto p-4",
// say that. `overscroll-contain`: a flick that reaches the end
// of the form stops there instead of chaining into the page
// (or the browser's pull-to-refresh) behind the sheet.
"flex min-h-0 flex-1 flex-col gap-4 overflow-x-clip overflow-y-auto overscroll-contain p-4",
// Without a footer the body is the sheet's bottom edge, so it
// keeps its last row above the home indicator of an
// installed app; the footer does that when there is one.
!footer && "pb-[calc(env(safe-area-inset-bottom,0px)+1rem)]",
bodyClassName,
)}
>
+6
View File
@@ -61,6 +61,12 @@ function SheetContent({
<SheetOverlay />
<SheetPrimitive.Content
data-slot="sheet-content"
// Read by the safe-area and on-screen-keyboard rules in
// `globals.css`: a bottom sheet stands on the keyboard, the
// full-height sides keep clear of the status bar and the notch.
// Its own attribute rather than `data-slot`, which consumers
// replace with theirs (`responsive-sheet-content`, `day-panel`).
data-sheet-side={side}
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
side === "right" &&
+3 -4
View File
@@ -12,10 +12,9 @@ import { Toaster as Sonner, type ToasterProps } from "sonner";
const Toaster = ({ ...props }: ToasterProps) => {
return (
<Sonner
// `theme="system"` lets Sonner match the resolved theme on the
// root element (set pre-paint by the inline theme script in
// `app/layout.tsx`) instead of pinning dark — on light-mode the
// hardcoded dark toaster contrasted badly against the page.
// A fallback only: Sonner's `"system"` follows the operating
// system's scheme, not the app's theme, so the app's toaster passes
// its resolved theme (`AppToaster` in `providers.tsx`).
theme="system"
className="toaster group"
icons={{
+8 -5
View File
@@ -14,10 +14,13 @@ import { cn } from "@/lib/utils";
*
* What it bakes in:
*
* - `text-base sm:text-sm` — iOS Safari zooms the viewport on
* focus when the font-size renders below 16 px, which yanks the
* keyboard up and leaves the user lost. 16 px on mobile holds
* the viewport; 14 px on `sm`+ keeps the compact desktop look.
* - `text-base sm:pointer-fine:text-sm` — iOS Safari zooms the
* viewport on focus when the font-size renders below 16 px, which
* yanks the keyboard up and leaves the user lost. 16 px on a touch
* screen holds the viewport at any width (a phone held sideways is
* past `sm`, an iPad past `md`); 14 px only where a mouse or
* trackpad drives (`pointer-fine`, globals.css) keeps the compact
* desktop look.
*
* - `min-h-11 sm:min-h-9` — WCAG 2.5.5 / Apple HIG floor of 44 px
* on touch screens, shrinking to 36 px once we know we have a
@@ -65,7 +68,7 @@ function Textarea({
className={cn(
// iOS zoom defence + WCAG 2.5.5 tap-target floor; see the
// primitive's docblock for the full reasoning.
"border-input bg-background text-foreground placeholder:text-muted-foreground dark:bg-input/30 min-h-11 w-full rounded-md border px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 sm:min-h-9 sm:text-sm",
"border-input bg-background text-foreground placeholder:text-muted-foreground dark:bg-input/30 min-h-11 w-full rounded-md border px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 sm:min-h-9 sm:pointer-fine:text-sm",
"focus-visible:border-input-focus aria-invalid:focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className,
+2 -2
View File
@@ -45,10 +45,10 @@ const FIELD_HEIGHT_CLASSES = "min-h-11 h-11 sm:min-h-10 sm:h-10";
// Focus vocabulary and font-size parity with `<Input>` / `<DateField>`: the
// border turns the neutral `input-focus` tone with no ring (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`
// focus) and `text-base md:pointer-fine:text-sm`
// to keep iOS Safari from auto-zooming a sub-16 px input.
const FIELD_BASE_CLASSES =
"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-input-focus 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:pointer-fine:text-sm shadow-xs transition-[color,box-shadow] focus-within:border-input-focus focus-within:outline-none";
const TIME_RE = /^([01]\d|2[0-3]):([0-5]\d)$/;