Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,28 @@ Then open `http://localhost:8000`.
| onChange | `(page: number, pageSize: number, info?: { recommendPage?: number }) => void` | - | Triggered when page or page size changes. The `info.recommendPage` is only provided when pageSize changes, indicating the recommended page number based on the new page size. |
| onShowSizeChange | `(current: number, size: number) => void` | - | Triggered when page size changes. |

## Custom item accessibility

Default page and jump anchors expose button semantics, and previous/next actions
use native buttons. The outer `li` keeps its list-item semantics. Focus, labels,
current-page state, and activation belong to the inner control.

When `itemRender` or an icon supplies a DOM element, Pagination merges its
control semantics (role, labels, tab order, and activation) directly onto that
element instead of wrapping it. Native buttons and links with `href` keep their
own keyboard activation; other DOM elements receive button semantics and
Enter/Space handling. Native buttons preserve their disabled state and default
to `type="button"` unless a type is explicitly supplied.

Custom components, Fragments, and plain content retain delegated interaction on
the list item, since custom components need not forward DOM props. Custom
previous/next components continue to receive the `disabled` input. Authors are
responsible for accessible control semantics inside these custom renderers;
Pagination does not introduce an interactive wrapper around them.

Custom styles that target the outer item's focus should also account for focus
on its child, for example with `:focus-within` or a child `:focus-visible` rule.

## Development

