mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
Eight hotfix-ready UX/a11y findings from the v1.4.34.3 UX audit
land as one batch.
i18n leaks (3 primitives):
- Dialog + Sheet close-X carried hardcoded `<span sr-only>Close</span>`
so a German user heard 'Close, button' on every modal. Both lift
`useTranslations()` and read `common.close`.
- ChartSkeleton's sr-only loading announcement was English-only.
The component is already a client island; reads `charts.loadingLabel`
from each locale now. New key shipped in DE / EN / FR / ES / IT / PL.
- Notifications-section breadcrumb `<nav aria-label="Breadcrumb">`
was hardcoded. Routes through `t("nav.breadcrumb")` with a new
key in each locale.
Focus rings (mobile + tablet nav):
- Bottom-nav primary links + the More overflow trigger relied on a
`text-muted-foreground` → `text-primary` colour transition only;
keyboard / switch-control users saw no focus indicator at all.
Added `focus-visible:ring-ring/50 focus-visible:ring-2
focus-visible:outline-none focus-visible:ring-offset-2` matching
the v1.4.33 sidebar treatment.
- Mobile top-bar trigger had `focus:outline-none` without a
`focus-visible` replacement. Same treatment applied.
Tap-target floor (WCAG 2.5.5):
- Input / NativeSelect / Select / DateTimeInput were locked to
h-10 (40 px) on every viewport. Switched to `h-11 sm:h-10` so
mobile clears the 44 px floor while desktop keeps the 40 px norm
that the Button primitive already converged on in v1.4.33.
Admin chip-strip auto-scroll-into-view:
- The admin shell shipped 13 chips but lacked the
`scrollIntoView({inline: 'center'})` effect the settings shell
got in v1.4.33. Admins tapping a chip past the right edge landed
on the new page with the strip scrolled to the left and the
active chip off-screen. Copied the settings-shell pattern verbatim.
List-page subtitles on mobile:
- Measurements / Mood / Medications pages hid their descriptive
subtitle on `<sm` with `hidden sm:block`. Comparable apps keep
the subtitle on mobile because it doubles as contextual help.
Dropped the hide rule; the H1 still anchors the visual.
Insights empty-state CTA vocabulary:
- `ALLOWED_ADD_TYPES` on `/measurements` accepted tokens (`GLUCOSE`,
`TEMPERATURE`, `HEART_RATE`, `BMI`) that didn't match the
`MEASUREMENT_TYPES` enum the form uses. Several `/insights/*`
empty-state CTAs emitted those mismatched tokens and the dialog
silently failed to open. Replaced with a single source of truth
derived from `MEASUREMENT_TYPES` plus a normalisation map for
legacy tokens. Added BODY_TEMPERATURE to the form's enum row.
New unit test walks every `?add=` token emitted under
`src/app/insights/**` and asserts it resolves to a real form type.
60 lines
2.5 KiB
TypeScript
60 lines
2.5 KiB
TypeScript
"use client";
|
|
|
|
import * as React from "react";
|
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
/**
|
|
* v1.4.27 MB7 / CF-52 — shared `<select>` primitive.
|
|
*
|
|
* Three settings/admin surfaces (`AccountSection`, `TimezonePicker`,
|
|
* `GeneralSettingsSection`) each declared a local `NATIVE_SELECT_CLASS`
|
|
* constant that traced the same visual contract as the shadcn
|
|
* `<SelectTrigger>` (border + bg + ring tokens, focus vocabulary).
|
|
* The three copies had drifted slightly — one dropped `w-full`,
|
|
* another dropped the `shadow-xs` token — so the three forms rendered
|
|
* at subtly different weights despite reading "the same select" to
|
|
* the user.
|
|
*
|
|
* The primitive collapses the duplicates. Consumers wrap it like a
|
|
* raw `<select>` and the className still composes via `cn(...)` so
|
|
* site-specific tweaks (`sm:max-w-sm`, etc.) keep working.
|
|
*
|
|
* Height: `h-10` (40 px) — matches the v1.4.27 tap-target floor on
|
|
* `<Input>` and `<SelectTrigger>` (MB2). The legacy `h-9` (36 px)
|
|
* the three call sites carried was the pre-v1.4.27 contract; the
|
|
* primitive extraction lands at the new floor so every form input on
|
|
* the same row reads at the same height.
|
|
*
|
|
* Why native? The shadcn `<Select>` from `@radix-ui/react-select` is
|
|
* the better keyboard experience on desktop but pulls in extra
|
|
* client JS and does not show the iOS / Android native picker.
|
|
* Settings/admin surfaces benefit from the native picker (especially
|
|
* for the 400-entry timezone list) so we deliberately stay on
|
|
* `<select>` here. The styled primitive keeps the visual rhythm
|
|
* uniform with the rest of the form's `<Input>` + `<SelectTrigger>`
|
|
* rows.
|
|
*/
|
|
// v1.4.34 IW-G — floor at 44 px on mobile to clear WCAG 2.5.5; shrink
|
|
// to 40 px on sm+ where the pointer precision is higher.
|
|
const NATIVE_SELECT_CLASS =
|
|
"border-input bg-background text-foreground ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-11 sm:h-10 w-full rounded-md border px-3 py-1 text-sm shadow-xs transition-[color,box-shadow] focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none";
|
|
|
|
export type NativeSelectProps = React.SelectHTMLAttributes<HTMLSelectElement>;
|
|
|
|
export const NativeSelect = React.forwardRef<
|
|
HTMLSelectElement,
|
|
NativeSelectProps
|
|
>(function NativeSelect({ className, children, ...props }, ref) {
|
|
return (
|
|
<select
|
|
ref={ref}
|
|
data-slot="native-select"
|
|
className={cn(NATIVE_SELECT_CLASS, className)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</select>
|
|
);
|
|
});
|