mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
fix(meds): focus management and full-row hit area on the wizard
Step advance moves focus into the new step's first input so keyboard and screen-reader users land where they need to type. The cadence-picker rows now register clicks across the full 44 px row, not just the radio glyph + label. The "describe it" trigger on step 1 rises to 44 px so it clears the WCAG 2.5.5 floor the rest of the codebase honours. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
4ccb70aff8
commit
a730ebf07b
2 files changed
+39
-13
No files matched your search
@@ -308,7 +308,13 @@ function CadenceOption({
|
||||
data-slot="cadence-option"
|
||||
data-kind={kind}
|
||||
>
|
||||
<div className="flex min-h-11 items-center gap-3">
|
||||
{/* Wrapping the row in <label> makes the entire 44 px-tall card
|
||||
a click target for the radio (WCAG 2.5.5) instead of just the
|
||||
16 px radio glyph + the label text node. */}
|
||||
<label
|
||||
htmlFor={inputId}
|
||||
className="flex min-h-11 cursor-pointer items-center gap-3 select-none"
|
||||
>
|
||||
<input
|
||||
id={inputId}
|
||||
type="radio"
|
||||
@@ -316,17 +322,11 @@ function CadenceOption({
|
||||
checked={selected}
|
||||
disabled={disabled}
|
||||
onChange={onPick}
|
||||
aria-label={label}
|
||||
className="text-primary focus-visible:ring-ring h-4 w-4 cursor-pointer focus-visible:outline-none focus-visible:ring-2"
|
||||
data-slot="cadence-option-radio"
|
||||
/>
|
||||
<Label
|
||||
htmlFor={inputId}
|
||||
className="cursor-pointer select-none text-sm font-medium"
|
||||
>
|
||||
{label}
|
||||
</Label>
|
||||
</div>
|
||||
<span className="text-sm font-medium">{label}</span>
|
||||
</label>
|
||||
|
||||
{selected && kind === "weekdays" && (
|
||||
<div className="mt-3">
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
* .errors.fieldRequired — "Required"
|
||||
*/
|
||||
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { Loader2, Sparkles } from "lucide-react";
|
||||
@@ -522,10 +522,37 @@ export function CreationWizard({
|
||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||
const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
|
||||
|
||||
// Ref on the step body so we can pull focus into the first
|
||||
// interactive control whenever the wizard advances. Without this a
|
||||
// keyboard / screen-reader user stays parked on the Back / Next
|
||||
// button across step transitions instead of landing on the new
|
||||
// step's input.
|
||||
const bodyRef = useRef<HTMLDivElement | null>(null);
|
||||
const isFirstRenderRef = useRef(true);
|
||||
|
||||
const applyPartial = useCallback((partial: Partial<WizardPayload>) => {
|
||||
setPayload((prev) => ({ ...prev, ...partial }));
|
||||
}, []);
|
||||
|
||||
// Focus the first interactive element when the step changes.
|
||||
// Skip the initial render so opening the wizard never steals focus
|
||||
// from the page chrome.
|
||||
useEffect(() => {
|
||||
if (isFirstRenderRef.current) {
|
||||
isFirstRenderRef.current = false;
|
||||
return;
|
||||
}
|
||||
const node = bodyRef.current;
|
||||
if (!node) return;
|
||||
const target = node.querySelector<HTMLElement>(
|
||||
'input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex="-1"])',
|
||||
);
|
||||
if (!target) return;
|
||||
// preventScroll keeps the focus jump silent for reduced-motion
|
||||
// users; the surrounding layout already keeps the body in view.
|
||||
target.focus({ preventScroll: true });
|
||||
}, [step]);
|
||||
|
||||
const canContinue = useMemo(
|
||||
() => validateStep(payload, step),
|
||||
[payload, step],
|
||||
@@ -627,7 +654,7 @@ export function CreationWizard({
|
||||
<p className="text-muted-foreground text-xs">{stepOf}</p>
|
||||
<CardTitle className="text-base">{stepTitle}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<CardContent className="space-y-4" ref={bodyRef}>
|
||||
{step === 1 && (
|
||||
<Step1NameDose
|
||||
payload={payload}
|
||||
@@ -733,9 +760,8 @@ function Step1NameDose({
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onNlClick}
|
||||
className="w-full"
|
||||
className="h-11 w-full"
|
||||
data-slot="wizard-nl-button"
|
||||
>
|
||||
<Sparkles className="mr-2 h-4 w-4" aria-hidden="true" />
|
||||
|
||||
Reference in new issue
Block a user