Skip to content

Commit 84704f0

Browse files
committed
feat(a11y): add descriptive captions to declaration tables
Split single declarations table into two separate tables (current and previous) with sr-only captions describing the table content and column structure, as specified in Figma design notes.
1 parent ce4e246 commit 84704f0

2 files changed

Lines changed: 128 additions & 98 deletions

File tree

packages/app/src/modules/my-space/DeclarationsSection.tsx

Lines changed: 108 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use client";
22

3+
import type { ReactNode } from "react";
34
import { useState } from "react";
45

56
import { getCurrentYear } from "~/modules/domain";
@@ -56,27 +57,28 @@ export function DeclarationsSection({
5657
);
5758
const previousDeclarations = declarations.filter((d) => d.year < currentYear);
5859

59-
const allRows = [
60-
...currentYearDeclarations.map((d) => ({
61-
kind: "row" as const,
62-
declaration: d,
63-
})),
64-
...(previousDeclarations.length > 0
65-
? [{ kind: "separator" as const, declaration: null }]
66-
: []),
67-
...previousDeclarations.map((d) => ({
68-
kind: "row" as const,
69-
declaration: d,
70-
})),
71-
];
72-
7360
const [pageSize, setPageSize] = useState(PAGE_SIZE_OPTIONS[0] ?? 10);
7461
const [currentPage, setCurrentPage] = useState(1);
7562

76-
const totalPages = Math.max(1, Math.ceil(allRows.length / pageSize));
63+
const totalRows =
64+
currentYearDeclarations.length + previousDeclarations.length;
65+
const totalPages = Math.max(1, Math.ceil(totalRows / pageSize));
7766
const safePage = Math.min(currentPage, totalPages);
7867
const startIndex = (safePage - 1) * pageSize;
79-
const visibleRows = allRows.slice(startIndex, startIndex + pageSize);
68+
const endIndex = startIndex + pageSize;
69+
70+
const visibleCurrentDeclarations = currentYearDeclarations.slice(
71+
startIndex,
72+
endIndex,
73+
);
74+
const remainingSlots = endIndex - currentYearDeclarations.length;
75+
const visiblePreviousDeclarations =
76+
remainingSlots > 0
77+
? previousDeclarations.slice(
78+
Math.max(0, startIndex - currentYearDeclarations.length),
79+
remainingSlots,
80+
)
81+
: [];
8082

8183
function handlePageSizeChange(newSize: number) {
8284
setPageSize(newSize);
@@ -101,47 +103,46 @@ export function DeclarationsSection({
101103
</div>
102104
)}
103105
</div>
106+
{visibleCurrentDeclarations.length > 0 && (
107+
<DeclarationsTable
108+
caption={
109+
<>
110+
Ce tableau présente la liste des démarches en cours.
111+
<br />
112+
Chaque ligne correspond à une démarche, avec les informations
113+
suivantes : le type de démarche, l'année concernée, l'étape
114+
actuelle, la date d'échéance, l'état d'avancement et les
115+
ressources disponibles.
116+
</>
117+
}
118+
declarations={visibleCurrentDeclarations}
119+
hasCse={hasCse}
120+
siren={siren}
121+
userPhone={userPhone}
122+
/>
123+
)}
124+
{visiblePreviousDeclarations.length > 0 && (
125+
<>
126+
<h2 className="fr-mt-6w fr-mb-3w">Années précédentes</h2>
127+
<DeclarationsTable
128+
caption={
129+
<>
130+
Ce tableau présente l'historique des démarches.
131+
<br />
132+
Chaque ligne correspond à une démarche passée, avec les
133+
informations suivantes : le type de démarche, l'année concernée,
134+
les différentes étapes, les échéances, l'état final et les
135+
ressources associées.
136+
</>
137+
}
138+
declarations={visiblePreviousDeclarations}
139+
hasCse={hasCse}
140+
siren={siren}
141+
userPhone={userPhone}
142+
/>
143+
</>
144+
)}
104145
<div className="fr-table">
105-
<div className="fr-table__wrapper">
106-
<div className="fr-table__container">
107-
<div className="fr-table__content">
108-
<table>
109-
<caption className="fr-sr-only">
110-
Liste des déclarations de l'entreprise
111-
</caption>
112-
<thead>
113-
<tr>
114-
<th scope="col">Déclaration</th>
115-
<th scope="col">Année</th>
116-
<th scope="col">Étape</th>
117-
<th scope="col">Statut</th>
118-
<th scope="col">Échéance</th>
119-
<th scope="col">Mise à jour</th>
120-
</tr>
121-
</thead>
122-
<tbody>
123-
{visibleRows.map((row, i) =>
124-
row.kind === "separator" ? (
125-
<tr key="separator">
126-
<td className="fr-text--bold" colSpan={6}>
127-
Années précédentes
128-
</td>
129-
</tr>
130-
) : row.declaration ? (
131-
<DeclarationRow
132-
declaration={row.declaration}
133-
hasCse={hasCse}
134-
key={`${row.declaration.type}-${row.declaration.year}-${i}`}
135-
siren={siren}
136-
userPhone={userPhone}
137-
/>
138-
) : null,
139-
)}
140-
</tbody>
141-
</table>
142-
</div>
143-
</div>
144-
</div>
145146
<div className="fr-table__footer--start">
146147
<div className="fr-select-group">
147148
<label className="fr-sr-only fr-label" htmlFor="table-page-size">
@@ -173,39 +174,66 @@ export function DeclarationsSection({
173174
);
174175
}
175176

176-
type DeclarationRowProps = {
177-
declaration: DeclarationItem;
177+
type DeclarationsTableProps = {
178+
declarations: DeclarationItem[];
179+
caption: ReactNode;
178180
siren: string;
179181
userPhone: string | null;
180182
hasCse: boolean | null;
181183
};
182184

183-
function DeclarationRow({
184-
declaration,
185+
function DeclarationsTable({
186+
declarations,
187+
caption,
185188
siren,
186189
userPhone,
187190
hasCse,
188-
}: DeclarationRowProps) {
191+
}: DeclarationsTableProps) {
189192
return (
190-
<tr>
191-
<td>
192-
<DeclarationLink
193-
hasCse={hasCse}
194-
siren={siren}
195-
type={declaration.type}
196-
userPhone={userPhone}
197-
>
198-
{TYPE_LABELS[declaration.type]}
199-
</DeclarationLink>
200-
</td>
201-
<td>{declaration.year}</td>
202-
<td>{getDeclarationStepLabel(declaration.currentStep)}</td>
203-
<td>
204-
<StatusBadge status={declaration.status} />
205-
</td>
206-
<td>{getDeadline(declaration)}</td>
207-
<td>{formatDate(declaration.updatedAt)}</td>
208-
</tr>
193+
<div className="fr-table">
194+
<div className="fr-table__wrapper">
195+
<div className="fr-table__container">
196+
<div className="fr-table__content">
197+
<table>
198+
<caption className="fr-sr-only">{caption}</caption>
199+
<thead>
200+
<tr>
201+
<th scope="col">Déclaration</th>
202+
<th scope="col">Année</th>
203+
<th scope="col">Étape</th>
204+
<th scope="col">Statut</th>
205+
<th scope="col">Échéance</th>
206+
<th scope="col">Mise à jour</th>
207+
</tr>
208+
</thead>
209+
<tbody>
210+
{declarations.map((declaration) => (
211+
<tr key={`${declaration.type}-${declaration.year}`}>
212+
<td>
213+
<DeclarationLink
214+
hasCse={hasCse}
215+
siren={siren}
216+
type={declaration.type}
217+
userPhone={userPhone}
218+
>
219+
{TYPE_LABELS[declaration.type]}
220+
</DeclarationLink>
221+
</td>
222+
<td>{declaration.year}</td>
223+
<td>{getDeclarationStepLabel(declaration.currentStep)}</td>
224+
<td>
225+
<StatusBadge status={declaration.status} />
226+
</td>
227+
<td>{getDeadline(declaration)}</td>
228+
<td>{formatDate(declaration.updatedAt)}</td>
229+
</tr>
230+
))}
231+
</tbody>
232+
</table>
233+
</div>
234+
</div>
235+
</div>
236+
</div>
209237
);
210238
}
211239

packages/app/src/modules/my-space/__tests__/DeclarationsSection.test.tsx

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -59,23 +59,23 @@ describe("DeclarationsSection", () => {
5959
it("renders the table column headers including Échéance and Mise à jour", () => {
6060
renderSection();
6161
expect(
62-
screen.getByRole("columnheader", { name: "Déclaration" }),
63-
).toBeInTheDocument();
64-
expect(
65-
screen.getByRole("columnheader", { name: "Année" }),
66-
).toBeInTheDocument();
67-
expect(
68-
screen.getByRole("columnheader", { name: "Étape" }),
69-
).toBeInTheDocument();
62+
screen.getAllByRole("columnheader", { name: "Déclaration" }),
63+
).toHaveLength(2);
64+
expect(screen.getAllByRole("columnheader", { name: "Année" })).toHaveLength(
65+
2,
66+
);
67+
expect(screen.getAllByRole("columnheader", { name: "Étape" })).toHaveLength(
68+
2,
69+
);
7070
expect(
71-
screen.getByRole("columnheader", { name: "Statut" }),
72-
).toBeInTheDocument();
71+
screen.getAllByRole("columnheader", { name: "Statut" }),
72+
).toHaveLength(2);
7373
expect(
74-
screen.getByRole("columnheader", { name: "Échéance" }),
75-
).toBeInTheDocument();
74+
screen.getAllByRole("columnheader", { name: "Échéance" }),
75+
).toHaveLength(2);
7676
expect(
77-
screen.getByRole("columnheader", { name: "Mise à jour" }),
78-
).toBeInTheDocument();
77+
screen.getAllByRole("columnheader", { name: "Mise à jour" }),
78+
).toHaveLength(2);
7979
});
8080

8181
it("renders declaration rows with year, status badge, and step label", () => {
@@ -97,9 +97,11 @@ describe("DeclarationsSection", () => {
9797
expect(screen.getByText("15/03/2025")).toBeInTheDocument();
9898
});
9999

100-
it("renders 'Années précédentes' separator when there are past declarations", () => {
100+
it("renders 'Années précédentes' heading when there are past declarations", () => {
101101
renderSection();
102-
expect(screen.getByText("Années précédentes")).toBeInTheDocument();
102+
expect(
103+
screen.getByRole("heading", { level: 2, name: "Années précédentes" }),
104+
).toBeInTheDocument();
103105
});
104106

105107
it("renders 'Rémunération' and 'Représentation' links", () => {
@@ -144,8 +146,8 @@ describe("DeclarationsSection", () => {
144146
const pagination = screen.getByRole("navigation", { name: "Pagination" });
145147
expect(pagination).toBeInTheDocument();
146148

147-
// Page 1: 1 header + 10 visible entries (1 current + 1 separator + 8 previous)
148-
expect(screen.getAllByRole("row")).toHaveLength(1 + 10);
149+
// 2 headers (current + previous tables) + 10 data rows (1 current + 9 previous)
150+
expect(screen.getAllByRole("row")).toHaveLength(2 + 10);
149151

150152
// Navigate to page 2
151153
fireEvent.click(screen.getByTitle("Page 2"));

0 commit comments

Comments
 (0)