Skip to content

Commit 34ea2cd

Browse files
committed
feat(accessibility): add accesibility changes
1 parent ef62217 commit 34ea2cd

10 files changed

Lines changed: 154 additions & 38 deletions

File tree

packages/components/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { ColorSchemeProvider, useColorScheme, useResolvedTheme } from "./context
1111
export type { ChartColorScheme, EchartsThemeValue } from "./themes";
1212
export type { ChartProviderProps } from "./context/ChartProvider";
1313
export type { DashboardProviderProps } from "./context/DashboardProvider";
14-
export type { DashboardProviderGridProps } from "./context/DashboardGridProvider";
14+
export type { DashboardGridProviderProps } from "./context/DashboardGridProvider";
1515
export type { DashboardData, DashboardSlot, RenderDashboardProps } from "./components/RenderDashboard";
1616

1717
export * from "./types";

packages/webapp/src/components/ChartOptions.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -165,17 +165,20 @@ function ChartOptions({
165165

166166
return (
167167
<div key={field.name} className={`form-control ${gridSpan}`}>
168-
<label className="label cursor-pointer justify-start gap-3">
168+
<label htmlFor={`opt-${field.name}`} className="label">
169+
<span className="label-text font-medium">
170+
{translateLabel(field.label)}
171+
</span>
172+
</label>
173+
<div className="h-12 flex items-center">
169174
<input
175+
id={`opt-${field.name}`}
170176
className="checkbox checkbox-primary"
171177
type="checkbox"
172178
{...field.otherProps}
173179
{...register(field.name, { required: field.required })}
174180
/>
175-
<span className="label-text font-medium">
176-
{translateLabel(field.label)}
177-
</span>
178-
</label>
181+
</div>
179182
{errors[field.name] && (
180183
<label className="label">
181184
<span className="label-text-alt text-error">

packages/webapp/src/components/EditStepComponent.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ export default function EditStepComponent(props: {
66
isOpen: boolean;
77
isDisabled: boolean;
88
index: number;
9+
headingRef?: React.Ref<HTMLHeadingElement>;
910
}) {
10-
const { title, description, Icon, children, isOpen, index, isDisabled } = props;
11+
const { title, description, Icon, children, isOpen, index, isDisabled, headingRef } = props;
1112
return (
1213
<details className="collapse collapse-arrow bg-base-100 border border-base-200" name={`my-accordion-det-${index}`} aria-disabled={isDisabled} open={isOpen}>
1314
<summary className="collapse-title font-semibold">
@@ -21,7 +22,7 @@ export default function EditStepComponent(props: {
2122
</span>
2223
</div>
2324
<div>
24-
<h2 className="card-title text-xl">
25+
<h2 ref={headingRef} tabIndex={-1} className="card-title text-xl">
2526
{title}
2627
</h2>
2728
<p className="text-sm text-base-content/60">

packages/webapp/src/components/layout/ThemeSwitcher.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,35 @@
1+
import { useTranslation } from "react-i18next";
2+
13
export default function ThemeSwitcherComponent(props: {
24
currentTheme: "light" | "dark";
35
handleChange: (value: string) => void;
6+
/** Optional contextual prefix for the accessible label (e.g. "Anteprima grafico"). */
7+
contextLabel?: string;
48
}) {
5-
const { currentTheme, handleChange } = props;
9+
const { currentTheme, handleChange, contextLabel } = props;
10+
const { t } = useTranslation("components", {
11+
keyPrefix: "components.themeSwitcher",
12+
});
13+
const stateLabel = t(currentTheme === "dark" ? "values.dark" : "values.light");
14+
const baseLabel = t("label");
15+
const ariaLabel = contextLabel
16+
? `${contextLabel}${baseLabel}: ${stateLabel}`
17+
: `${baseLabel}: ${stateLabel}`;
618
return (
719
<div className="flex p-2 rounded-lg ">
820
<label className="flex items-center gap-4 cursor-pointer">
9-
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" >
21+
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" >
1022
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
1123
</svg>
1224
<input
1325
type="checkbox"
26+
role="switch"
1427
className="toggle toggle-sm"
1528
checked={currentTheme === "dark"}
29+
aria-label={ariaLabel}
1630
onChange={(e) => handleChange(e.target.checked ? "dark" : "light")}
1731
/>
18-
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" >
32+
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" >
1933
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
2034
</svg>
2135
</label>

packages/webapp/src/components/load-data/TransformDataTable.tsx

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useMemo, useState } from "react";
1+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
22
import DataTable from "react-data-table-component";
33
import type { TableColumn } from "react-data-table-component";
44
import { useTranslation } from "react-i18next";
@@ -149,13 +149,36 @@ export default function TransformData({
149149
return columnOrder
150150
.filter((key) => visibleColumns.has(key))
151151
.map((key) => ({
152+
id: key,
152153
name: key,
153154
selector: (row: RowRecord) => row[key] as string,
154155
sortable: true,
155156
reorder: true,
156157
}));
157158
}, [columnOrder, visibleColumns]);
158159

160+
// a11y: react-data-table-component does not expose aria-sort on column
161+
// headers, so we sync it manually after each render. Sort icons rendered by
162+
// the library are also aria-hidden to avoid duplicate announcements.
163+
const tableRef = useRef<HTMLDivElement>(null);
164+
useEffect(() => {
165+
const root = tableRef.current;
166+
if (!root) return;
167+
const headers = root.querySelectorAll<HTMLElement>(
168+
'[role="columnheader"][data-column-id]',
169+
);
170+
headers.forEach((el) => {
171+
const id = el.getAttribute("data-column-id");
172+
const value =
173+
sortState && sortState.columnKey === id
174+
? sortState.direction === "asc"
175+
? "ascending"
176+
: "descending"
177+
: "none";
178+
el.setAttribute("aria-sort", value);
179+
});
180+
}, [sortState, columns]);
181+
159182
// Handle column reorder from DataTable drag-and-drop
160183
const handleColumnOrderChange = useCallback(
161184
(newCols: TableColumn<RowRecord>[]) => {
@@ -331,21 +354,24 @@ export default function TransformData({
331354
/>
332355
)}
333356

334-
<DataTable
335-
title={t(`table.title`)}
336-
columns={columns}
337-
data={objectData}
338-
theme={currentTheme}
339-
pagination
340-
dense
341-
highlightOnHover
342-
fixedHeader
343-
fixedHeaderScrollHeight="360px"
344-
responsive
345-
onColumnOrderChange={handleColumnOrderChange}
346-
onSort={handleSort}
347-
sortServer={false}
348-
/>
357+
<div ref={tableRef}>
358+
<DataTable
359+
title={t(`table.title`)}
360+
columns={columns}
361+
data={objectData}
362+
theme={currentTheme}
363+
pagination
364+
dense
365+
highlightOnHover
366+
fixedHeader
367+
fixedHeaderScrollHeight="360px"
368+
responsive
369+
onColumnOrderChange={handleColumnOrderChange}
370+
onSort={handleSort}
371+
sortServer={false}
372+
sortIcon={<span aria-hidden="true"></span>}
373+
/>
374+
</div>
349375

350376
{sortState && (
351377
<div className="mt-2 text-xs text-base-content/50">

packages/webapp/src/i18n/locales/en/components.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,13 @@
547547
"label": "Chart type"
548548
}
549549
}
550+
},
551+
"themeSwitcher": {
552+
"label": "Theme",
553+
"values": {
554+
"light": "Light",
555+
"dark": "Dark"
556+
}
550557
}
551558
}
552559
}

packages/webapp/src/i18n/locales/en/pages.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,9 @@
7676
"configuration": {
7777
"title": "Configure the chart",
7878
"description": "Choose the chart type and customize its appearance",
79-
"status": "Please load data and proceed to configuration step to see chart options"
79+
"status": "Load your data to get started. Once data is available you'll be able to pick the chart type and options.",
80+
"statusAwaitingSeries": "Data loaded. Confirm the series selection in the preview section to enable configuration.",
81+
"announcement": "Configuration available. Choose the chart type and customize its options."
8082
},
8183
"data": {
8284
"title": "Load your data",
@@ -90,7 +92,12 @@
9092
},
9193
"preview": {
9294
"loadDataMessage": "Load your data to display the data preview",
93-
"chartA11y": "Chart of type {{type}}: {{name}}. {{description}}"
95+
"chartA11y": "Chart of type {{type}}: {{name}}. {{description}}",
96+
"seriesSelector": {
97+
"title": "Series selection",
98+
"description": "Pick the columns to use as data series and confirm to proceed with the chart configuration.",
99+
"announcement": "Data loaded. Continue with the series selection in the preview section to enable configuration."
100+
}
94101
}
95102
}
96103
},

packages/webapp/src/i18n/locales/it/components.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,13 @@
547547
"label": "Tipo di grafico"
548548
}
549549
}
550+
},
551+
"themeSwitcher": {
552+
"label": "Tema",
553+
"values": {
554+
"light": "Chiaro",
555+
"dark": "Scuro"
556+
}
550557
}
551558
}
552559
}

packages/webapp/src/i18n/locales/it/pages.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,9 @@
7676
"configuration": {
7777
"title": "Configura il grafico",
7878
"description": "Scegli il tipo di grafico e personalizza l'aspetto",
79-
"status": "Carica i dati e procedi alla fase di configurazione per vedere le opzioni del grafico"
79+
"status": "Carica i dati per iniziare. Quando i dati saranno disponibili potrai scegliere il tipo di grafico e le opzioni.",
80+
"statusAwaitingSeries": "Dati caricati. Conferma la selezione delle serie nella sezione anteprima per abilitare la configurazione.",
81+
"announcement": "Configurazione disponibile. Scegli il tipo di grafico e personalizza le opzioni."
8082
},
8183
"data": {
8284
"title": "Carica i tuoi dati",
@@ -90,7 +92,12 @@
9092
},
9193
"preview": {
9294
"loadDataMessage": "Carica i tuoi dati per visualizzare l'anteprima",
93-
"chartA11y": "Grafico di tipo {{type}}: {{name}}. {{description}}"
95+
"chartA11y": "Grafico di tipo {{type}}: {{name}}. {{description}}",
96+
"seriesSelector": {
97+
"title": "Selezione serie",
98+
"description": "Scegli le colonne da utilizzare come serie dei dati e conferma per proseguire con la configurazione del grafico.",
99+
"announcement": "Dati caricati. Procedi con la selezione delle serie nella sezione anteprima per abilitare la configurazione."
100+
}
94101
}
95102
}
96103
},

