mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
fix(ui): reserve Button loader space to eliminate CLS during in-flight requests
Lighthouse v1.4.33 run flagged a 0.186 CLS attached to a
`div.space-y-2 > Button > Loader-Circle` element. Root cause: the
two heavy submit buttons in `/bugreport` and the medications import
dialog drew `{loading && <Loader2/>}<Icon/>` -- rendering BOTH icons
during the in-flight request and growing the button by ~24 px. The
fix is to swap (not append) the leading icon, so Loader2 occupies
the same 16x16 slot the static icon would.
- src/app/bugreport/page.tsx: ternary swap on `loading`, pass
`aria-busy` for screen-reader narration.
- src/app/medications/page.tsx (Import dialog): same shape.
- src/components/ui/button.tsx: document the loading-state contract
on the primitive so future contributors apply the swap convention
by default. (Callsites inside `src/components/settings/**` are
owned by IW4 -- left untouched, the doc-comment makes the convention
visible from the primitive.)
This commit is contained in:
1 parent
523ee0c7d1
commit
e65cdc97b2
3 files changed
+55
-5
No files matched your search
@@ -227,9 +227,22 @@ export default function BugReportPage() {
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-end pt-2">
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />}
|
||||
<Bug className="mr-2 h-4 w-4" />
|
||||
<Button type="submit" disabled={loading} aria-busy={loading || undefined}>
|
||||
{/*
|
||||
v1.4.33 IW9 — swap the icon on loading (Loader2 takes
|
||||
the same 16x16 slot as the Bug icon) so the button
|
||||
width is stable across the disabled flip. The previous
|
||||
`{loading && <Loader2/>}<Bug/>` rendered both icons
|
||||
during the in-flight request and grew the button by
|
||||
~24 px — the CLS regression flagged in the v1.4.33
|
||||
Lighthouse punch list (0.186 on
|
||||
`div.space-y-2 > Button > Loader-Circle`).
|
||||
*/}
|
||||
{loading ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<Bug className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
{t("bugreport.submit")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -505,9 +505,21 @@ function IntakeImportDialog({
|
||||
<Button
|
||||
onClick={handleImport}
|
||||
disabled={importing || !jsonText.trim()}
|
||||
aria-busy={importing || undefined}
|
||||
>
|
||||
{importing && <Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />}
|
||||
<Upload className="mr-2 h-4 w-4" />
|
||||
{/*
|
||||
v1.4.33 IW9 — icon swap (not append) on `importing`
|
||||
to keep the button width stable through the
|
||||
in-flight request. Pre-v1.4.33 both icons painted
|
||||
during loading and the button grew by ~24 px, which
|
||||
showed up as a CLS hit on the medications import
|
||||
dialog.
|
||||
*/}
|
||||
{importing ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<Upload className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
{t("common.import")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,31 @@ import { Slot } from "radix-ui";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* v1.4.33 IW9 — loading-state convention.
|
||||
*
|
||||
* Callers driving a transient loading state on a button must SWAP the
|
||||
* leading icon for `<Loader2 />` rather than APPENDING the spinner
|
||||
* next to the existing icon. The legacy `{loading && <Loader2/>}<Icon/>`
|
||||
* pattern grew the button by ~24 px during the request, which produced
|
||||
* the 0.186 CLS hit flagged on the v1.4.33 Lighthouse run. The
|
||||
* spinner and the icon are both 16 px (`h-4 w-4`); swapping keeps the
|
||||
* intrinsic button width stable.
|
||||
*
|
||||
* ```tsx
|
||||
* <Button disabled={loading} aria-busy={loading || undefined}>
|
||||
* {loading ? (
|
||||
* <Loader2 className="mr-2 h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
* ) : (
|
||||
* <Icon className="mr-2 h-4 w-4" />
|
||||
* )}
|
||||
* {label}
|
||||
* </Button>
|
||||
* ```
|
||||
*
|
||||
* Also: pass `aria-busy={loading || undefined}` so screen readers
|
||||
* announce the in-flight state.
|
||||
*/
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user