Skip to content

Commit df04492

Browse files
authored
Merge pull request #1145 from dnum-mi/develop
Develop
2 parents 53e95c5 + 0d5e0ff commit df04492

File tree

4 files changed

+321
-14
lines changed

4 files changed

+321
-14
lines changed

src/components/DsfrDataTable/DsfrDataTable.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,20 @@ Si vous avez des propositions, veuillez lancer une [**discussion**](https://gith
1616

1717
<VIcon name="vi-file-type-storybook" /> La story sur le tableau de données sur le storybook de [VueDsfr](https://storybook.vue-ds.fr/?path=/docs/composants-dsfrdatatable--docs)
1818

19-
## 📐 Structure
19+
## 📐 Structure
2020

2121
Le composant `DsfrDataTable` s'utilise pour afficher des données structurées sous forme de tableau. Il prend en charge le tri des colonnes, la pagination des lignes, et l'ajout de boutons ou d'icônes pour effectuer des actions spécifiques sur les données.
2222

23+
### Accessibilité
24+
25+
Le composant gère automatiquement l'attribut `aria-sort` sur les en-têtes de colonnes triables :
26+
- `aria-sort="ascending"` pour une colonne triée en ordre croissant
27+
- `aria-sort="descending"` pour une colonne triée en ordre décroissant
28+
- `aria-sort="none"` pour les colonnes triables non actuellement triées
29+
- Pas d'attribut `aria-sort` pour les colonnes non triables
30+
31+
Cela permet aux lecteurs d'écran d'annoncer correctement l'état de tri de chaque colonne aux utilisateurs.
32+
2333
## 🛠️Props
2434

2535
| Nom | Type | Défaut | Obligatoire | Description |
Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
import { fireEvent, render } from '@testing-library/vue'
2+
3+
import VIcon from '../VIcon/VIcon.vue'
4+
5+
import DsfrDataTable from './DsfrDataTable.vue'
6+
7+
describe('DsfrDataTable', () => {
8+
it('should render a simple data table with headers and rows', () => {
9+
// Given
10+
const title = 'Test Table'
11+
const headersRow = ['Name', 'Age', 'City']
12+
const rows = [
13+
['Alice', '25', 'Paris'],
14+
['Bob', '30', 'Lyon'],
15+
['Charlie', '35', 'Marseille'],
16+
]
17+
18+
// When
19+
const { getByText } = render(DsfrDataTable, {
20+
global: {
21+
components: {
22+
VIcon,
23+
},
24+
},
25+
props: {
26+
title,
27+
headersRow,
28+
rows,
29+
},
30+
})
31+
32+
// Then
33+
expect(getByText('Alice')).toBeTruthy()
34+
expect(getByText('Bob')).toBeTruthy()
35+
expect(getByText('Charlie')).toBeTruthy()
36+
})
37+
38+
it('should set aria-sort="none" on sortable columns that are not currently sorted', () => {
39+
// Given
40+
const title = 'Sortable Table'
41+
const headersRow = ['Name', 'Age', 'City']
42+
const rows = [
43+
['Alice', '25', 'Paris'],
44+
['Bob', '30', 'Lyon'],
45+
]
46+
47+
// When
48+
const { container } = render(DsfrDataTable, {
49+
global: {
50+
components: {
51+
VIcon,
52+
},
53+
},
54+
props: {
55+
title,
56+
headersRow,
57+
rows,
58+
sortableRows: true,
59+
},
60+
})
61+
62+
// Then
63+
const headers = container.querySelectorAll('th[scope="col"]')
64+
headers.forEach((header) => {
65+
expect(header.getAttribute('aria-sort')).toBe('none')
66+
})
67+
})
68+
69+
it('should set aria-sort="ascending" on the currently sorted column when sorting ascending', async () => {
70+
// Given
71+
const title = 'Sortable Table'
72+
const headersRow = ['Name', 'Age', 'City']
73+
const rows = [
74+
['Alice', '25', 'Paris'],
75+
['Bob', '30', 'Lyon'],
76+
]
77+
78+
// When
79+
const { getByText } = render(DsfrDataTable, {
80+
global: {
81+
components: {
82+
VIcon,
83+
},
84+
},
85+
props: {
86+
title,
87+
headersRow,
88+
rows,
89+
sortableRows: true,
90+
},
91+
})
92+
93+
const nameHeader = getByText('Name').closest('th')
94+
await fireEvent.click(nameHeader!)
95+
96+
// Then
97+
expect(nameHeader!.getAttribute('aria-sort')).toBe('ascending')
98+
99+
const ageHeader = getByText('Age').closest('th')
100+
const cityHeader = getByText('City').closest('th')
101+
expect(ageHeader!.getAttribute('aria-sort')).toBe('none')
102+
expect(cityHeader!.getAttribute('aria-sort')).toBe('none')
103+
})
104+
105+
it('should set aria-sort="descending" on the currently sorted column when sorting descending', async () => {
106+
// Given
107+
const title = 'Sortable Table'
108+
const headersRow = ['Name', 'Age', 'City']
109+
const rows = [
110+
['Alice', '25', 'Paris'],
111+
['Bob', '30', 'Lyon'],
112+
]
113+
114+
// When
115+
const { getByText } = render(DsfrDataTable, {
116+
global: {
117+
components: {
118+
VIcon,
119+
},
120+
},
121+
props: {
122+
title,
123+
headersRow,
124+
rows,
125+
sortableRows: true,
126+
},
127+
})
128+
129+
const nameHeader = getByText('Name').closest('th')
130+
131+
// Click once for ascending
132+
await fireEvent.click(nameHeader!)
133+
expect(nameHeader!.getAttribute('aria-sort')).toBe('ascending')
134+
135+
// Click twice for descending
136+
await fireEvent.click(nameHeader!)
137+
138+
// Then
139+
expect(nameHeader!.getAttribute('aria-sort')).toBe('descending')
140+
})
141+
142+
it('should not set aria-sort on non-sortable tables', () => {
143+
// Given
144+
const title = 'Non-sortable Table'
145+
const headersRow = ['Name', 'Age', 'City']
146+
const rows = [
147+
['Alice', '25', 'Paris'],
148+
['Bob', '30', 'Lyon'],
149+
]
150+
151+
// When
152+
const { container } = render(DsfrDataTable, {
153+
global: {
154+
components: {
155+
VIcon,
156+
},
157+
},
158+
props: {
159+
title,
160+
headersRow,
161+
rows,
162+
sortableRows: false,
163+
},
164+
})
165+
166+
// Then
167+
const headers = container.querySelectorAll('th[scope="col"]')
168+
headers.forEach((header) => {
169+
expect(header.getAttribute('aria-sort')).toBeNull()
170+
})
171+
})
172+
173+
it('should only set aria-sort on specifically sortable columns when sortableRows is an array', async () => {
174+
// Given
175+
const title = 'Partially Sortable Table'
176+
const headersRow = [
177+
{ key: 'name', label: 'Name' },
178+
{ key: 'age', label: 'Age' },
179+
{ key: 'city', label: 'City' },
180+
]
181+
const rows = [
182+
{ name: 'Alice', age: 25, city: 'Paris' },
183+
{ name: 'Bob', age: 30, city: 'Lyon' },
184+
]
185+
186+
// When
187+
const { getByText } = render(DsfrDataTable, {
188+
global: {
189+
components: {
190+
VIcon,
191+
},
192+
},
193+
props: {
194+
title,
195+
headersRow,
196+
rows,
197+
sortableRows: ['name', 'age'], // Only name and age are sortable
198+
},
199+
})
200+
201+
// Then
202+
const nameHeader = getByText('Name').closest('th')
203+
const ageHeader = getByText('Age').closest('th')
204+
const cityHeader = getByText('City').closest('th')
205+
206+
expect(nameHeader!.getAttribute('aria-sort')).toBe('none')
207+
expect(ageHeader!.getAttribute('aria-sort')).toBe('none')
208+
expect(cityHeader!.getAttribute('aria-sort')).toBeNull() // Not sortable
209+
210+
// Click on name to sort
211+
await fireEvent.click(nameHeader!)
212+
expect(nameHeader!.getAttribute('aria-sort')).toBe('ascending')
213+
expect(ageHeader!.getAttribute('aria-sort')).toBe('none')
214+
expect(cityHeader!.getAttribute('aria-sort')).toBeNull()
215+
})
216+
217+
it('should cycle through sort states: none -> ascending -> descending -> none', async () => {
218+
// Given
219+
const title = 'Sortable Table'
220+
const headersRow = ['Name']
221+
const rows = [['Alice'], ['Bob']]
222+
223+
// When
224+
const { getByText } = render(DsfrDataTable, {
225+
global: {
226+
components: {
227+
VIcon,
228+
},
229+
},
230+
props: {
231+
title,
232+
headersRow,
233+
rows,
234+
sortableRows: true,
235+
},
236+
})
237+
238+
const nameHeader = getByText('Name').closest('th')
239+
240+
// Initial state
241+
expect(nameHeader!.getAttribute('aria-sort')).toBe('none')
242+
243+
// First click - ascending
244+
await fireEvent.click(nameHeader!)
245+
expect(nameHeader!.getAttribute('aria-sort')).toBe('ascending')
246+
247+
// Second click - descending
248+
await fireEvent.click(nameHeader!)
249+
expect(nameHeader!.getAttribute('aria-sort')).toBe('descending')
250+
251+
// Third click - back to none (unsorted)
252+
await fireEvent.click(nameHeader!)
253+
expect(nameHeader!.getAttribute('aria-sort')).toBe('none')
254+
})
255+
256+
it('should handle empty rows array without errors', () => {
257+
// Given
258+
const title = 'Empty Table'
259+
const headersRow = ['Name', 'Age']
260+
const rows: string[][] = []
261+
262+
// When
263+
const { container } = render(DsfrDataTable, {
264+
global: {
265+
components: {
266+
VIcon,
267+
},
268+
},
269+
props: {
270+
title,
271+
headersRow,
272+
rows,
273+
sortableRows: true,
274+
},
275+
})
276+
277+
// Then
278+
const headers = container.querySelectorAll('th[scope="col"]')
279+
expect(headers.length).toBe(2)
280+
headers.forEach((header) => {
281+
expect(header.getAttribute('aria-sort')).toBe('none')
282+
})
283+
})
284+
})

src/components/DsfrDataTable/DsfrDataTable.stories.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import type { Meta, StoryObj } from '@storybook/vue3-vite'
22

3-
import { ref } from 'vue'
4-
53
import DsfrDataTable from './DsfrDataTable.vue'
64

75
const meta = {
@@ -76,9 +74,7 @@ export const Simple: Story = {
7674
components: { DsfrDataTable },
7775

7876
setup () {
79-
return {
80-
...args,
81-
}
77+
return args
8278
},
8379

8480
template: `
@@ -120,11 +116,7 @@ export const Complexe: Story = {
120116
components: { DsfrDataTable },
121117

122118
setup () {
123-
const selection = ref([])
124-
return {
125-
...args,
126-
selection,
127-
}
119+
return args
128120
},
129121

130122
template: `
@@ -136,6 +128,8 @@ export const Complexe: Story = {
136128
:rows="rows"
137129
selectable-rows
138130
sortable-rows
131+
v-model:sorted-by="sortedBy"
132+
v-model:sorted-desc="sortedDesc"
139133
row-key="id"
140134
>
141135
<template #header="{ key, label }">
@@ -154,6 +148,7 @@ export const Complexe: Story = {
154148
</template>
155149
</DsfrDataTable>
156150
<p>IDs sélectionnées : {{ selection }}</p>
151+
<p>Tri courant : {{ sortedBy ? (sortedBy + ' — ' + (sortedDesc ? 'descendant' : 'ascendant')) : 'aucun' }}</p>
157152
</div>
158153
`,
159154
}),
@@ -186,7 +181,9 @@ export const Complexe: Story = {
186181
[11, 'Henry Moore', '[email protected]'],
187182
[12, 'Iris Taylor', '[email protected]'],
188183
],
189-
rowKey: 'key',
184+
selection: [],
185+
sortedBy: 'id',
186+
sortedDesc: false,
190187
},
191188
}
192189

@@ -212,8 +209,8 @@ export const PlusComplexe: Story = {
212209
:rows-per-page
213210
:pagination-options
214211
:sorted
215-
:sorted-by
216-
:sorted-desc
212+
v-model:sorted-by="sortedBy"
213+
v-model:sorted-desc="sortedDesc"
217214
:sortable-rows
218215
>
219216
<template #header="{ label }">
@@ -230,6 +227,7 @@ export const PlusComplexe: Story = {
230227
</template>
231228
</DsfrDataTable>
232229
<p>IDs sélectionnées : {{ selection }}</p>
230+
<p>Tri courant : {{ sortedBy ? (sortedBy + ' — ' + (sortedDesc ? 'descendant' : 'ascendant')) : 'aucun' }}</p>
233231
</div>
234232
`,
235233
}),

0 commit comments

Comments
 (0)