Skip to content

Commit 2d6b78c

Browse files
ergot-rpergotsejuliajforestidougfabris
authored
feat(fuselage): Add accessibility improvements to Pagination (#1685)
Co-authored-by: erik <erik.gothe@fotbollsdata.se> Co-authored-by: juliajforesti <juliajforesti@gmail.com> Co-authored-by: Douglas Fabris <devfabris@gmail.com>
1 parent 479a0ec commit 2d6b78c

6 files changed

Lines changed: 285 additions & 53 deletions

File tree

.changeset/legal-parrots-dig.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@rocket.chat/fuselage': minor
3+
---
4+
5+
feat(fuselage): Add accessibility improvements to `Pagination`
Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,33 @@
1+
import { composeStories } from '@storybook/react-webpack5';
2+
import { axe } from 'jest-axe';
3+
14
import { render } from '../../testing';
25

3-
import Pagination from './Pagination';
6+
import * as stories from './Pagination.stories';
7+
8+
const { Default } = composeStories(stories);
9+
10+
jest.mock('react', () => ({
11+
...jest.requireActual('react'),
12+
useId: () => 'test-id',
13+
}));
14+
15+
it('renders without crashing', () => {
16+
const tree = render(<Default />);
17+
expect(tree.baseElement).toMatchSnapshot();
18+
});
19+
20+
it('%s should have no a11y violations', async () => {
21+
const { container } = render(<Default />);
22+
23+
const results = await axe(container);
24+
expect(results).toHaveNoViolations();
25+
});
26+
27+
it('should override with custom aria-label', () => {
28+
const { getByLabelText } = render(
29+
<Default aria-label='Custom Pagination Navigation' />,
30+
);
431

5-
describe('[Pagination Component]', () => {
6-
it('renders without crashing', () => {
7-
render(<Pagination count={0} />);
8-
});
32+
expect(getByLabelText('Custom Pagination Navigation')).toBeInTheDocument();
933
});

packages/fuselage/src/components/Pagination/Pagination.stories.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ export default {
88
component: Pagination,
99
} satisfies Meta<typeof Pagination>;
1010

11-
export const Default: StoryFn<typeof Pagination> = () => (
11+
export const Default: StoryFn<typeof Pagination> = (args) => (
1212
<Pagination
13+
{...args}
1314
count={500}
1415
onSetItemsPerPage={action('setItemsPerPage')}
1516
onSetCurrent={action('setCurrent')}

packages/fuselage/src/components/Pagination/Pagination.styles.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080
.rcx-pagination__label {
8181
@include typography.use-font-scale(c1);
8282

83-
color: colors.font(secondary-info);
83+
color: colors.font(annotation);
8484
}
8585

8686
.rcx-pagination__list {

packages/fuselage/src/components/Pagination/Pagination.tsx

Lines changed: 66 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
import type { Dispatch, SetStateAction } from 'react';
2-
import { useMemo } from 'react';
1+
import type { Dispatch, HTMLAttributes, SetStateAction } from 'react';
2+
import { useId, useMemo } from 'react';
33

4-
import { Box, type BoxProps } from '../Box';
54
import { Chevron } from '../Chevron';
65

76
type ItemsPerPage = 25 | 50 | 100;
87

9-
export type PaginationProps = BoxProps & {
8+
export type PaginationProps = {
109
count: number;
1110
current?: number;
1211
divider?: boolean;
@@ -23,7 +22,7 @@ export type PaginationProps = BoxProps & {
2322
}) => string;
2423
onSetCurrent?: Dispatch<SetStateAction<number>>;
2524
onSetItemsPerPage?: Dispatch<SetStateAction<ItemsPerPage>>;
26-
};
25+
} & HTMLAttributes<HTMLElement>;
2726

2827
const defaultItemsPerPageLabel = () => 'Items per page:';
2928

@@ -41,7 +40,7 @@ const defaultShowingResultsLabel = ({
4140
count,
4241
)} of ${count}`;
4342

44-
const itemsPerPageOptions = [25, 50, 100] as ItemsPerPage[];
43+
const itemsPerPageOptions = [25, 50, 100] as const;
4544

4645
const Pagination = ({
4746
count,
@@ -54,6 +53,9 @@ const Pagination = ({
5453
divider,
5554
...props
5655
}: PaginationProps) => {
56+
const paginationResultLabelId = useId();
57+
const itemsPerPageLabelId = useId();
58+
5759
const hasItemsPerPageSelection = itemsPerPageOptions.length > 1;
5860
const currentPage = Math.floor(current / itemsPerPage);
5961
const pages = Math.ceil(count / itemsPerPage);
@@ -97,73 +99,91 @@ const Pagination = ({
9799
};
98100

99101
return (
100-
<Box is='nav' rcx-pagination rcx-pagination--divider={divider} {...props}>
102+
<nav
103+
className={[
104+
'rcx-box rcx-box--full rcx-pagination',
105+
divider && 'rcx-pagination--divider',
106+
]
107+
.filter(Boolean)
108+
.join(' ')}
109+
aria-label='Pagination Navigation'
110+
{...props}
111+
>
101112
{hasItemsPerPageSelection && (
102-
<Box rcx-pagination__left>
103-
<Box is='span' rcx-pagination__label>
113+
<div className='rcx-pagination__left'>
114+
<span className='rcx-pagination__label' id={itemsPerPageLabelId}>
104115
{itemsPerPageLabel(renderingContext)}
105-
</Box>
106-
<Box is='ol' rcx-pagination__list>
116+
</span>
117+
<ol
118+
className='rcx-box rcx-box--full rcx-pagination__list'
119+
aria-labelledby={itemsPerPageLabelId}
120+
>
107121
{itemsPerPageOptions.map((itemsPerPageOption) => (
108-
<Box key={itemsPerPageOption} is='li' rcx-pagination__list-item>
109-
<Box
110-
is='button'
111-
rcx-pagination__link
122+
<li
123+
key={itemsPerPageOption}
124+
className='rcx-pagination__list-item'
125+
>
126+
<button
127+
className='rcx-box rcx-box--full rcx-pagination__link'
112128
tabIndex={itemsPerPage === itemsPerPageOption ? -1 : 0}
113129
disabled={itemsPerPage === itemsPerPageOption}
130+
aria-label={`Show ${itemsPerPageOption} items per page`}
114131
onClick={handleSetItemsPerPageLinkClick(itemsPerPageOption)}
115132
>
116133
{itemsPerPageOption}
117-
</Box>
118-
</Box>
134+
</button>
135+
</li>
119136
))}
120-
</Box>
121-
</Box>
137+
</ol>
138+
</div>
122139
)}
123-
<Box rcx-pagination__right>
124-
<Box is='span' rcx-pagination__label>
140+
<div className='rcx-pagination__right'>
141+
<span className='rcx-pagination__label' id={paginationResultLabelId}>
125142
{showingResultsLabel(renderingContext)}
126-
</Box>
127-
<Box is='ol' rcx-pagination__list>
128-
<Box is='li' rcx-pagination__list-item>
129-
<Box
130-
is='button'
131-
rcx-pagination__back
143+
</span>
144+
<ol
145+
className='rcx-box rcx-box--full rcx-pagination__list'
146+
aria-labelledby={paginationResultLabelId}
147+
>
148+
<li className='rcx-pagination__list-item'>
149+
<button
150+
className='rcx-box rcx-box--full rcx-pagination__back'
132151
disabled={currentPage === 0}
152+
aria-label='Previous page'
133153
onClick={handleSetPageLinkClick(currentPage - 1)}
134154
>
135155
<Chevron left size='x16' />
136-
</Box>
137-
</Box>
156+
</button>
157+
</li>
138158
{displayedPages.map((page, i) => (
139-
<Box key={i} is='li' rcx-pagination__list-item>
159+
<li key={i} className='rcx-pagination__list-item'>
140160
{page === '⋯' ? (
141161
'⋯'
142162
) : (
143-
<Box
144-
is='button'
145-
rcx-pagination__link
163+
<button
164+
className='rcx-box rcx-box--full rcx-pagination__link'
146165
disabled={currentPage === page}
147-
onClick={handleSetPageLinkClick(page as number)}
166+
aria-label={`Page ${Number(page) + 1}`}
167+
onClick={handleSetPageLinkClick(Number(page))}
148168
>
149-
{(page as number) + 1}
150-
</Box>
169+
{Number(page) + 1}
170+
</button>
151171
)}
152-
</Box>
172+
</li>
153173
))}
154-
<Box is='li' rcx-pagination__list-item>
155-
<Box
156-
is='button'
157-
rcx-pagination__forward
174+
<li className='rcx-pagination__list-item'>
175+
<button
176+
className='rcx-box rcx-box--full rcx-pagination__forward'
158177
disabled={currentPage === pages - 1}
178+
aria-label='Next page'
159179
onClick={handleSetPageLinkClick(currentPage + 1)}
160180
>
161181
<Chevron right size='x16' />
162-
</Box>
163-
</Box>
164-
</Box>
165-
</Box>
166-
</Box>
182+
</button>
183+
</li>
184+
</ol>
185+
</div>
186+
</nav>
167187
);
168188
};
169189

0 commit comments

Comments
 (0)