Skip to content

Commit 5541381

Browse files
authored
fix: improve pagination aria labels and semantics (#690)
1 parent 87c4ff3 commit 5541381

7 files changed

Lines changed: 1424 additions & 240 deletions

File tree

src/Pager.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { PaginationProps } from './interface';
66
export interface PagerProps extends Pick<PaginationProps, 'itemRender'> {
77
rootPrefixCls: string;
88
page: number;
9+
pageLabel?: string;
910
active?: boolean;
1011
className?: string;
1112
style?: React.CSSProperties;
@@ -22,6 +23,7 @@ const Pager: React.FC<PagerProps> = (props) => {
2223
const {
2324
rootPrefixCls,
2425
page,
26+
pageLabel,
2527
active,
2628
className,
2729
style,
@@ -50,7 +52,14 @@ const Pager: React.FC<PagerProps> = (props) => {
5052
onKeyPress(e, onClick, page);
5153
};
5254

53-
const pager = itemRender(page, 'page', <a rel="nofollow">{page}</a>);
55+
const pager = itemRender(
56+
page,
57+
'page',
58+
<a tabIndex={-1} aria-hidden="true" rel="nofollow">
59+
{page}
60+
</a>,
61+
);
62+
const pagerLabel = `${pageLabel} ${page}`.trim();
5463

5564
return pager ? (
5665
<li
@@ -60,6 +69,9 @@ const Pager: React.FC<PagerProps> = (props) => {
6069
onClick={handleClick}
6170
onKeyDown={handleKeyPress}
6271
tabIndex={0}
72+
role="button"
73+
aria-label={pagerLabel}
74+
aria-current={active ? 'page' : undefined}
6375
>
6476
{pager}
6577
</li>

src/Pagination.tsx

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
warning,
77
} from '@rc-component/util';
88
import React, { useEffect } from 'react';
9+
import isEnterOrSpaceKey from './isEnterOrSpaceKey';
910
import type { PaginationProps } from './interface';
1011
import zhCN from './locale/zh_CN';
1112
import Options from './Options';
@@ -116,12 +117,15 @@ const Pagination: React.FC<PaginationProps> = (props) => {
116117

117118
function getItemIcon(
118119
icon: React.ReactNode | React.ComponentType<PaginationProps>,
119-
label: string,
120+
_label: string,
121+
title?: string,
120122
) {
121123
let iconNode = icon || (
122124
<button
123125
type="button"
124-
aria-label={label}
126+
tabIndex={-1}
127+
aria-hidden="true"
128+
title={title}
125129
className={`${prefixCls}-item-link`}
126130
/>
127131
);
@@ -246,52 +250,59 @@ const Pagination: React.FC<PaginationProps> = (props) => {
246250
handleChange(jumpNextPage);
247251
}
248252

249-
function runIfEnter(
253+
function runIfEnterOrSpace(
250254
event: React.KeyboardEvent<HTMLLIElement>,
251255
callback: (...args: any[]) => void,
252256
...restParams: any[]
253257
) {
254-
if (
255-
event.key === 'Enter' ||
256-
event.charCode === KeyCode.ENTER ||
257-
event.keyCode === KeyCode.ENTER
258-
) {
258+
if (isEnterOrSpaceKey(event)) {
259+
event.preventDefault();
259260
callback(...restParams);
260261
}
261262
}
262263

263264
function runIfEnterPrev(event: React.KeyboardEvent<HTMLLIElement>) {
264-
runIfEnter(event, prevHandle);
265+
runIfEnterOrSpace(event, prevHandle);
265266
}
266267

267268
function runIfEnterNext(event: React.KeyboardEvent<HTMLLIElement>) {
268-
runIfEnter(event, nextHandle);
269+
runIfEnterOrSpace(event, nextHandle);
269270
}
270271

271272
function runIfEnterJumpPrev(event: React.KeyboardEvent<HTMLLIElement>) {
272-
runIfEnter(event, jumpPrevHandle);
273+
runIfEnterOrSpace(event, jumpPrevHandle);
273274
}
274275

275276
function runIfEnterJumpNext(event: React.KeyboardEvent<HTMLLIElement>) {
276-
runIfEnter(event, jumpNextHandle);
277+
runIfEnterOrSpace(event, jumpNextHandle);
277278
}
278279

279280
function renderPrev(prevPage: number) {
281+
const prevPageTitle = locale.prev_page || 'prev page';
280282
const prevButton = itemRender(
281283
prevPage,
282284
'prev',
283-
getItemIcon(prevIcon, 'prev page'),
285+
getItemIcon(
286+
prevIcon,
287+
prevPageTitle,
288+
showTitle ? prevPageTitle : undefined,
289+
),
284290
);
285291
return React.isValidElement<HTMLButtonElement>(prevButton)
286292
? React.cloneElement(prevButton, { disabled: !hasPrev })
287293
: prevButton;
288294
}
289295

290296
function renderNext(nextPage: number) {
297+
const nextPageTitle = locale.next_page || 'next page';
291298
const nextButton = itemRender(
292299
nextPage,
293300
'next',
294-
getItemIcon(nextIcon, 'next page'),
301+
getItemIcon(
302+
nextIcon,
303+
nextPageTitle,
304+
showTitle ? nextPageTitle : undefined,
305+
),
295306
);
296307
return React.isValidElement<HTMLButtonElement>(nextButton)
297308
? React.cloneElement(nextButton, { disabled: !hasNext })
@@ -335,9 +346,10 @@ const Pagination: React.FC<PaginationProps> = (props) => {
335346
const pagerProps: PagerProps = {
336347
rootPrefixCls: prefixCls,
337348
onClick: handleChange,
338-
onKeyPress: runIfEnter,
349+
onKeyPress: runIfEnterOrSpace,
339350
showTitle,
340351
itemRender,
352+
pageLabel: locale.page,
341353
page: -1,
342354
className: paginationClassNames?.item,
343355
style: styles?.item,
@@ -436,40 +448,50 @@ const Pagination: React.FC<PaginationProps> = (props) => {
436448
const jumpPrevContent = itemRender(
437449
jumpPrevPage,
438450
'jump-prev',
439-
getItemIcon(jumpPrevIcon, 'prev page'),
451+
getItemIcon(
452+
jumpPrevIcon,
453+
prevItemTitle,
454+
showTitle ? prevItemTitle : undefined,
455+
),
440456
);
441457
const jumpNextContent = itemRender(
442458
jumpNextPage,
443459
'jump-next',
444-
getItemIcon(jumpNextIcon, 'next page'),
460+
getItemIcon(
461+
jumpNextIcon,
462+
nextItemTitle,
463+
showTitle ? nextItemTitle : undefined,
464+
),
445465
);
446466

447467
if (showPrevNextJumpers) {
448468
jumpPrev = jumpPrevContent ? (
449469
<li
450-
title={showTitle ? prevItemTitle : null}
451470
key="prev"
452471
onClick={jumpPrevHandle}
453472
tabIndex={0}
454473
onKeyDown={runIfEnterJumpPrev}
455474
className={clsx(`${prefixCls}-jump-prev`, {
456475
[`${prefixCls}-jump-prev-custom-icon`]: !!jumpPrevIcon,
457476
})}
477+
role="button"
478+
aria-label={prevItemTitle}
458479
>
459480
{jumpPrevContent}
460481
</li>
461482
) : null;
462483

463484
jumpNext = jumpNextContent ? (
464485
<li
465-
title={showTitle ? nextItemTitle : null}
466486
key="next"
467487
onClick={jumpNextHandle}
468488
tabIndex={0}
469489
onKeyDown={runIfEnterJumpNext}
470490
className={clsx(`${prefixCls}-jump-next`, {
471491
[`${prefixCls}-jump-next-custom-icon`]: !!jumpNextIcon,
472492
})}
493+
role="button"
494+
aria-label={nextItemTitle}
473495
>
474496
{jumpNextContent}
475497
</li>
@@ -542,7 +564,6 @@ const Pagination: React.FC<PaginationProps> = (props) => {
542564
const prevDisabled = !hasPrev || !allPages;
543565
prev = (
544566
<li
545-
title={showTitle ? locale.prev_page : null}
546567
onClick={prevHandle}
547568
tabIndex={prevDisabled ? null : 0}
548569
onKeyDown={runIfEnterPrev}
@@ -551,6 +572,8 @@ const Pagination: React.FC<PaginationProps> = (props) => {
551572
})}
552573
style={styles?.item}
553574
aria-disabled={prevDisabled}
575+
role="button"
576+
aria-label={locale.prev_page}
554577
>
555578
{prev}
556579
</li>
@@ -571,7 +594,6 @@ const Pagination: React.FC<PaginationProps> = (props) => {
571594

572595
next = (
573596
<li
574-
title={showTitle ? locale.next_page : null}
575597
onClick={nextHandle}
576598
tabIndex={nextTabIndex}
577599
onKeyDown={runIfEnterNext}
@@ -580,6 +602,8 @@ const Pagination: React.FC<PaginationProps> = (props) => {
580602
})}
581603
style={styles?.item}
582604
aria-disabled={nextDisabled}
605+
role="button"
606+
aria-label={locale.next_page}
583607
>
584608
{next}
585609
</li>

src/isEnterOrSpaceKey.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { KeyCode } from '@rc-component/util';
2+
3+
interface KeyboardEventLike {
4+
key?: string;
5+
charCode?: number;
6+
keyCode?: number;
7+
}
8+
9+
export default function isEnterOrSpaceKey(event: KeyboardEventLike) {
10+
return (
11+
event.key === 'Enter' ||
12+
event.key === ' ' ||
13+
event.key === 'Spacebar' ||
14+
event.charCode === KeyCode.ENTER ||
15+
event.keyCode === KeyCode.ENTER
16+
);
17+
}

0 commit comments

Comments
 (0)