From 7e64c834779292a28b741bca8beff55e1996ea57 Mon Sep 17 00:00:00 2001 From: Zou Quan <2990918167@qq.com> Date: Mon, 1 Jun 2026 11:53:59 +0000 Subject: [PATCH 01/11] fix: improve pagination a11y semantics without locale fallback helper --- src/Pagination.tsx | 2 +- tests/__snapshots__/demo.test.tsx.snap | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Pagination.tsx b/src/Pagination.tsx index 2ca06bd8..07fc3b6a 100644 --- a/src/Pagination.tsx +++ b/src/Pagination.tsx @@ -413,7 +413,7 @@ const Pagination: React.FC = (props) => { ) : ( Date: Tue, 2 Jun 2026 09:20:21 +0000 Subject: [PATCH 02/11] fix: address pagination a11y review feedback --- src/Pagination.tsx | 6 +++--- tests/index.test.tsx | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/Pagination.tsx b/src/Pagination.tsx index 07fc3b6a..f6d84a2a 100644 --- a/src/Pagination.tsx +++ b/src/Pagination.tsx @@ -413,7 +413,7 @@ const Pagination: React.FC = (props) => { ) : ( = (props) => { style={styles?.item} aria-disabled={prevDisabled} role="button" - aria-label={locale.prev_page} + aria-label={locale.prev_page || 'prev page'} > {prev} @@ -612,7 +612,7 @@ const Pagination: React.FC = (props) => { style={styles?.item} aria-disabled={nextDisabled} role="button" - aria-label={locale.next_page} + aria-label={locale.next_page || 'next page'} > {next} diff --git a/tests/index.test.tsx b/tests/index.test.tsx index 7cba57e2..709635e7 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -111,6 +111,21 @@ describe('Uncontrolled Pagination', () => { expect(currentPage).toHaveAttribute('aria-current', 'page'); }); + it('should fallback navigation labels when locale values are missing', () => { + const { container } = render( + , + ); + + expect(container.querySelector('.rc-pagination-prev')).toHaveAttribute( + 'aria-label', + 'prev page', + ); + expect(container.querySelector('.rc-pagination-next')).toHaveAttribute( + 'aria-label', + 'next page', + ); + }); + it('should response mouse click right', () => { const pagers = $$('.rc-pagination-item'); expect(pagers).toHaveLength(3); From 0227aa1cc174e7813289c0a9ee42598a7020de7d Mon Sep 17 00:00:00 2001 From: Zou Quan <2990918167@qq.com> Date: Wed, 3 Jun 2026 06:07:46 +0000 Subject: [PATCH 03/11] test: update demo snapshots for simple input label --- tests/__snapshots__/demo.test.tsx.snap | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/__snapshots__/demo.test.tsx.snap b/tests/__snapshots__/demo.test.tsx.snap index e9079656..2b1ffd7e 100644 --- a/tests/__snapshots__/demo.test.tsx.snap +++ b/tests/__snapshots__/demo.test.tsx.snap @@ -2687,7 +2687,7 @@ exports[`Example jumperWithGoButton 1`] = ` title="1/5" > Date: Thu, 4 Jun 2026 11:56:11 +0000 Subject: [PATCH 04/11] fix: limit pagination prev next fallback usage --- src/Pagination.tsx | 4 ++-- tests/index.test.tsx | 15 --------------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/src/Pagination.tsx b/src/Pagination.tsx index f6d84a2a..c1da47c7 100644 --- a/src/Pagination.tsx +++ b/src/Pagination.tsx @@ -582,7 +582,7 @@ const Pagination: React.FC = (props) => { style={styles?.item} aria-disabled={prevDisabled} role="button" - aria-label={locale.prev_page || 'prev page'} + aria-label={locale.prev_page} > {prev} @@ -612,7 +612,7 @@ const Pagination: React.FC = (props) => { style={styles?.item} aria-disabled={nextDisabled} role="button" - aria-label={locale.next_page || 'next page'} + aria-label={locale.next_page} > {next} diff --git a/tests/index.test.tsx b/tests/index.test.tsx index 709635e7..7cba57e2 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -111,21 +111,6 @@ describe('Uncontrolled Pagination', () => { expect(currentPage).toHaveAttribute('aria-current', 'page'); }); - it('should fallback navigation labels when locale values are missing', () => { - const { container } = render( - , - ); - - expect(container.querySelector('.rc-pagination-prev')).toHaveAttribute( - 'aria-label', - 'prev page', - ); - expect(container.querySelector('.rc-pagination-next')).toHaveAttribute( - 'aria-label', - 'next page', - ); - }); - it('should response mouse click right', () => { const pagers = $$('.rc-pagination-item'); expect(pagers).toHaveLength(3); From 3585ea4d924fcb05bb45ea205130f8b7e0bcccf6 Mon Sep 17 00:00:00 2001 From: Zou Quan <2990918167@qq.com> Date: Thu, 4 Jun 2026 12:07:26 +0000 Subject: [PATCH 05/11] fix: remove extra pagination locale fallbacks --- src/Pagination.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Pagination.tsx b/src/Pagination.tsx index c1da47c7..2ca06bd8 100644 --- a/src/Pagination.tsx +++ b/src/Pagination.tsx @@ -413,7 +413,7 @@ const Pagination: React.FC = (props) => { ) : ( Date: Wed, 8 Jul 2026 17:31:51 +0800 Subject: [PATCH 06/11] fix: improve pagination a11y semantics --- assets/index.less | 86 +- src/Pager.tsx | 32 +- src/Pagination.tsx | 244 +- tests/__snapshots__/demo.test.tsx.snap | 2976 ++++++++-------------- tests/__snapshots__/simple.test.tsx.snap | 10 +- tests/index.test.tsx | 216 +- tests/itemRender.test.tsx | 29 +- tests/jumper.test.tsx | 4 +- 8 files changed, 1416 insertions(+), 2181 deletions(-) diff --git a/assets/index.less b/assets/index.less index 64ceac6d..7e63ee71 100644 --- a/assets/index.less +++ b/assets/index.less @@ -71,10 +71,17 @@ cursor: pointer; user-select: none; - a { + a, + button { display: block; + width: 100%; + height: 100%; padding: 0 6px; color: rgba(0, 0, 0, 0.85); + background: transparent; + border: 0; + cursor: pointer; + outline: none; transition: none; &:hover { @@ -83,10 +90,12 @@ } &:focus, + &:focus-within, &:hover { border-color: @primary-color; transition: all 0.3s; - a { + a, + button { color: @primary-color; } } @@ -96,17 +105,23 @@ background: @pagination-item-bg-active; border-color: @primary-color; - a { + a, + button { color: @primary-color; } &:focus, + &:focus-within, &:hover { border-color: #40a9ff; } &:focus a, - &:hover a { + &:hover a, + &:focus-within a, + &:focus button, + &:hover button, + &:focus-within button { color: #40a9ff; } } @@ -115,18 +130,6 @@ &-jump-prev, &-jump-next { outline: 0; - - button { - background: transparent; - border: none; - cursor: pointer; - color: #666; - } - - button:after { - display: block; - content: '•••'; - } } &-prev, @@ -153,20 +156,14 @@ } &-prev, - &-next { + &-next, + &-jump-prev, + &-jump-next { outline: 0; - button { - color: rgba(0, 0, 0, 0.85); - cursor: pointer; - user-select: none; - } - - &:hover button { - border-color: #40a9ff; - } - - .@{pagination-prefix-cls}-item-link { + button, + .@{pagination-prefix-cls}-item-link, + .@{pagination-prefix-cls}-item-button { display: block; width: 100%; height: 100%; @@ -176,32 +173,45 @@ border: 1px solid #d9d9d9; border-radius: 2px; outline: none; + cursor: pointer; + user-select: none; transition: all 0.3s; } + button:focus, &:focus .@{pagination-prefix-cls}-item-link, - &:hover .@{pagination-prefix-cls}-item-link { + &:focus .@{pagination-prefix-cls}-item-button, + &:hover .@{pagination-prefix-cls}-item-link, + &:hover .@{pagination-prefix-cls}-item-button, + &:hover button { color: @primary-color; border-color: @primary-color; } } - &-prev button:after { + &-prev .@{pagination-prefix-cls}-item-link::after { content: '‹'; display: block; } - &-next button:after { + &-next .@{pagination-prefix-cls}-item-link::after { content: '›'; display: block; } + &-jump-prev .@{pagination-prefix-cls}-item-link::after, + &-jump-next .@{pagination-prefix-cls}-item-link::after { + display: block; + content: '•••'; + } + &-disabled { &, &:hover, &:focus { cursor: not-allowed; - .@{pagination-prefix-cls}-item-link { + .@{pagination-prefix-cls}-item-link, + .@{pagination-prefix-cls}-item-button { color: fade(#000, 25%); border-color: #d9d9d9; cursor: not-allowed; @@ -250,7 +260,8 @@ height: @pagination-item-size-sm; line-height: @pagination-item-size-sm; vertical-align: top; - .@{pagination-prefix-cls}-item-link { + .@{pagination-prefix-cls}-item-link, + .@{pagination-prefix-cls}-item-button { height: @pagination-item-size-sm; background-color: transparent; border: 0; @@ -293,7 +304,8 @@ border-color: #d9d9d9; cursor: not-allowed; - a { + a, + button { color: fade(#000, 25%); background: transparent; border: none; @@ -303,13 +315,15 @@ &-active { background: @pagination-item-disabled-bg-active; border-color: transparent; - a { + a, + button { color: @pagination-item-disabled-color-active; } } } - .@{pagination-prefix-cls}-item-link { + .@{pagination-prefix-cls}-item-link, + .@{pagination-prefix-cls}-item-button { color: fade(#000, 25%); background: hsv(0, 0, 96%); border-color: #d9d9d9; diff --git a/src/Pager.tsx b/src/Pager.tsx index 5a512afd..02446b0d 100644 --- a/src/Pager.tsx +++ b/src/Pager.tsx @@ -7,6 +7,7 @@ export interface PagerProps extends Pick { rootPrefixCls: string; page: number; pageLabel?: string; + defaultItemRender?: boolean; active?: boolean; className?: string; style?: React.CSSProperties; @@ -24,6 +25,7 @@ const Pager: React.FC = (props) => { rootPrefixCls, page, pageLabel, + defaultItemRender, active, className, style, @@ -52,24 +54,36 @@ const Pager: React.FC = (props) => { onKeyPress(e, onClick, page); }; - const pager = itemRender( - page, - 'page', - , - ); const pagerLabel = `${pageLabel} ${page}`.trim(); + const itemTitle = showTitle ? String(page) : undefined; + + if (defaultItemRender) { + return ( +
  • + +
  • + ); + } + + const pager = itemRender(page, 'page', {page}); return pager ? (
  • diff --git a/src/Pagination.tsx b/src/Pagination.tsx index 2ca06bd8..bd51ed8a 100644 --- a/src/Pagination.tsx +++ b/src/Pagination.tsx @@ -17,6 +17,8 @@ import Pager from './Pager'; const defaultItemRender: PaginationProps['itemRender'] = (_, __, element) => element; +type PaginationItemType = 'prev' | 'next' | 'jump-prev' | 'jump-next'; + function noop() {} function isInteger(v: number) { @@ -115,27 +117,62 @@ const Pagination: React.FC = (props) => { calculatePage(undefined, pageSize, total), current + (showLessItems ? 3 : 5), ); + const isDefaultRender = itemRender === defaultItemRender; - function getItemIcon( + function getIconNode( icon: React.ReactNode | React.ComponentType, - _label: string, + ) { + let iconNode = icon; + if (typeof icon === 'function') { + iconNode = React.createElement(icon, props); + } + return iconNode as React.ReactNode; + } + + function getItemIcon(title?: string) { + return ; + } + + function getControlButtonContent( + icon: React.ReactNode | React.ComponentType, + ) { + const iconNode = getIconNode(icon); + + if ( + React.isValidElement<{ children?: React.ReactNode }>(iconNode) && + typeof iconNode.type === 'string' && + (iconNode.type === 'button' || iconNode.type === 'a') + ) { + return iconNode.props.children; + } + + return iconNode; + } + + function renderDefaultControlButton( + type: PaginationItemType, + icon: React.ReactNode | React.ComponentType, + label?: string, title?: string, + onClick?: React.MouseEventHandler, + buttonDisabled?: boolean, ) { - let iconNode = isReactRenderable(icon) ? ( - icon - ) : ( + const iconContent = getControlButtonContent(icon); + + return ( ); - if (typeof icon === 'function') { - iconNode = React.createElement(icon, props); - } - return iconNode as React.ReactNode; } function getValidValue(e: any): number { @@ -286,15 +323,21 @@ const Pagination: React.FC = (props) => { function renderPrev(prevPage: number) { const prevPageTitle = locale.prev_page || 'prev page'; - const prevButton = itemRender( - prevPage, - 'prev', - getItemIcon( - prevIcon, - prevPageTitle, - showTitle ? prevPageTitle : undefined, - ), - ); + const prevButton = isDefaultRender + ? renderDefaultControlButton( + 'prev', + prevIcon, + prevPageTitle, + showTitle ? prevPageTitle : undefined, + prevHandle, + !hasPrev, + ) + : itemRender( + prevPage, + 'prev', + getIconNode(prevIcon) || + getItemIcon(showTitle ? prevPageTitle : undefined), + ); return React.isValidElement(prevButton) ? React.cloneElement(prevButton, { disabled: !hasPrev }) : prevButton; @@ -302,15 +345,21 @@ const Pagination: React.FC = (props) => { function renderNext(nextPage: number) { const nextPageTitle = locale.next_page || 'next page'; - const nextButton = itemRender( - nextPage, - 'next', - getItemIcon( - nextIcon, - nextPageTitle, - showTitle ? nextPageTitle : undefined, - ), - ); + const nextButton = isDefaultRender + ? renderDefaultControlButton( + 'next', + nextIcon, + nextPageTitle, + showTitle ? nextPageTitle : undefined, + nextHandle, + !hasNext, + ) + : itemRender( + nextPage, + 'next', + getIconNode(nextIcon) || + getItemIcon(showTitle ? nextPageTitle : undefined), + ); return React.isValidElement(nextButton) ? React.cloneElement(nextButton, { disabled: !hasNext }) : nextButton; @@ -352,6 +401,7 @@ const Pagination: React.FC = (props) => { const pagerProps: PagerProps = { rootPrefixCls: prefixCls, + defaultItemRender: isDefaultRender, onClick: handleChange, onKeyPress: runIfEnterOrSpace, showTitle, @@ -455,56 +505,84 @@ const Pagination: React.FC = (props) => { const jumpPrevContent = itemRender( jumpPrevPage, 'jump-prev', - getItemIcon( - jumpPrevIcon, - prevItemTitle, - showTitle ? prevItemTitle : undefined, - ), + getIconNode(jumpPrevIcon) || + getItemIcon(showTitle ? prevItemTitle : undefined), ); const jumpNextContent = itemRender( jumpNextPage, 'jump-next', - getItemIcon( - jumpNextIcon, - nextItemTitle, - showTitle ? nextItemTitle : undefined, - ), + getIconNode(jumpNextIcon) || + getItemIcon(showTitle ? nextItemTitle : undefined), ); if (showPrevNextJumpers) { - jumpPrev = isReactRenderable(jumpPrevContent) ? ( -
  • - {jumpPrevContent} -
  • - ) : null; + if (isDefaultRender) { + jumpPrev = ( +
  • + {renderDefaultControlButton( + 'jump-prev', + jumpPrevIcon, + prevItemTitle, + showTitle ? prevItemTitle : undefined, + jumpPrevHandle, + )} +
  • + ); - jumpNext = isReactRenderable(jumpNextContent) ? ( -
  • - {jumpNextContent} -
  • - ) : null; + jumpNext = ( +
  • + {renderDefaultControlButton( + 'jump-next', + jumpNextIcon, + nextItemTitle, + showTitle ? nextItemTitle : undefined, + jumpNextHandle, + )} +
  • + ); + } else { + jumpPrev = jumpPrevContent ? ( +
  • + {jumpPrevContent} +
  • + ) : null; + + jumpNext = jumpNextContent ? ( +
  • + {jumpNextContent} +
  • + ) : null; + } } let left = Math.max(1, current - pageBufferSize); @@ -573,16 +651,16 @@ const Pagination: React.FC = (props) => { const prevDisabled = !hasPrev || !allPages; prev = (
  • {prev}
  • @@ -603,16 +681,16 @@ const Pagination: React.FC = (props) => { next = (
  • {next}
  • diff --git a/tests/__snapshots__/demo.test.tsx.snap b/tests/__snapshots__/demo.test.tsx.snap index 2b1ffd7e..a2e739e9 100644 --- a/tests/__snapshots__/demo.test.tsx.snap +++ b/tests/__snapshots__/demo.test.tsx.snap @@ -7,45 +7,36 @@ exports[`Example align 1`] = ` >
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • @@ -1450,7 +1113,6 @@ exports[`Example itemRender 1`] = `
  • @@ -1463,7 +1125,6 @@ exports[`Example itemRender 1`] = `
  • @@ -1476,7 +1137,6 @@ exports[`Example itemRender 1`] = `
  • @@ -1489,7 +1149,6 @@ exports[`Example itemRender 1`] = `
  • @@ -1502,21 +1161,17 @@ exports[`Example itemRender 1`] = `
  • -
  • @@ -1530,15 +1185,12 @@ exports[`Example itemRender 1`] = ` aria-disabled="false" aria-label="下一页" class="rc-pagination-next" - role="button" tabindex="0" + title="下一页" > -
  • Prev
  • @@ -1560,14 +1212,11 @@ exports[`Example itemRender 1`] = ` aria-current="page" aria-label="页 1" class="rc-pagination-item rc-pagination-item-1 rc-pagination-item-active" - role="button" tabindex="0" title="1" > @@ -1575,14 +1224,11 @@ exports[`Example itemRender 1`] = `
  • @@ -1590,14 +1236,11 @@ exports[`Example itemRender 1`] = `
  • @@ -1605,14 +1248,11 @@ exports[`Example itemRender 1`] = `
  • @@ -1620,14 +1260,11 @@ exports[`Example itemRender 1`] = `
  • @@ -1635,28 +1272,22 @@ exports[`Example itemRender 1`] = `
  • -
  • @@ -1665,8 +1296,8 @@ exports[`Example itemRender 1`] = ` aria-disabled="false" aria-label="下一页" class="rc-pagination-next" - role="button" tabindex="0" + title="下一页" > Next
  • @@ -1681,7 +1312,7 @@ exports[`Example itemRender 1`] = ` aria-disabled="true" aria-label="上一页" class="rc-pagination-prev rc-pagination-disabled" - role="button" + title="上一页" >
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - - - - - - + + + + + +
  • - +
  • - - - - - + + + + +
  • - - - - - + + + + +
  • - +
  • - +
  • - +
  • - - - - - - + + + + + +
  • - +
  • - - - - - + + + + +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • -
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • @@ -3845,99 +3122,72 @@ exports[`Example showTitle 1`] = ` >
  • - +
  • - +
  • - +
  • - +
  • @@ -3961,135 +3211,100 @@ exports[`Example showTotal 1`] = `
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • + } + nextIcon={ + + } + jumpPrevIcon={ + // biome-ignore lint/a11y/useValidAnchor: test fallback output + + jumpPrevIcon + + } + jumpNextIcon={ + // biome-ignore lint/a11y/useValidAnchor: test fallback output + + jumpNextIcon + + } + />, + ); + + expect( + container.querySelectorAll('.rc-pagination-prev button'), + ).toHaveLength(1); + expect( + container.querySelectorAll('.rc-pagination-next button'), + ).toHaveLength(1); + expect( + container.querySelectorAll('.rc-pagination-jump-prev button'), + ).toHaveLength(1); + expect( + container.querySelectorAll('.rc-pagination-jump-next button'), + ).toHaveLength(1); + expect(container.querySelector('.rc-pagination-prev')).toHaveTextContent( + 'prevIcon', + ); + expect(container.querySelector('.rc-pagination-next')).toHaveTextContent( + 'nextIcon', + ); + expect( + container.querySelector('.rc-pagination-jump-prev'), + ).toHaveTextContent('jumpPrevIcon'); + expect( + container.querySelector('.rc-pagination-jump-next'), + ).toHaveTextContent('jumpNextIcon'); + }); + describe('showPrevNextJumpers props', () => { it('should hide jump-prev, jump-next if showPrevNextJumpers equals false', () => { const { container } = render( @@ -610,8 +682,10 @@ describe('should emit onChange when total is string', () => { }); it('onChange should be called when click page', () => { - const pagers = wrapper.container.querySelectorAll('.rc-pagination-item-3'); - fireEvent.click(pagers[0]); + const page3Button = wrapper.container.querySelector( + '.rc-pagination-item-3 button', + ); + fireEvent.click(page3Button); expect(onChange).toHaveBeenCalledWith(3, 10); }); }); @@ -632,35 +706,39 @@ describe('keyboard support', () => { onChange.mockReset(); }); - it('should work for prev page', () => { - const prevButton = $('li.rc-pagination-prev'); + it('should work for prev page', async () => { + const user = userEvent.setup(); + const prevButton = $('li.rc-pagination-prev button'); expect(prevButton).toBeTruthy(); fireEvent.click(prevButton); fireEvent.click(prevButton); - fireEvent.keyDown(prevButton, { key: 'Enter', keyCode: 13, which: 13 }); - fireEvent.keyDown(prevButton, { key: 'Enter', keyCode: 13, which: 13 }); + prevButton.focus(); + await user.keyboard('{Enter}{Enter}'); expect(onChange).toHaveBeenLastCalledWith(46, 10); }); - it('should work for prev page with space key', () => { - const prevButton = $('li.rc-pagination-prev'); + it('should work for prev page with space key', async () => { + const user = userEvent.setup(); + const prevButton = $('li.rc-pagination-prev button'); expect(prevButton).toBeTruthy(); - fireEvent.keyDown(prevButton, { key: ' ', keyCode: 32, which: 32 }); - fireEvent.keyDown(prevButton, { key: 'Spacebar', keyCode: 32, which: 32 }); + prevButton.focus(); + await user.keyboard(' '); + await user.keyboard(' '); expect(onChange).toHaveBeenLastCalledWith(48, 10); }); - it('should work for next page', () => { - const nextButton = $('li.rc-pagination-next'); + it('should work for next page', async () => { + const user = userEvent.setup(); + const nextButton = $('li.rc-pagination-next button'); expect(nextButton).toBeTruthy(); - fireEvent.keyDown(nextButton, { key: 'Enter', keyCode: 13, which: 13 }); - fireEvent.keyDown(nextButton, { key: 'Enter', keyCode: 13, which: 13 }); + nextButton.focus(); + await user.keyboard('{Enter}{Enter}'); fireEvent.click(nextButton); fireEvent.click(nextButton); @@ -668,69 +746,73 @@ describe('keyboard support', () => { expect(onChange).toHaveBeenLastCalledWith(54, 10); }); - it('should work for next page with space key', () => { - const nextButton = $('li.rc-pagination-next'); + it('should work for next page with space key', async () => { + const user = userEvent.setup(); + const nextButton = $('li.rc-pagination-next button'); expect(nextButton).toBeTruthy(); - fireEvent.keyDown(nextButton, { key: ' ', keyCode: 32, which: 32 }); - fireEvent.keyDown(nextButton, { key: 'Spacebar', keyCode: 32, which: 32 }); + nextButton.focus(); + await user.keyboard(' '); + await user.keyboard(' '); expect(onChange).toHaveBeenLastCalledWith(52, 10); }); - it('should work for next page with enter keyCode only', () => { - const nextButton = $('li.rc-pagination-next'); + it('should work for next page with enter keyCode only', async () => { + const user = userEvent.setup(); + const nextButton = $('li.rc-pagination-next button'); expect(nextButton).toBeTruthy(); - fireEvent.keyDown(nextButton, { keyCode: 13, which: 13 }); + nextButton.focus(); + await user.keyboard('{Enter}'); expect(onChange).toHaveBeenLastCalledWith(51, 10); }); - it('should work for jump prev page', () => { - const jumpPrevButton = $('li.rc-pagination-jump-prev'); + it('should work for jump prev page', async () => { + const user = userEvent.setup(); + const jumpPrevButton = $('li.rc-pagination-jump-prev button'); expect(jumpPrevButton).toBeTruthy(); - fireEvent.keyDown(jumpPrevButton, { key: 'Enter', keyCode: 13, which: 13 }); fireEvent.click(jumpPrevButton); + jumpPrevButton.focus(); + await user.keyboard('{Enter}'); expect(onChange).toHaveBeenLastCalledWith(40, 10); }); - it('should work for jump prev page with space key', () => { - const jumpPrevButton = $('li.rc-pagination-jump-prev'); + it('should work for jump prev page with space key', async () => { + const user = userEvent.setup(); + const jumpPrevButton = $('li.rc-pagination-jump-prev button'); expect(jumpPrevButton).toBeTruthy(); - fireEvent.keyDown(jumpPrevButton, { key: ' ', keyCode: 32, which: 32 }); - fireEvent.keyDown(jumpPrevButton, { - key: 'Spacebar', - keyCode: 32, - which: 32, - }); + jumpPrevButton.focus(); + await user.keyboard(' '); + await user.keyboard(' '); expect(onChange).toHaveBeenLastCalledWith(40, 10); }); - it('should work for jump next page', () => { - const jumpNextButton = $('li.rc-pagination-jump-next'); + it('should work for jump next page', async () => { + const user = userEvent.setup(); + const jumpNextButton = $('li.rc-pagination-jump-next button'); expect(jumpNextButton).toBeTruthy(); fireEvent.click(jumpNextButton); - fireEvent.keyDown(jumpNextButton, { key: 'Enter', keyCode: 13, which: 13 }); + jumpNextButton.focus(); + await user.keyboard('{Enter}'); expect(onChange).toHaveBeenLastCalledWith(60, 10); }); - it('should work for jump next page with space key', () => { - const jumpNextButton = $('li.rc-pagination-jump-next'); + it('should work for jump next page with space key', async () => { + const user = userEvent.setup(); + const jumpNextButton = $('li.rc-pagination-jump-next button'); expect(jumpNextButton).toBeTruthy(); - fireEvent.keyDown(jumpNextButton, { key: ' ', keyCode: 32, which: 32 }); - fireEvent.keyDown(jumpNextButton, { - key: 'Spacebar', - keyCode: 32, - which: 32, - }); + jumpNextButton.focus(); + await user.keyboard(' '); + await user.keyboard(' '); expect(onChange).toHaveBeenLastCalledWith(60, 10); }); diff --git a/tests/itemRender.test.tsx b/tests/itemRender.test.tsx index 50c426f2..374099bd 100644 --- a/tests/itemRender.test.tsx +++ b/tests/itemRender.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import type { RenderResult } from '@testing-library/react'; -import { render } from '@testing-library/react'; +import { fireEvent, render } from '@testing-library/react'; import Pagination from '../src'; describe('itemRender', () => { @@ -108,4 +108,31 @@ describe('itemRender', () => { expect(prev.innerHTML).toBe('0'); expect(next.innerHTML).toBe('2'); }); + + it('should keep wrapper interaction for custom itemRender fallback', () => { + const onChange = jest.fn(); + const { container } = render( + originalElement} + />, + ); + + const pageButton = container.querySelector('.rc-pagination-item-13'); + const jumpNextButton = container.querySelector('.rc-pagination-jump-next'); + + expect(pageButton).not.toHaveAttribute('role'); + expect(jumpNextButton).not.toHaveAttribute('role'); + + fireEvent.click(pageButton); + fireEvent.keyDown(jumpNextButton, { + key: 'Spacebar', + keyCode: 32, + which: 32, + }); + + expect(onChange).toHaveBeenLastCalledWith(18, 10); + }); }); diff --git a/tests/jumper.test.tsx b/tests/jumper.test.tsx index f87dbcb7..474acc2f 100644 --- a/tests/jumper.test.tsx +++ b/tests/jumper.test.tsx @@ -185,7 +185,9 @@ describe('simple quick jumper', () => { // https://github.com/ant-design/ant-design/issues/10080 it('should not quick jump to previous page when input invalid char', () => { - const nextButton = wrapper.container.querySelector('.rc-pagination-next'); + const nextButton = wrapper.container.querySelector( + '.rc-pagination-next button', + ); fireEvent.click(nextButton); const input = wrapper.container.querySelector('input'); fireEvent.change(input, { target: { value: '&' } }); From e4544767127c4d2f57af4da915ee7f1486f1d8e8 Mon Sep 17 00:00:00 2001 From: ZQDesigned <2990918167@qq.com> Date: Thu, 9 Jul 2026 00:55:01 +0800 Subject: [PATCH 07/11] fix: follow up pagination a11y review --- src/Pager.tsx | 2 +- src/Pagination.tsx | 32 ++++++++++++++++++-------------- tests/index.test.tsx | 34 +++++++++++++--------------------- 3 files changed, 32 insertions(+), 36 deletions(-) diff --git a/src/Pager.tsx b/src/Pager.tsx index 02446b0d..ede68cb0 100644 --- a/src/Pager.tsx +++ b/src/Pager.tsx @@ -54,7 +54,7 @@ const Pager: React.FC = (props) => { onKeyPress(e, onClick, page); }; - const pagerLabel = `${pageLabel} ${page}`.trim(); + const pagerLabel = pageLabel ? `${pageLabel} ${page}` : String(page); const itemTitle = showTitle ? String(page) : undefined; if (defaultItemRender) { diff --git a/src/Pagination.tsx b/src/Pagination.tsx index bd51ed8a..e4223366 100644 --- a/src/Pagination.tsx +++ b/src/Pagination.tsx @@ -651,16 +651,18 @@ const Pagination: React.FC = (props) => { const prevDisabled = !hasPrev || !allPages; prev = (
  • {prev}
  • @@ -668,29 +670,31 @@ const Pagination: React.FC = (props) => { } let next = renderNext(nextPage); - if (isReactRenderable(next)) { - let nextDisabled: boolean, nextTabIndex: number | null; + if (next) { + let nextDisabled: boolean, nextTabIndex: number | undefined; if (simple) { nextDisabled = !hasNext; - nextTabIndex = hasPrev ? 0 : null; + nextTabIndex = hasPrev ? 0 : undefined; } else { nextDisabled = !hasNext || !allPages; - nextTabIndex = nextDisabled ? null : 0; + nextTabIndex = nextDisabled ? undefined : 0; } next = (
  • {next}
  • diff --git a/tests/index.test.tsx b/tests/index.test.tsx index 243cc4f4..93d33120 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -225,9 +225,9 @@ describe('Uncontrolled Pagination', () => { it('should response keyboard event', async () => { const user = userEvent.setup(); - const page3Button = wrapper.container.querySelector( + const page3Button = wrapper.container.querySelector( '.rc-pagination-item-3 button', - ); + )!; page3Button.focus(); await user.keyboard('{Enter}'); expect( @@ -693,7 +693,8 @@ describe('should emit onChange when total is string', () => { describe('keyboard support', () => { let wrapper: RenderResult; const onChange = jest.fn(); - const $ = (selector: string) => wrapper.container.querySelector(selector); + const $button = (selector: string) => + wrapper.container.querySelector(selector)!; beforeEach(() => { wrapper = render( @@ -708,8 +709,7 @@ describe('keyboard support', () => { it('should work for prev page', async () => { const user = userEvent.setup(); - const prevButton = $('li.rc-pagination-prev button'); - expect(prevButton).toBeTruthy(); + const prevButton = $button('li.rc-pagination-prev button'); fireEvent.click(prevButton); fireEvent.click(prevButton); @@ -722,8 +722,7 @@ describe('keyboard support', () => { it('should work for prev page with space key', async () => { const user = userEvent.setup(); - const prevButton = $('li.rc-pagination-prev button'); - expect(prevButton).toBeTruthy(); + const prevButton = $button('li.rc-pagination-prev button'); prevButton.focus(); await user.keyboard(' '); @@ -734,8 +733,7 @@ describe('keyboard support', () => { it('should work for next page', async () => { const user = userEvent.setup(); - const nextButton = $('li.rc-pagination-next button'); - expect(nextButton).toBeTruthy(); + const nextButton = $button('li.rc-pagination-next button'); nextButton.focus(); await user.keyboard('{Enter}{Enter}'); @@ -748,8 +746,7 @@ describe('keyboard support', () => { it('should work for next page with space key', async () => { const user = userEvent.setup(); - const nextButton = $('li.rc-pagination-next button'); - expect(nextButton).toBeTruthy(); + const nextButton = $button('li.rc-pagination-next button'); nextButton.focus(); await user.keyboard(' '); @@ -760,8 +757,7 @@ describe('keyboard support', () => { it('should work for next page with enter keyCode only', async () => { const user = userEvent.setup(); - const nextButton = $('li.rc-pagination-next button'); - expect(nextButton).toBeTruthy(); + const nextButton = $button('li.rc-pagination-next button'); nextButton.focus(); await user.keyboard('{Enter}'); @@ -771,8 +767,7 @@ describe('keyboard support', () => { it('should work for jump prev page', async () => { const user = userEvent.setup(); - const jumpPrevButton = $('li.rc-pagination-jump-prev button'); - expect(jumpPrevButton).toBeTruthy(); + const jumpPrevButton = $button('li.rc-pagination-jump-prev button'); fireEvent.click(jumpPrevButton); jumpPrevButton.focus(); @@ -783,8 +778,7 @@ describe('keyboard support', () => { it('should work for jump prev page with space key', async () => { const user = userEvent.setup(); - const jumpPrevButton = $('li.rc-pagination-jump-prev button'); - expect(jumpPrevButton).toBeTruthy(); + const jumpPrevButton = $button('li.rc-pagination-jump-prev button'); jumpPrevButton.focus(); await user.keyboard(' '); @@ -795,8 +789,7 @@ describe('keyboard support', () => { it('should work for jump next page', async () => { const user = userEvent.setup(); - const jumpNextButton = $('li.rc-pagination-jump-next button'); - expect(jumpNextButton).toBeTruthy(); + const jumpNextButton = $button('li.rc-pagination-jump-next button'); fireEvent.click(jumpNextButton); jumpNextButton.focus(); @@ -807,8 +800,7 @@ describe('keyboard support', () => { it('should work for jump next page with space key', async () => { const user = userEvent.setup(); - const jumpNextButton = $('li.rc-pagination-jump-next button'); - expect(jumpNextButton).toBeTruthy(); + const jumpNextButton = $button('li.rc-pagination-jump-next button'); jumpNextButton.focus(); await user.keyboard(' '); From f7d4df2ca10816576817933d888bf619feb2450b Mon Sep 17 00:00:00 2001 From: ZQDesigned <2990918167@qq.com> Date: Thu, 9 Jul 2026 00:58:05 +0800 Subject: [PATCH 08/11] fix: disable default pagination buttons --- src/Pager.tsx | 8 ++++++-- src/Pagination.tsx | 7 ++++--- tests/__snapshots__/demo.test.tsx.snap | 9 +++++++++ tests/index.test.tsx | 9 +++++++++ 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/Pager.tsx b/src/Pager.tsx index ede68cb0..cb48e8c7 100644 --- a/src/Pager.tsx +++ b/src/Pager.tsx @@ -3,7 +3,10 @@ import { clsx } from 'clsx'; import React from 'react'; import type { PaginationProps } from './interface'; -export interface PagerProps extends Pick { +export interface PagerProps extends Pick< + PaginationProps, + 'disabled' | 'itemRender' +> { rootPrefixCls: string; page: number; pageLabel?: string; @@ -25,6 +28,7 @@ const Pager: React.FC = (props) => { rootPrefixCls, page, pageLabel, + disabled, defaultItemRender, active, className, @@ -66,7 +70,7 @@ const Pager: React.FC = (props) => { title={itemTitle} aria-label={pagerLabel} aria-current={active ? 'page' : undefined} - disabled={cls.includes(`${prefixCls}-disabled`)} + disabled={disabled || cls.includes(`${prefixCls}-disabled`)} > {page} diff --git a/src/Pagination.tsx b/src/Pagination.tsx index e4223366..c2330951 100644 --- a/src/Pagination.tsx +++ b/src/Pagination.tsx @@ -163,7 +163,7 @@ const Pagination: React.FC = (props) => { + ); } diff --git a/src/Pagination.tsx b/src/Pagination.tsx index c2330951..dc5ec45f 100644 --- a/src/Pagination.tsx +++ b/src/Pagination.tsx @@ -485,12 +485,7 @@ const Pagination: React.FC = (props) => { if (allPages <= 3 + pageBufferSize * 2) { if (!allPages) { pagerList.push( - , + , ); } diff --git a/tests/__snapshots__/demo.test.tsx.snap b/tests/__snapshots__/demo.test.tsx.snap index c91ca5ef..ab1eff1d 100644 --- a/tests/__snapshots__/demo.test.tsx.snap +++ b/tests/__snapshots__/demo.test.tsx.snap @@ -20,14 +20,16 @@ exports[`Example align 1`] = `
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • { const currentPage = wrapper.container.querySelector( '.rc-pagination-item-active', ); - const currentButton = currentPage.querySelector('button'); + const currentLink = currentPage.querySelector('a'); expect(currentPage).toHaveTextContent('1'); expect($$('.rc-pagination-item')[0]).toHaveTextContent('1'); - expect(currentButton).toHaveAttribute('aria-current', 'page'); - expect(currentButton).toHaveAttribute('title', '1'); + expect(currentLink).toHaveAttribute('aria-current', 'page'); + expect(currentLink).toHaveAttribute('title', '1'); }); it('prev-button should be disabled', () => { @@ -97,7 +97,7 @@ describe('Uncontrolled Pagination', () => { expect(nextButton).toHaveAttribute('aria-disabled', 'false'); }); - it('should expose semantic buttons for navigation items', () => { + it('should expose semantic links for pages and buttons for navigation items', () => { const prevItem = wrapper.container.querySelector('.rc-pagination-prev'); const nextItem = wrapper.container.querySelector('.rc-pagination-next'); const currentItem = wrapper.container.querySelector( @@ -105,7 +105,7 @@ describe('Uncontrolled Pagination', () => { ); const prevButton = prevItem.querySelector('button'); const nextButton = nextItem.querySelector('button'); - const currentButton = currentItem.querySelector('button'); + const currentLink = currentItem.querySelector('a'); expect(prevItem).not.toHaveAttribute('role'); expect(prevButton).toHaveAttribute('aria-label'); @@ -113,14 +113,15 @@ describe('Uncontrolled Pagination', () => { expect(nextItem).not.toHaveAttribute('role'); expect(nextButton).toHaveAttribute('aria-label'); expect(currentItem).not.toHaveAttribute('role'); - expect(currentButton).toHaveAttribute('aria-current', 'page'); - expect(currentButton).toHaveAttribute('aria-label'); + expect(currentLink).toHaveAttribute('href', '#'); + expect(currentLink).toHaveAttribute('aria-current', 'page'); + expect(currentLink).toHaveAttribute('aria-label'); }); it('should response mouse click right', () => { const pagers = $$('.rc-pagination-item'); expect(pagers).toHaveLength(3); - const page2 = pagers[1].querySelector('button'); + const page2 = pagers[1].querySelector('a'); expect(pagers[1]).toHaveClass('rc-pagination-item-2'); fireEvent.click(page2); expect( @@ -225,10 +226,10 @@ describe('Uncontrolled Pagination', () => { it('should response keyboard event', async () => { const user = userEvent.setup(); - const page3Button = wrapper.container.querySelector( - '.rc-pagination-item-3 button', + const page3Link = wrapper.container.querySelector( + '.rc-pagination-item-3 a', )!; - page3Button.focus(); + page3Link.focus(); await user.keyboard('{Enter}'); expect( wrapper.container.querySelector('.rc-pagination-item-active'), @@ -507,9 +508,14 @@ describe('Other props', () => { expect( container.querySelector('.rc-pagination-next button'), ).toBeDisabled(); - expect( - container.querySelector('.rc-pagination-item button'), - ).toBeDisabled(); + expect(container.querySelector('.rc-pagination-item a')).toHaveAttribute( + 'aria-disabled', + 'true', + ); + expect(container.querySelector('.rc-pagination-item a')).toHaveAttribute( + 'tabindex', + '-1', + ); }); }); @@ -691,10 +697,10 @@ describe('should emit onChange when total is string', () => { }); it('onChange should be called when click page', () => { - const page3Button = wrapper.container.querySelector( - '.rc-pagination-item-3 button', + const page3Link = wrapper.container.querySelector( + '.rc-pagination-item-3 a', ); - fireEvent.click(page3Button); + fireEvent.click(page3Link); expect(onChange).toHaveBeenCalledWith(3, 10); }); }); From bd41ff45af46b9eb8c56c740c6fb8272631aa869 Mon Sep 17 00:00:00 2001 From: ZQDesigned <2990918167@qq.com> Date: Wed, 5 Aug 2026 21:27:26 +0800 Subject: [PATCH 10/11] test: cover default pager space key variants --- tests/index.test.tsx | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/index.test.tsx b/tests/index.test.tsx index 0a8b959a..3099262e 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -236,6 +236,30 @@ describe('Uncontrolled Pagination', () => { ).toHaveTextContent('3'); expect(onChange).toHaveBeenLastCalledWith(3, 10); }); + + it('should response space keyboard event', () => { + const page3Link = wrapper.container.querySelector( + '.rc-pagination-item-3 a', + )!; + + fireEvent.keyDown(page3Link, { key: ' ' }); + expect( + wrapper.container.querySelector('.rc-pagination-item-active'), + ).toHaveTextContent('3'); + expect(onChange).toHaveBeenLastCalledWith(3, 10); + }); + + it('should response legacy spacebar keyboard event', () => { + const page3Link = wrapper.container.querySelector( + '.rc-pagination-item-3 a', + )!; + + fireEvent.keyDown(page3Link, { key: 'Spacebar' }); + expect( + wrapper.container.querySelector('.rc-pagination-item-active'), + ).toHaveTextContent('3'); + expect(onChange).toHaveBeenLastCalledWith(3, 10); + }); }); describe('Controlled Pagination', () => { From d2a61b679f0baa4bae4cba11f135c2605a15376e Mon Sep 17 00:00:00 2001 From: ZQDesigned <2990918167@qq.com> Date: Sun, 9 Aug 2026 20:35:56 +0800 Subject: [PATCH 11/11] test: cover custom itemRender keyboard fallbacks --- tests/itemRender.test.tsx | 64 ++++++++++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 8 deletions(-) diff --git a/tests/itemRender.test.tsx b/tests/itemRender.test.tsx index 374099bd..619a4145 100644 --- a/tests/itemRender.test.tsx +++ b/tests/itemRender.test.tsx @@ -8,6 +8,15 @@ describe('itemRender', () => { const currentPage = 12; const itemRender = (current: number) => {current}; const $$ = (selector: string) => wrapper.container.querySelector(selector); + const renderFallbackPagination = (onChange = jest.fn()) => + render( + originalElement} + />, + ); beforeEach(() => { wrapper = render( @@ -111,14 +120,7 @@ describe('itemRender', () => { it('should keep wrapper interaction for custom itemRender fallback', () => { const onChange = jest.fn(); - const { container } = render( - originalElement} - />, - ); + const { container } = renderFallbackPagination(onChange); const pageButton = container.querySelector('.rc-pagination-item-13'); const jumpNextButton = container.querySelector('.rc-pagination-jump-next'); @@ -135,4 +137,50 @@ describe('itemRender', () => { expect(onChange).toHaveBeenLastCalledWith(18, 10); }); + + it('should support keyboard interaction for custom page item wrapper', () => { + const onChange = jest.fn(); + const { container } = renderFallbackPagination(onChange); + const pageButton = container.querySelector('.rc-pagination-item-13'); + + fireEvent.keyDown(pageButton, { + key: 'Enter', + keyCode: 13, + which: 13, + }); + + expect(onChange).toHaveBeenLastCalledWith(13, 10); + }); + + it.each([ + [ + 'prev', + '.rc-pagination-prev', + { key: 'Enter', keyCode: 13, which: 13 }, + 11, + ], + [ + 'next', + '.rc-pagination-next', + { key: 'Enter', keyCode: 13, which: 13 }, + 13, + ], + [ + 'jump-prev', + '.rc-pagination-jump-prev', + { key: ' ', keyCode: 32, which: 32 }, + 7, + ], + ])( + 'should support keyboard interaction for custom %s wrapper', + (_, selector, eventInit, expectedPage) => { + const onChange = jest.fn(); + const { container } = renderFallbackPagination(onChange); + const wrapperButton = container.querySelector(selector); + + fireEvent.keyDown(wrapperButton, eventInit); + + expect(onChange).toHaveBeenLastCalledWith(expectedPage, 10); + }, + ); });