packages/webapp/src/pages/private/EditChart.tsx

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { Helmet } from "react-helmet";
1111
import toast from "react-hot-toast";
1212
import { useTranslation } from "react-i18next";
1313
import { FaCog, FaDatabase, FaInfo } from "react-icons/fa";
14-
import { startTransition, useEffect, useState } from "react";
14+
import { startTransition, useEffect, useRef, useState } from "react";
1515
import { useNavigate, useParams } from "react-router-dom";
1616

1717
import { HOME_ROUTE, ROUTES } from "../../router.tsx";
@@ -69,6 +69,10 @@ function EditChartPage() {
6969

7070
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
7171
const [saveStatus, setSaveStatus] = useState<string>("");
72+
// Announces step transitions to assistive tech (WCAG 1.3.2 / 3.3.2)
73+
const [stepAnnouncement, setStepAnnouncement] = useState<string>("");
74+
const seriesSelectorRef = useRef<HTMLDivElement>(null);
75+
const configurationHeadingRef = useRef<HTMLHeadingElement>(null);
7276
useUnsavedChanges(hasUnsavedChanges, t(`unsavedChanges`));
7377

7478
// After the initial load completes, reset any dirty flag that child components
@@ -124,6 +128,21 @@ function EditChartPage() {
124128
}, [paramId]);
125129

