From a730ebf07be6acde88de0f5968fe626a09068931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 28 May 2026 10:20:23 +0200 Subject: [PATCH] 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) --- .../medications/scheduling/CadencePicker.tsx | 18 +++++----- .../medications/scheduling/CreationWizard.tsx | 34 ++++++++++++++++--- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/src/components/medications/scheduling/CadencePicker.tsx b/src/components/medications/scheduling/CadencePicker.tsx index 85b886448..7a84ef0b5 100644 --- a/src/components/medications/scheduling/CadencePicker.tsx +++ b/src/components/medications/scheduling/CadencePicker.tsx @@ -308,7 +308,13 @@ function CadenceOption({ data-slot="cadence-option" data-kind={kind} > -
+ {/* Wrapping the row in
+ {label} + {selected && kind === "weekdays" && (
diff --git a/src/components/medications/scheduling/CreationWizard.tsx b/src/components/medications/scheduling/CreationWizard.tsx index acac32ecb..f0564a4e7 100644 --- a/src/components/medications/scheduling/CreationWizard.tsx +++ b/src/components/medications/scheduling/CreationWizard.tsx @@ -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(null); const [fieldErrors, setFieldErrors] = useState>({}); + // 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(null); + const isFirstRenderRef = useRef(true); + const applyPartial = useCallback((partial: Partial) => { 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( + '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({

{stepOf}

{stepTitle} - + {step === 1 && (