Skip to content

Commit 53e95c5

Browse files
authored
Merge pull request #1138 from dnum-mi/fix/restaure-title-pagination
Correction de l'accessibilité de DsfrPagination
2 parents 767a2fd + 02c7626 commit 53e95c5

File tree

7 files changed

+270
-23
lines changed

7 files changed

+270
-23
lines changed

.storybook/theme.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
.sbdocs-a, .sbdocs a, .docblock-argstable-body a {
2-
color: #000091 !important;
3-
outline-color: #000091 !important;
2+
color: #000091;
3+
outline-color: #000091;
44
--link-underline: 0;
55
--link-blank-content: "";
66
text-decoration: none;

src/components/DsfrPagination/DsfrPagination.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,24 @@ Le composant `DsfrPagination` est un système de pagination conforme aux bonnes
1010

1111
## 📐 Structure
1212

13-
Ce composant affiche des liens pour la première page, la précédente, les pages centrales, la suivante, et la dernière, avec des contrôles adaptatifs selon l'état de la pagination.
13+
Ce composant affiche des liens vers les pages avoisinant la page courante (mise en avant).
14+
Il affiche aussi la dernière page de la liste comme dernier élément de la pagination afin que l’usager connaisse le nombre total de pages.
15+
Il présente un accès rapide vers la première page, la précédente, la suivante, et la dernière, avec des contrôles adaptatifs selon l'état de la pagination.
16+
Des troncatures sont affichées (éventuellement masquées pour de petits écrans) pour matérialiser les pages ommises.
17+
Le composant propose aussi l'ajout d'un suffixe au texte du titre (`title` qui sert nottament à l'affichage d'une bulle d'aide) de la page courante pour la mettre en valeur.
1418

1519
## 🛠️ Props
1620

1721
| Nom | Type | Défaut | Description |
1822
|-------------------|-----------------------|---------------------|--------------------------------------------------------------------------------------------------|
19-
| `pages` | `Page[]` | **requis** | Liste des pages, où chaque page est un objet contenant des informations comme `href` et `label`. |
23+
| `pages` | `Page[]` | **requis** | Liste des pages, où chaque page est un objet contenant des informations comme `href`, `label` et `title`. |
2024
| `truncLimit` | `number` | `5` | Nombre maximum de pages affichées simultanément. |
2125
| `currentPage` | `number` | `0` | Index de la page actuellement sélectionnée (commence à `0`). |
2226
| `firstPageTitle` | `string` | `'Première page'` | Texte d'info-bulle pour le lien de la première page. |
2327
| `lastPageTitle` | `string` | `'Dernière page'` | Texte d'info-bulle pour le lien de la dernière page. |
2428
| `nextPageTitle` | `string` | `'Page suivante'` | Texte d'info-bulle pour le lien de la page suivante. |
2529
| `prevPageTitle` | `string` | `'Page précédente'` | Texte d'info-bulle pour le lien de la page précédente. |
30+
| `currentPageTitleSuffix` | `string` | `undefined` | Texte aditionnel d'info-bulle de la page courante. |
2631

2732
## 📡Événements
2833

src/components/DsfrPagination/DsfrPagination.spec.ts

Lines changed: 209 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
import { fireEvent, render } from '@testing-library/vue'
2+
import { describe, expect, it } from 'vitest'
23

34
import VIcon from '../VIcon/VIcon.vue'
45

56
import Pagination from './DsfrPagination.vue'
67

8+
function makePages (n: number) {
9+
return Array.from({ length: n }, (_, i) => ({
10+
label: String(i + 1),
11+
href: `#p${i + 1}`,
12+
title: `page ${i + 1}`,
13+
}))
14+
}
15+
716
describe('DsfrPagination', () => {
8-
it('should render a list of links to give quick access to several pages', async () => {
17+
it('should render a list of links to give quick access to several pages without title if undefined', async () => {
918
// Given
1019
const pages = [
1120
{ label: '1', href: '/#' },
@@ -17,7 +26,7 @@ describe('DsfrPagination', () => {
1726
const currentPage = 1
1827

1928
// When
20-
const { getByText, emitted } = render(Pagination, {
29+
const { getByText, getAllByRole, emitted } = render(Pagination, {
2130
global: {
2231
components: {
2332
VIcon,
@@ -31,9 +40,207 @@ describe('DsfrPagination', () => {
3140

3241
const thirdLink = getByText('3')
3342
await fireEvent.click(thirdLink)
43+
const pageLinks = getAllByRole('link', { name: /\d+/ })
3444

3545
// Then
3646
expect(emitted()['update:current-page']).toBeTruthy()
3747
expect(emitted()['update:current-page'][0][0]).toBe(2)
48+
pageLinks.forEach((link) => {
49+
expect(link.getAttribute('title')).toBe(null)
50+
})
51+
})
52+
53+
it('should render a list of links without title if equal to label', async () => {
54+
// Given
55+
const pages = [
56+
{ label: '1', title: '1', href: '/#' },
57+
{ label: '2', title: '2', href: '/#' },
58+
{ label: '3', title: '3', href: '/#' },
59+
{ label: '4', title: '4', href: '/#' },
60+
{ label: '5', title: '5', href: '/#' },
61+
]
62+
const currentPage = 1
63+
64+
// When
65+
const { getAllByRole } = render(Pagination, {
66+
global: {
67+
components: {
68+
VIcon,
69+
},
70+
},
71+
props: {
72+
pages,
73+
currentPage,
74+
currentPageTitleSuffix: ' - page courante',
75+
},
76+
})
77+
78+
const pageLinks = getAllByRole('link', { name: /\d+/ })
79+
80+
// Then
81+
pageLinks.forEach((link) => {
82+
if (link.ariaCurrent !== 'page') {
83+
expect(link.getAttribute('title')).toBe(null)
84+
} else {
85+
expect(link.getAttribute('title')).equal('2 - page courante')
86+
}
87+
})
88+
})
89+
90+
it('should render a list of links with appropriate title', async () => {
91+
// Given
92+
const pages = makePages(6)
93+
94+
// When
95+
const { getByRole } = render(Pagination, {
96+
global: { components: { VIcon } },
97+
props: {
98+
pages,
99+
currentPage: 2,
100+
truncLimit: 4,
101+
firstPageTitle: 'Première page',
102+
lastPageTitle: 'Dernière page',
103+
nextPageTitle: 'Page suivante',
104+
prevPageTitle: 'Page précédente',
105+
currentPageTitleSuffix: ' - page courante',
106+
ellipsisTitle: 'Pages intermédiaires non affichées',
107+
},
108+
})
109+
110+
const nextLink = getByRole('link', { name: 'Page suivante' })
111+
const prevLink = getByRole('link', { name: 'Page précédente' })
112+
const firstLink = getByRole('link', { name: 'Première page' })
113+
const lastLink = getByRole('link', { name: 'Dernière page' })
114+
const currentLink = getByRole('link', { current: 'page' })
115+
// Then
116+
expect(nextLink.getAttribute('title')).toBe('Page suivante')
117+
expect(prevLink.getAttribute('title')).toBe('Page précédente')
118+
expect(firstLink.getAttribute('title')).toBe('Première page')
119+
expect(lastLink.getAttribute('title')).toBe('Dernière page')
120+
expect(currentLink.getAttribute('title')).toBe('page 3 - page courante')
121+
})
122+
123+
it('renders navigation with default aria-label', () => {
124+
// Given
125+
const pages = makePages(3)
126+
127+
// When
128+
const { getByRole } = render(Pagination, {
129+
global: { components: { VIcon } },
130+
props: { pages },
131+
})
132+
133+
// Then
134+
expect(getByRole('navigation', { name: 'Pagination' })).toBeTruthy()
135+
})
136+
137+
it('emits update:current-page when using navigation controls and page links', async () => {
138+
// Given
139+
const pages = makePages(5)
140+
141+
// When
142+
const { getByRole, getByText, emitted } = render(Pagination, {
143+
global: { components: { VIcon } },
144+
props: { pages, currentPage: 1 },
145+
})
146+
147+
await fireEvent.click(getByRole('link', { name: 'Page suivante' })) // next -> 2
148+
await fireEvent.click(getByRole('link', { name: 'Page précédente' })) // prev -> 0
149+
await fireEvent.click(getByRole('link', { name: 'Première page' })) // first -> 0
150+
await fireEvent.click(getByRole('link', { name: 'Dernière page' })) // last -> pages.length - 1
151+
await fireEvent.click(getByText('3')) // specific page -> index 2
152+
153+
// Then
154+
const emits = emitted()['update:current-page']
155+
expect(emits).toBeTruthy()
156+
expect(emits![0][0]).toBe(2)
157+
expect(emits![1][0]).toBe(0)
158+
expect(emits![2][0]).toBe(0)
159+
expect(emits![3][0]).toBe(pages.length - 1)
160+
expect(emits![4][0]).toBe(2)
161+
})
162+
163+
it('applies truncation and shows ellipsis when pages.length > truncLimit', () => {
164+
// Given
165+
const pages = makePages(15)
166+
const currentPage = 5
167+
168+
// When
169+
const { getAllByRole, getAllByText } = render(Pagination, {
170+
global: { components: { VIcon } },
171+
props: { pages, currentPage },
172+
})
173+
174+
// Then
175+
// find links whose accessible name contains a digit (covers cases with ellipsis + digits)
176+
const pageLinks = getAllByRole('link', { name: /\d+/ })
177+
const ellipsis = getAllByText('...')
178+
179+
const firstEllipsis = ellipsis[0].textContent?.trim() ?? ''
180+
expect(firstEllipsis.includes('...')).toBe(true)
181+
182+
expect(pageLinks.length).toBeLessThan(pages.length)
183+
184+
const lastEllipsis = ellipsis[1].textContent?.trim() ?? ''
185+
expect(lastEllipsis.includes('...')).toBe(true)
186+
})
187+
188+
it('should have correct title attributes on page links', () => {
189+
// Given
190+
const pages = makePages(5)
191+
192+
// When
193+
const { getAllByRole } = render(Pagination, {
194+
global: { components: { VIcon } },
195+
props: { pages, currentPage: 0, currentPageTitleSuffix: ' - page courante' },
196+
})
197+
198+
// Then
199+
const pageLinks = getAllByRole('link', { name: /\d+/ })
200+
pageLinks.forEach((link, index) => {
201+
if (link.ariaCurrent === 'page') {
202+
expect(link.getAttribute('title')).toBe(`page ${index + 1} - page courante`)
203+
} else {
204+
expect(link.getAttribute('title')).toBe(`page ${index + 1}`)
205+
}
206+
})
207+
})
208+
209+
it('should have correct title attributes on page links when titles are equal to labels', () => {
210+
// Given
211+
const pages = makePages(5).map((page) => ({ ...page, title: page.label }))
212+
213+
// When
214+
const { getAllByRole } = render(Pagination, {
215+
global: { components: { VIcon } },
216+
props: { pages, currentPage: 0, currentPageTitleSuffix: ' - page courante' },
217+
})
218+
219+
// Then
220+
const pageLinks = getAllByRole('link', { name: /\d+/ })
221+
pageLinks.forEach((link) => {
222+
if (link.ariaCurrent !== 'page') {
223+
expect(link.getAttribute('title')).toBe(null)
224+
} else {
225+
expect(link.getAttribute('title')).equal('1 - page courante')
226+
}
227+
})
228+
})
229+
230+
it('should disable navigation controls and add disabled class at boundaries', () => {
231+
// Given
232+
const pages = makePages(5)
233+
234+
// When
235+
const { getByRole } = render(Pagination, {
236+
global: { components: { VIcon } },
237+
props: { pages, currentPage: 0 },
238+
})
239+
240+
// Then
241+
expect(getByRole('link', { name: 'Première page' }).getAttribute('aria-disabled')).toBe('true')
242+
expect(getByRole('link', { name: 'Première page' }).classList.contains('fr-pagination__link--disabled')).toBe(true)
243+
expect(getByRole('link', { name: 'Page précédente' }).getAttribute('aria-disabled')).toBe('true')
244+
expect(getByRole('link', { name: 'Page précédente' }).classList.contains('fr-pagination__link--disabled')).toBe(true)
38245
})
39246
})

src/components/DsfrPagination/DsfrPagination.stories.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ const meta = {
4343
description:
4444
'Permet de limiter le nombre de pages affichées dans la pagination (avec un maximum de 5 pages)',
4545
},
46+
currentPageTitleSuffix: {
47+
control: 'text',
48+
description:
49+
'Permet d’ajouter un suffixe au titre de la page courante pour indiquer à l’utilisateur qu’il se trouve sur cette page',
50+
},
4651
'onUpdate:currentPage': fn(),
4752
},
4853
} satisfies Meta<typeof DsfrPagination>
@@ -68,6 +73,8 @@ const render = (args: any) => ({
6873
<DsfrPagination
6974
:pages="args.pages"
7075
v-model:current-page="currentPage"
76+
:trunc-limit="args.truncLimit"
77+
:current-page-title-suffix="args.currentPageTitleSuffix"
7178
/>
7279
`,
7380
})
@@ -102,6 +109,7 @@ export const Pagination: Story = {
102109
title: 'Page 5',
103110
},
104111
],
112+
truncLimit: 5,
105113
currentPage: 0,
106114
},
107115
play: async ({ canvasElement, args }) => {
@@ -167,6 +175,7 @@ export const PaginationTronquee: Story = {
167175
title: 'Page 9',
168176
},
169177
],
170-
currentPage: 4,
178+
truncLimit: 3,
179+
currentPage: 3,
171180
},
172181
}
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
export type Page = { href?: string, label: string, title: string }
1+
export type Page = {
2+
href?: string
3+
label: string
4+
title: string
5+
}
26

37
export type DsfrPaginationProps = {
48
pages: Page[]
@@ -7,6 +11,7 @@ export type DsfrPaginationProps = {
711
lastPageTitle?: string
812
nextPageTitle?: string
913
prevPageTitle?: string
14+
currentPageTitleSuffix?: string
1015
truncLimit?: number
1116
ariaLabel?: string
1217
}

0 commit comments

Comments
 (0)