126130
const haveData = data && data[0].length > 0 ? true : dataSource ? true : false;
131+
const isConfigStep = state.matches("config");
132+
133+
// Announce step transitions and move keyboard focus so AT users learn that
134+
// the next interactive section just became available (WCAG 1.3.2, 3.3.2).
135+
useEffect(() => {
136+
if (loading) return;
137+
if (isConfigStep) {
138+
setStepAnnouncement(t(`body.options.configuration.announcement`));
139+
// Defer until the configuration section is rendered/opened.
140+
requestAnimationFrame(() => configurationHeadingRef.current?.focus());
141+
} else if (currentData && !haveData) {
142+
setStepAnnouncement(t(`body.preview.seriesSelector.announcement`));
143+
requestAnimationFrame(() => seriesSelectorRef.current?.focus());
144+
}
145+
}, [isConfigStep, currentData, haveData, loading, t]);
127146

128147
function handleUpload(d: any) {
129148
setHasUnsavedChanges(true);
@@ -216,6 +235,10 @@ function EditChartPage() {
216235
<div role="status" aria-live="polite" aria-atomic="true" className="sr-only">
217236
{saveStatus}
218237
</div>
238+
{/* Live region for step-flow transitions (WCAG 1.3.2, 3.3.2) */}
239+
<div role="status" aria-live="polite" aria-atomic="true" className="sr-only">
240+
{stepAnnouncement}
241+
</div>
219242
<div className="w-full flex justify-between items-center gap-2 mb-4 bg-base-300 py-4 px-10 rounded-lg">
220243
<button
221244
type="button"
@@ -266,7 +289,9 @@ function EditChartPage() {
266289
<input
267290
id="chart_visibility"
268291
type="checkbox"
292+
role="switch"
269293
checked={chartPublish}
294+
aria-describedby="chart_visibility_state"
270295
onChange={() => {
271296
setHasUnsavedChanges(true);
272297
setChartPublish(!chartPublish);
@@ -279,7 +304,10 @@ function EditChartPage() {
279304
>
280305
{t(`body.options.setup.form.fields.visibility.label`)}
281306
</label>
282-
<span className="text-sm text-base-content font-bold">
307+
<span
308+
id="chart_visibility_state"
309+
className="text-sm text-base-content font-bold"
310+
>
283311
{t(
284312
`body.options.setup.form.fields.visibility.values.${chartPublish ? "public" : "private"}`,
285313
)}
@@ -333,9 +361,10 @@ function EditChartPage() {
333361
isOpen={currentStepIndex > 0 ? true : false}
334362
isDisabled={currentStepIndex === 0 ? true : false}
335363
index={2}
364+
headingRef={configurationHeadingRef}
336365
>
337366
<div>
338-
{state.matches("config") ? (
367+
{isConfigStep ? (
339368
<div className="card bg-base-100 shadow-sm border border-base-200">
340369
<div className="card-body">
341370
<SelectChart
@@ -359,8 +388,9 @@ function EditChartPage() {
359388
</div>
360389
) : (
361390
<div role="status">
362-
{" "}
363-
{t(`body.options.configuration.status`)}{" "}
391+
{currentData && !haveData
392+
? t(`body.options.configuration.statusAwaitingSeries`)
393+
: t(`body.options.configuration.status`)}
364394
</div>
365395
)}
366396
</div>
@@ -426,6 +456,7 @@ function EditChartPage() {
426456
handleChange={(value: ChartColorScheme) =>
427457
setPreviewScheme(value)
428458
}
459+
contextLabel={t(`header.preview.heading`)}
429460
/>
430461
<figure
431462
role="figure"
@@ -472,9 +503,22 @@ function EditChartPage() {
472503
{t(`body.preview.loadDataMessage`)}
473504
</p>
474505
{currentData && (
475-
<div className="card bg-base-100 shadow-sm border border-base-200">
506+
<div
507+
ref={seriesSelectorRef}
508+
tabIndex={-1}
509+
aria-labelledby="series-selector-heading"
510+
className="card bg-base-100 shadow-sm border border-base-200"
511+
>
476512
<div className="card-body">
477-
<h4>serie selector</h4>
513+
<h3
514+
id="series-selector-heading"
515+
className="text-lg font-semibold"
516+
>
517+
{t(`body.preview.seriesSelector.title`)}
518+
</h3>
519+
<p className="text-sm text-base-content/70">
520+
{t(`body.preview.seriesSelector.description`)}
521+
</p>
478522
<SeriesSelector
479523
initialData={currentData || data}
480524
setData={(d) => {

0 commit comments

Comments
 (0)