mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
fix(a11y): give the adherence and efficacy charts a keyboard way to their days
Both charts open a day on a click or a tap but carried no data table and no day link, so a keyboard or screen-reader user could not reach the days at all. Render the chart's points as a data table with day links, and have the coverage guard require that way on every day-linked chart or name where it lives instead.
This commit is contained in:
1 parent
efbaf02335
commit
ecf56c6a1f
3 files changed
+85
-1
No files matched your search
@@ -63,6 +63,7 @@ import { formatDateShort } from "@/lib/format";
|
||||
import { makeFormatters } from "@/lib/format-locale";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { RichChartTooltip, type RichTooltipRow } from "./chart-tooltip";
|
||||
import { ChartDataTable } from "./chart-data-table";
|
||||
import { ChartEmptyState } from "./chart-empty-state";
|
||||
import { ChartErrorState } from "./chart-error-state";
|
||||
import { ChartOverlayControls } from "./chart-overlay-controls";
|
||||
@@ -743,6 +744,23 @@ export function MedicationComplianceChart({
|
||||
insetLeft={8 + 48}
|
||||
insetRight={8}
|
||||
/>
|
||||
{/* The keyboard and screen-reader way to a day: the plot opens
|
||||
days on a click or a tap only. */}
|
||||
{chartDays.active ? (
|
||||
<ChartDataTable
|
||||
points={chartData.map((point) => ({
|
||||
date: point.date,
|
||||
timestamp: point.timestamp,
|
||||
rate: point.rate,
|
||||
}))}
|
||||
columns={[{ key: "rate", label: t("dashboard.compliance7d") }]}
|
||||
formatValue={(value) => `${fmt.integer(value)} %`}
|
||||
formatDate={tzFmt.date}
|
||||
bucket="day"
|
||||
metricLabel={displayTitle}
|
||||
dayLinks
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,10 @@ import { describe, expect, it } from "vitest";
|
||||
* B. a file that draws its own Recharts chart over a date axis (an
|
||||
* `<XAxis>` keyed on a date-shaped field, or `scale="time"`) calls
|
||||
* `useChartDayLinks` and renders its parts (`ChartDayFooter`,
|
||||
* `OPEN_DAY_LINE`, the tooltip's props and its touch action);
|
||||
* `OPEN_DAY_LINE`, the tooltip's props and its touch action), and
|
||||
* offers the keyboard way to the same days: a `<ChartDataTable
|
||||
* dayLinks>` or a `DayLink` of its own, or an entry on
|
||||
* `KEYBOARD_ELSEWHERE` naming where that way lives;
|
||||
* C. a calendar heatmap (anything laid out by `heatmapDays`) calls
|
||||
* `useHeatmapDay`.
|
||||
*
|
||||
@@ -83,6 +86,29 @@ const NOT_HERE: Record<string, string> = {
|
||||
"The server's own resource use, not a health record.",
|
||||
};
|
||||
|
||||
/**
|
||||
* Day-linked charts whose keyboard and screen-reader way to a day is not in
|
||||
* their own file. Path → the file that carries it, and why there.
|
||||
*/
|
||||
const KEYBOARD_ELSEWHERE: Record<string, { file: string; why: string }> = {
|
||||
"src/components/labs/lab-biomarker-chart.tsx": {
|
||||
file: "src/components/labs/lab-history-list.tsx",
|
||||
why: "The marker's readings list (its values page) dates every reading with a day link; the chart is the shape, the list the numbers.",
|
||||
},
|
||||
"src/components/mental-health/assessment-history-chart.tsx": {
|
||||
file: "src/components/mental-health/assessment-history.tsx",
|
||||
why: "The questionnaire history renders the chart and, under it, the list of every result with its day link.",
|
||||
},
|
||||
};
|
||||
|
||||
/** True when the file itself offers a keyboard way to its days. */
|
||||
function hasKeyboardDoor(text: string): boolean {
|
||||
return (
|
||||
jsxTags(text, "ChartDataTable").some(mountOpensDays) ||
|
||||
/<DayLink(?:At|Stated)?[\s>]/.test(text)
|
||||
);
|
||||
}
|
||||
|
||||
function walk(path: string): string[] {
|
||||
const abs = join(ROOT, path);
|
||||
if (statSync(abs).isFile()) return [abs];
|
||||
@@ -201,6 +227,12 @@ function findMissingDoors(): {
|
||||
if (wired) {
|
||||
doors.charts += 1;
|
||||
doors.chartFiles.push(file);
|
||||
if (!hasKeyboardDoor(text) && !(file in KEYBOARD_ELSEWHERE)) {
|
||||
missing.push({
|
||||
file,
|
||||
why: "a day-linked chart with no keyboard way to its days (a <ChartDataTable dayLinks> or a DayLink)",
|
||||
});
|
||||
}
|
||||
} else if (!listed) {
|
||||
missing.push({
|
||||
file,
|
||||
@@ -246,6 +278,19 @@ describe("every chart drawn in days opens its days, or says why not", () => {
|
||||
expect(doors.heatmaps).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it("every KEYBOARD_ELSEWHERE entry points at a file with day links", () => {
|
||||
for (const [chart, { file, why }] of Object.entries(KEYBOARD_ELSEWHERE)) {
|
||||
expect(source.get(chart), `${chart} does not exist`).toBeDefined();
|
||||
expect(hasKeyboardDoor(source.get(chart)!), `${chart} has its own`).toBe(
|
||||
false,
|
||||
);
|
||||
const text = source.get(file);
|
||||
expect(text, `${file} does not exist`).toBeDefined();
|
||||
expect(hasKeyboardDoor(text!), `${file} has no day link`).toBe(true);
|
||||
expect(why.length).toBeGreaterThan(30);
|
||||
}
|
||||
});
|
||||
|
||||
it("every NOT_HERE entry names an existing chart and gives a reason", () => {
|
||||
for (const [file, reason] of Object.entries(NOT_HERE)) {
|
||||
const text = source.get(file);
|
||||
|
||||
@@ -32,9 +32,11 @@ import {
|
||||
RichChartTooltip,
|
||||
type RichTooltipRow,
|
||||
} from "@/components/charts/chart-tooltip";
|
||||
import { ChartDataTable } from "@/components/charts/chart-data-table";
|
||||
import {
|
||||
ChartDayFooter,
|
||||
OPEN_DAY_LINE,
|
||||
dayAnchor,
|
||||
useChartDayLinks,
|
||||
} from "@/components/day/chart-day-links";
|
||||
import { dateKeyOfInstant } from "@/components/day/day-url";
|
||||
@@ -406,6 +408,25 @@ export function EfficacyChart({
|
||||
insetLeft={40}
|
||||
insetRight={8}
|
||||
/>
|
||||
{/* The keyboard and screen-reader way to a day: the plot opens days on
|
||||
a click or a tap only. Each row stands at its day's anchor, the
|
||||
calendar day the plot's own door opens. */}
|
||||
{chartDays.active ? (
|
||||
<ChartDataTable
|
||||
points={displaySeries.map((point, index) => ({
|
||||
date: seriesDays[index]!,
|
||||
timestamp: dayAnchor(seriesDays[index]!),
|
||||
value: point.value,
|
||||
}))}
|
||||
columns={[{ key: "value", label: target.label }]}
|
||||
unit={unitSuffix ?? undefined}
|
||||
formatValue={(value) => String(value)}
|
||||
formatDate={fmt.date}
|
||||
bucket="day"
|
||||
metricLabel={target.label}
|
||||
dayLinks
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user