mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
fix(a11y): keyboard-reachable admin log rows, range-bar marker, heatmap cells
Admin app-log rows opened their JSON detail dialog through a click-only <tr> with no keyboard path; the Action cell now wraps a real <button> that carries the same handler plus a descriptive aria-label, leaving the row onClick as a pointer convenience. The target range-bar's current-value marker showed value + band only in a hover-only tooltip on a non-focusable div. It is now focusable (tabIndex, role=img) with an aria-label that reuses the same value / range / delta copy the tooltip already renders, so focus opens the tooltip and a screen reader gets the text alternative either way. Mood and medication-compliance heatmap day cells were pointer-only SVG rects. Each cell is now focusable with its own per-day aria-label (reusing the existing tooltip copy), and Escape dismisses any open tooltip — pinned (touch) or focus-shown. Reference: 2026-07-17 a11y audit, H1 + M1 + M2.
This commit is contained in:
1 parent
247bc4f33c
commit
4c436d7dfc
15 files changed
+243
-27
No files matched your search
@@ -98,3 +98,16 @@ describe("AppLogPreviewSection — summary lives outside the scroll wrapper", ()
|
||||
expect(wrapper![0]).not.toContain("app-log-preview-summary");
|
||||
});
|
||||
});
|
||||
|
||||
describe("AppLogPreviewSection — row detail is keyboard-reachable (2026-07-17 a11y audit H1)", () => {
|
||||
// The row itself stays a pointer-only convenience (`onClick` on the
|
||||
// `<tr>`); a real `<button>` in the Action cell gives keyboard / switch
|
||||
// users a focusable, activatable path to the same detail dialog.
|
||||
it("renders a real button carrying the action label and a descriptive aria-label", () => {
|
||||
const html = render();
|
||||
expect(html).toMatch(
|
||||
/<button[^>]*type="button"[^>]*>auth\.login\.success</,
|
||||
);
|
||||
expect(html).toContain('aria-label="View details for auth.login.success');
|
||||
});
|
||||
});
|
||||
@@ -230,29 +230,46 @@ export function AppLogPreviewSection() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-border divide-y">
|
||||
{events.map((event, i) => (
|
||||
<tr
|
||||
key={`${event.request_id}-${i}`}
|
||||
className={`${i % 2 === 0 ? "bg-muted/30" : ""} hover:bg-muted cursor-pointer`}
|
||||
onClick={() => setSelected(event)}
|
||||
>
|
||||
<td className="px-2 py-2">{levelIcon(event.level)}</td>
|
||||
<td className="text-muted-foreground px-2 py-2 text-xs whitespace-nowrap">
|
||||
{formatDateTime(event.timestamp)}
|
||||
</td>
|
||||
<td className="px-2 py-2 text-xs">
|
||||
{event.action?.name ??
|
||||
(`${event.http?.method ?? ""} ${event.http?.path ?? ""}`.trim() ||
|
||||
event.kind)}
|
||||
</td>
|
||||
<td className="text-muted-foreground px-2 py-2 text-right font-mono text-xs">
|
||||
{event.duration_ms} ms
|
||||
</td>
|
||||
<td className="text-muted-foreground px-2 py-2 font-mono text-xs">
|
||||
{event.trace_id.slice(0, 8)}…
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{events.map((event, i) => {
|
||||
const actionLabel =
|
||||
event.action?.name ??
|
||||
(`${event.http?.method ?? ""} ${event.http?.path ?? ""}`.trim() ||
|
||||
event.kind);
|
||||
return (
|
||||
<tr
|
||||
key={`${event.request_id}-${i}`}
|
||||
className={`${i % 2 === 0 ? "bg-muted/30" : ""} hover:bg-muted cursor-pointer`}
|
||||
// Pointer convenience only — the row itself is not
|
||||
// keyboard-reachable; the real interactive control is
|
||||
// the button below (2026-07-17 a11y audit H1).
|
||||
onClick={() => setSelected(event)}
|
||||
>
|
||||
<td className="px-2 py-2">{levelIcon(event.level)}</td>
|
||||
<td className="text-muted-foreground px-2 py-2 text-xs whitespace-nowrap">
|
||||
{formatDateTime(event.timestamp)}
|
||||
</td>
|
||||
<td className="px-2 py-2 text-xs">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelected(event)}
|
||||
className="focus-visible:ring-ring w-full rounded text-left underline-offset-2 hover:underline focus-visible:ring-2 focus-visible:outline-none"
|
||||
aria-label={t("admin.section.app-logs.viewDetails", {
|
||||
action: actionLabel,
|
||||
timestamp: formatDateTime(event.timestamp),
|
||||
})}
|
||||
>
|
||||
{actionLabel}
|
||||
</button>
|
||||
</td>
|
||||
<td className="text-muted-foreground px-2 py-2 text-right font-mono text-xs">
|
||||
{event.duration_ms} ms
|
||||
</td>
|
||||
<td className="text-muted-foreground px-2 py-2 font-mono text-xs">
|
||||
{event.trace_id.slice(0, 8)}…
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user