From 9d7157e32ae54f4f837cb2d78502f5c1062db34c Mon Sep 17 00:00:00 2001 From: Christopher Rotnes Date: Sun, 3 May 2026 18:02:03 +0200 Subject: [PATCH] a11y: add aria-live regions and step focus management - Report.jsx: wrap data loading/error and recs loading/error in aria-live="polite" so screen readers hear state changes - History.jsx: add aria-live + aria-busy to calendar skeleton and session accordion skeleton - Home.jsx: add aria-live + aria-busy to last-session loading indicator - MuscleMap.jsx: move focus to heading on step change (headingRef + useEffect); add role="list"/role="listitem" and aria-current="step" to the step progress indicator; wrap upload error in aria-live Closes #73, #72, #80 (partial) Co-Authored-By: Claude Sonnet 4.6 --- app/src/components/History.jsx | 4 +- app/src/components/Home.jsx | 71 +++++++++++++++++++++++--------- app/src/components/MuscleMap.jsx | 34 ++++++++------- app/src/components/Report.jsx | 8 +++- 4 files changed, 80 insertions(+), 37 deletions(-) diff --git a/app/src/components/History.jsx b/app/src/components/History.jsx index e9c7511..56b1db0 100644 --- a/app/src/components/History.jsx +++ b/app/src/components/History.jsx @@ -357,7 +357,7 @@ export default function History({ onShowHome, onShowLogger, onShowHistory, onSho /> {loading ? ( -
+
) : ( @@ -388,7 +388,7 @@ export default function History({ onShowHome, onShowLogger, onShowHistory, onSho )} {loadingSession && ( -
+
)} diff --git a/app/src/components/Home.jsx b/app/src/components/Home.jsx index f4a5ff7..cfc281d 100644 --- a/app/src/components/Home.jsx +++ b/app/src/components/Home.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { format, parseISO, startOfISOWeek, addDays } from "date-fns"; import { nb } from "date-fns/locale"; import { InlineLoading } from "@carbon/react"; @@ -45,7 +45,8 @@ export default function Home({ }) { const [lastSession, setLastSession] = useState(undefined); const [weekSessions, setWeekSessions] = useState(undefined); - const [hoveredDay, setHoveredDay] = useState(null); + const [tooltip, setTooltip] = useState(null); + const weekStripRef = useRef(); const [syncState, setSyncState] = useState(null); const [syncMsg, setSyncMsg] = useState(''); @@ -129,7 +130,7 @@ export default function Home({ {isToday ? "DAGENS ØKT" : "SISTE ØKT"} {lastSession === undefined && ( -
+
)} @@ -222,26 +223,58 @@ export default function Home({ {/* Weekly strip */} UKEN SÅ LANGT
-
- {weekDays.map(({ label, count, date }, i) => ( -
-
- {label} +
+
+ {weekDays.map(({ label, count, date, names }, i) => ( +
+
+ {label} +
+
0 ? () => onShowHistoryWithDate(date) : undefined} + onMouseEnter={count > 0 ? (e) => { + const rect = weekStripRef.current?.getBoundingClientRect(); + if (!rect) return; + setTooltip({ names, x: e.clientX - rect.left, y: e.clientY - rect.top }); + } : undefined} + onMouseMove={count > 0 ? (e) => { + const rect = weekStripRef.current?.getBoundingClientRect(); + if (!rect) return; + setTooltip(prev => prev ? { ...prev, x: e.clientX - rect.left, y: e.clientY - rect.top } : prev); + } : undefined} + onMouseLeave={count > 0 ? () => setTooltip(null) : undefined} + style={{ height: 36, background: heatColor(count), border: "1px solid var(--cds-border-subtle-01)", cursor: count > 0 ? "pointer" : "default" }} + />
-
0 ? () => onShowHistoryWithDate(date) : undefined} - onMouseEnter={count > 0 ? () => setHoveredDay(i) : undefined} - onMouseLeave={count > 0 ? () => setHoveredDay(null) : undefined} - style={{ height: 36, background: heatColor(count), border: "1px solid var(--cds-border-subtle-01)", cursor: count > 0 ? "pointer" : "default" }} - /> + ))} +
+ {tooltip && tooltip.names.length > 0 && ( +
+ {tooltip.names.map((name, i) => ( +
0 ? "4px 0 0" : 0, + }}> + {name} +
+ ))}
- ))} + )}
{weekSessions !== undefined && ( -
- {hoveredDay !== null && weekDays[hoveredDay]?.names.length > 0 - ? weekDays[hoveredDay].names.join(" · ") - : `${weekSessionCount} ØKTE${weekSessionCount !== 1 ? "R" : ""} · ${weekMuscleCount} MUSKELGRUPPE${weekMuscleCount !== 1 ? "R" : ""}`} +
+ {`${weekSessionCount} ØKTE${weekSessionCount !== 1 ? "R" : ""} · ${weekMuscleCount} MUSKELGRUPPE${weekMuscleCount !== 1 ? "R" : ""}`}
)}
diff --git a/app/src/components/MuscleMap.jsx b/app/src/components/MuscleMap.jsx index 2b30187..d6dc330 100644 --- a/app/src/components/MuscleMap.jsx +++ b/app/src/components/MuscleMap.jsx @@ -123,6 +123,8 @@ export default function MuscleMap({ onShowHome, onShowLogger, onShowHistory, onS }, [templatePreload]); const stepIndex = { upload: 0, analyzing: 0, confirm: 1, muscles: 2 }[step] ?? 0; + const headingRef = useRef(); + useEffect(() => { headingRef.current?.focus(); }, [step]); const exerciseMuscleMap = useMemo(() => buildMuscleMapFromExercises(exercises), [exercises]); const addImage = useCallback(async (file) => { @@ -214,14 +216,16 @@ export default function MuscleMap({ onShowHome, onShowLogger, onShowHistory, onS currentView={currentView} >
- LOGG ØKT - {STEP_HEADINGS[step]} -
+
+ LOGG ØKT + {STEP_HEADINGS[step]} +
+
{STEP_LABELS.map((label, idx) => { const isComplete = stepIndex > idx; const isActive = stepIndex === idx; return ( -
handleFiles(e.target.files)} /> - {error && ( - - )} +
+ {error && ( + + )} +
+
{loadingRecs && ( )} @@ -453,10 +455,11 @@ export default function Report({ onShowHome, onShowLogger, onShowHistory, onShow title="Feil:" subtitle={recsError} hideCloseButton - + style={{ marginTop: 12 }} /> )} +
{recs && recs.length > 0 && (() => { const recPrimary = [...new Set(recs.flatMap(r => r.primary || []))]; @@ -531,6 +534,7 @@ export default function Report({ onShowHome, onShowLogger, onShowHistory, onShow )} )} +
);