mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
fix(onboarding): 44px touch targets across wizard CTAs + carousel pager
Bring every back / skip / next / chevron / pager button on the v1.4.25 onboarding wizard up to the 44x44 px WCAG 2.5.8 floor. The previous `size="sm"` buttons measured 32 px tall, the chevron icons were 32 px square, and each dot pager target was 10 px square — all well under the target-size minimum for thumb-driven flows. Carousel slides now derive their accessible name from the slide title via `aria-labelledby`, so the polite live region (which mirrors "Slide N of M") is the single source for position announcements. The previous markup announced position twice on every swipe: once from the slide's own `aria-label`, again from the live region.
This commit is contained in:
1 parent
a400e5407b
commit
4a7195a1bc
5 files changed
+50
-27
No files matched your search
@@ -197,16 +197,16 @@ export function BaselineForm() {
|
||||
</fieldset>
|
||||
|
||||
<div className="flex items-center justify-between gap-2 pt-2">
|
||||
<Button asChild variant="ghost" size="sm">
|
||||
<Button asChild variant="ghost" className="min-h-11 min-w-11">
|
||||
<Link href="/onboarding/2">{t("onboarding.shell.back")}</Link>
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => advance({ saveProfile: false })}
|
||||
disabled={saving}
|
||||
className="min-h-11 min-w-11"
|
||||
>
|
||||
{t("onboarding.shell.skip")}
|
||||
</Button>
|
||||
@@ -214,6 +214,7 @@ export function BaselineForm() {
|
||||
type="button"
|
||||
onClick={() => advance({ saveProfile: true })}
|
||||
disabled={saving}
|
||||
className="min-h-11 min-w-11"
|
||||
>
|
||||
{saving ? <Loader2 className="size-4 animate-spin" /> : null}
|
||||
{t("onboarding.baseline.saveCta")}
|
||||
|
||||
@@ -249,20 +249,25 @@ export function GoalsChipPicker({ userId }: GoalsChipPickerProps) {
|
||||
</fieldset>
|
||||
|
||||
<div className="flex items-center justify-between gap-2 pt-2">
|
||||
<Button asChild variant="ghost" size="sm">
|
||||
<Button asChild variant="ghost" className="min-h-11 min-w-11">
|
||||
<Link href="/onboarding/0">{t("onboarding.shell.back")}</Link>
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={advance}
|
||||
disabled={advancing}
|
||||
className="min-h-11 min-w-11"
|
||||
>
|
||||
{t("onboarding.shell.skip")}
|
||||
</Button>
|
||||
<Button type="button" onClick={advance} disabled={advancing}>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={advance}
|
||||
disabled={advancing}
|
||||
className="min-h-11 min-w-11"
|
||||
>
|
||||
{t("onboarding.shell.next")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -135,7 +135,7 @@ export async function OnboardingShell({
|
||||
|
||||
<footer className="mt-8 flex items-center justify-between gap-2">
|
||||
{backHref ? (
|
||||
<Button asChild variant="ghost" size="sm">
|
||||
<Button asChild variant="ghost" className="min-h-11 min-w-11">
|
||||
<Link href={backHref}>{t("onboarding.shell.back")}</Link>
|
||||
</Button>
|
||||
) : (
|
||||
@@ -144,12 +144,12 @@ export async function OnboardingShell({
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{skipHref ? (
|
||||
<Button asChild variant="ghost" size="sm">
|
||||
<Button asChild variant="ghost" className="min-h-11 min-w-11">
|
||||
<Link href={skipHref}>{t("onboarding.shell.skip")}</Link>
|
||||
</Button>
|
||||
) : null}
|
||||
{nextHref ? (
|
||||
<Button asChild size="sm">
|
||||
<Button asChild className="min-h-11 min-w-11">
|
||||
<Link href={nextHref}>{primaryLabel}</Link>
|
||||
</Button>
|
||||
) : null}
|
||||
|
||||
@@ -152,20 +152,25 @@ export function SourceCardGrid() {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-2 pt-2">
|
||||
<Button asChild variant="ghost" size="sm">
|
||||
<Button asChild variant="ghost" className="min-h-11 min-w-11">
|
||||
<Link href="/onboarding/1">{t("onboarding.shell.back")}</Link>
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={advance}
|
||||
disabled={advancing}
|
||||
className="min-h-11 min-w-11"
|
||||
>
|
||||
{t("onboarding.shell.skip")}
|
||||
</Button>
|
||||
<Button type="button" onClick={advance} disabled={advancing}>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={advance}
|
||||
disabled={advancing}
|
||||
className="min-h-11 min-w-11"
|
||||
>
|
||||
{t("onboarding.shell.next")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -181,10 +181,7 @@ export function WelcomeCarousel() {
|
||||
data-slide-index={idx}
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
aria-label={t("onboarding.welcome.slideOf", {
|
||||
current: idx + 1,
|
||||
total: SLIDES.length,
|
||||
})}
|
||||
aria-labelledby={`onboarding-welcome-slide-${slide.id}-title`}
|
||||
className={cn(
|
||||
"bg-card border-border flex w-full shrink-0 snap-start flex-col items-center gap-4",
|
||||
"rounded-xl border p-6 text-center sm:p-8",
|
||||
@@ -196,7 +193,10 @@ export function WelcomeCarousel() {
|
||||
>
|
||||
<slide.Icon className="size-8" />
|
||||
</span>
|
||||
<h2 className="text-lg font-semibold tracking-tight">
|
||||
<h2
|
||||
id={`onboarding-welcome-slide-${slide.id}-title`}
|
||||
className="text-lg font-semibold tracking-tight"
|
||||
>
|
||||
{t(slide.titleKey)}
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-sm leading-relaxed">
|
||||
@@ -206,9 +206,11 @@ export function WelcomeCarousel() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Live region announces slide changes to AT users — matches the
|
||||
tour's pattern (tour.tsx:374) so the focus contract is
|
||||
consistent across the onboarding surface. */}
|
||||
{/* Single live region announces slide changes to AT users —
|
||||
slides themselves carry `aria-label="slide N of total"` so a
|
||||
second sr-only mirror would double-announce on every move.
|
||||
The carousel-level region wins because it's tied to the
|
||||
chevron + dot pager via `aria-controls`. */}
|
||||
<p id={liveRegionId} aria-live="polite" className="sr-only">
|
||||
{t("onboarding.welcome.slideOf", {
|
||||
current: active + 1,
|
||||
@@ -221,11 +223,12 @@ export function WelcomeCarousel() {
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
size="icon"
|
||||
onClick={() => scrollToSlide(Math.max(0, active - 1))}
|
||||
disabled={active === 0}
|
||||
aria-label={t("onboarding.welcome.prevSlide")}
|
||||
aria-controls={liveRegionId}
|
||||
className="min-h-11 min-w-11"
|
||||
>
|
||||
<ChevronLeft className="size-4" />
|
||||
</Button>
|
||||
@@ -233,7 +236,7 @@ export function WelcomeCarousel() {
|
||||
<div
|
||||
role="tablist"
|
||||
aria-label={t("onboarding.welcome.carouselLabel")}
|
||||
className="flex items-center gap-2"
|
||||
className="flex items-center"
|
||||
>
|
||||
{SLIDES.map((slide, idx) => (
|
||||
<button
|
||||
@@ -245,24 +248,32 @@ export function WelcomeCarousel() {
|
||||
index: idx + 1,
|
||||
})}
|
||||
onClick={() => scrollToSlide(idx)}
|
||||
className={cn(
|
||||
"size-2.5 rounded-full transition-colors duration-150 ease-out motion-reduce:transition-none",
|
||||
active === idx ? "bg-primary" : "bg-muted hover:bg-muted-foreground/30",
|
||||
)}
|
||||
/>
|
||||
className="inline-flex size-11 items-center justify-center"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
"size-2.5 rounded-full transition-colors duration-150 ease-out motion-reduce:transition-none",
|
||||
active === idx
|
||||
? "bg-primary"
|
||||
: "bg-muted hover:bg-muted-foreground/30",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
size="icon"
|
||||
onClick={() =>
|
||||
scrollToSlide(Math.min(SLIDES.length - 1, active + 1))
|
||||
}
|
||||
disabled={active === SLIDES.length - 1}
|
||||
aria-label={t("onboarding.welcome.nextSlide")}
|
||||
aria-controls={liveRegionId}
|
||||
className="min-h-11 min-w-11"
|
||||
>
|
||||
<ChevronRight className="size-4" />
|
||||
</Button>
|
||||
@@ -274,6 +285,7 @@ export function WelcomeCarousel() {
|
||||
size="lg"
|
||||
onClick={handleGetStarted}
|
||||
disabled={advancing}
|
||||
className="min-h-11"
|
||||
>
|
||||
{t("onboarding.welcome.cta")}
|
||||
</Button>
|
||||
|
||||
Reference in new issue
Block a user