```bash
Expand Down
4 changes: 4 additions & 0 deletions assets/index.less
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
}

&:focus,
&:focus-within,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7a777be - added a focus-within variant of the existing item-link selector for previous/next, so the inner control now shows the same focus styling as the numbered items.

&:hover {
border-color: @primary-color;
transition: all 0.3s;
Expand All @@ -101,11 +102,13 @@
}

&:focus,
&:focus-within,
&:hover {
border-color: #40a9ff;
}

&:focus a,
&:focus-within a,
&:hover a {
color: #40a9ff;
}
Expand Down Expand Up @@ -180,6 +183,7 @@
}

&:focus .@{pagination-prefix-cls}-item-link,
&:focus-within .@{pagination-prefix-cls}-item-link,
&:hover .@{pagination-prefix-cls}-item-link {
color: @primary-color;
border-color: @primary-color;
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"@types/node": "^26.0.1",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"axe-core": "^4.14.0",
"cross-env": "^10.1.0",
"dumi": "^2.4.38",
"eslint": "^10.6.0",
Expand Down
5 changes: 3 additions & 2 deletions src/Options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,9 @@ const Options: React.FC<OptionsProps> = (props) => {
setGoInputText('');
if (
e.relatedTarget &&
(e.relatedTarget.className.includes(`${rootPrefixCls}-item-link`) ||
e.relatedTarget.className.includes(`${rootPrefixCls}-item`))
e.relatedTarget.closest(
`.${rootPrefixCls}-item-link, .${rootPrefixCls}-item`,
)
) {
return;
}
Expand Down
17 changes: 7 additions & 10 deletions src/Pager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { clsx } from 'clsx';
import React from 'react';
import type { PaginationProps } from './interface';
import PaginationItem from './PaginationItem';

export interface PagerProps extends Pick<PaginationProps, 'itemRender'> {
rootPrefixCls: string;
Expand All @@ -11,6 +12,7 @@ export interface PagerProps extends Pick<PaginationProps, 'itemRender'> {
className?: string;
style?: React.CSSProperties;
showTitle: boolean;
disabled?: boolean;
onClick?: (page: number) => void;
onKeyPress?: (
e: React.KeyboardEvent<HTMLLIElement>,
Expand All @@ -28,6 +30,7 @@ const Pager: React.FC<PagerProps> = (props) => {
className,
style,
showTitle,
disabled,
onClick,
onKeyPress,
itemRender,
Expand All @@ -52,29 +55,23 @@ const Pager: React.FC<PagerProps> = (props) => {
onKeyPress(e, onClick, page);
};

const pager = itemRender(
page,
'page',
<a tabIndex={-1} aria-hidden="true" rel="nofollow">
{page}
</a>,
);
const pager = itemRender(page, 'page', <a rel="nofollow">{page}</a>);
const pagerLabel = `${pageLabel} ${page}`.trim();

return pager ? (
<li
<PaginationItem
title={showTitle ? String(page) : null}
className={cls}
style={style}
onClick={handleClick}
onKeyDown={handleKeyPress}
tabIndex={0}
role="button"
disabled={disabled || !page}
aria-label={pagerLabel}
aria-current={active ? 'page' : undefined}
>
{pager}
</li>
</PaginationItem>
) : null;
};

Expand Down
36 changes: 17 additions & 19 deletions src/Pagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import zhCN from './locale/zh_CN';
import Options from './Options';
import type { PagerProps } from './Pager';
import Pager from './Pager';
import PaginationItem from './PaginationItem';

const defaultItemRender: PaginationProps['itemRender'] = (_, __, element) =>
element;
Expand Down Expand Up @@ -127,7 +128,6 @@ const Pagination: React.FC<PaginationProps> = (props) => {
<button
type="button"
Comment thread
Copilot marked this conversation as resolved.
tabIndex={-1}
aria-hidden="true"
title={title}
className={`${prefixCls}-item-link`}
/>
Expand Down Expand Up @@ -295,9 +295,7 @@ const Pagination: React.FC<PaginationProps> = (props) => {
showTitle ? prevPageTitle : undefined,
),
);
return React.isValidElement<HTMLButtonElement>(prevButton)
? React.cloneElement(prevButton, { disabled: !hasPrev })
: prevButton;
return prevButton;
}

function renderNext(nextPage: number) {
Expand All @@ -311,9 +309,7 @@ const Pagination: React.FC<PaginationProps> = (props) => {
showTitle ? nextPageTitle : undefined,
),
);
return React.isValidElement<HTMLButtonElement>(nextButton)
? React.cloneElement(nextButton, { disabled: !hasNext })
: nextButton;
return nextButton;
}

function handleGoTO(event: any) {
Expand Down Expand Up @@ -355,6 +351,7 @@ const Pagination: React.FC<PaginationProps> = (props) => {
onClick: handleChange,
onKeyPress: runIfEnterOrSpace,
showTitle,
disabled,
itemRender,
pageLabel: locale.page,
page: -1,
Expand Down Expand Up @@ -437,6 +434,7 @@ const Pagination: React.FC<PaginationProps> = (props) => {
<Pager
{...pagerProps}
key="noPager"
disabled
page={1}
className={`${prefixCls}-item-disabled`}
/>,
Expand Down Expand Up @@ -473,7 +471,7 @@ const Pagination: React.FC<PaginationProps> = (props) => {

if (showPrevNextJumpers) {
jumpPrev = isReactRenderable(jumpPrevContent) ? (
<li
<PaginationItem
key="prev"
onClick={jumpPrevHandle}
tabIndex={0}
Expand All @@ -482,15 +480,15 @@ const Pagination: React.FC<PaginationProps> = (props) => {
[`${prefixCls}-jump-prev-custom-icon`]:
isReactRenderable(jumpPrevIcon),
})}
role="button"
disabled={disabled}
aria-label={prevItemTitle}
>
{jumpPrevContent}
</li>
</PaginationItem>
) : null;

jumpNext = isReactRenderable(jumpNextContent) ? (
<li
<PaginationItem
key="next"
onClick={jumpNextHandle}
tabIndex={0}
Expand All @@ -499,11 +497,11 @@ const Pagination: React.FC<PaginationProps> = (props) => {
[`${prefixCls}-jump-next-custom-icon`]:
isReactRenderable(jumpNextIcon),
})}
role="button"
disabled={disabled}
aria-label={nextItemTitle}
>
{jumpNextContent}
</li>
</PaginationItem>
) : null;
}

Expand Down Expand Up @@ -572,7 +570,7 @@ const Pagination: React.FC<PaginationProps> = (props) => {
if (isReactRenderable(prev)) {
const prevDisabled = !hasPrev || !allPages;
prev = (
<li
<PaginationItem
onClick={prevHandle}
tabIndex={prevDisabled ? null : 0}
onKeyDown={runIfEnterPrev}
Expand All @@ -581,11 +579,11 @@ const Pagination: React.FC<PaginationProps> = (props) => {
})}
style={styles?.item}
aria-disabled={prevDisabled}
role="button"
disabled={disabled || prevDisabled}
aria-label={locale.prev_page}
>
{prev}
</li>
</PaginationItem>
);
}

Expand All @@ -602,7 +600,7 @@ const Pagination: React.FC<PaginationProps> = (props) => {
}

next = (
<li
<PaginationItem
onClick={nextHandle}
tabIndex={nextTabIndex}
onKeyDown={runIfEnterNext}
Expand All @@ -611,11 +609,11 @@ const Pagination: React.FC<PaginationProps> = (props) => {
})}
style={styles?.item}
aria-disabled={nextDisabled}
role="button"
disabled={disabled || nextDisabled}
aria-label={locale.next_page}
>
{next}
</li>
</PaginationItem>
);
}

Expand Down
Loading
Loading