From c559371e289b4b24cfa85df92ddaa5feb0d8815f 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/15] fix: improve pagination a11y semantics without locale fallback helper --- src/Pager.tsx | 14 +- src/Pagination.tsx | 61 +- tests/__snapshots__/demo.test.tsx.snap | 1494 ++++++++++++++++++---- tests/__snapshots__/simple.test.tsx.snap | 14 +- tests/index.test.tsx | 18 + 5 files changed, 1359 insertions(+), 242 deletions(-) diff --git a/src/Pager.tsx b/src/Pager.tsx index 0d1dcc4f..f56d6c78 100644 --- a/src/Pager.tsx +++ b/src/Pager.tsx @@ -6,6 +6,7 @@ import type { PaginationProps } from './interface'; export interface PagerProps extends Pick { rootPrefixCls: string; page: number; + pageLabel?: string; active?: boolean; className?: string; style?: React.CSSProperties; @@ -22,6 +23,7 @@ const Pager: React.FC = (props) => { const { rootPrefixCls, page, + pageLabel, active, className, style, @@ -50,7 +52,14 @@ const Pager: React.FC = (props) => { onKeyPress(e, onClick, page); }; - const pager = itemRender(page, 'page', {page}); + const pager = itemRender( + page, + 'page', + , + ); + const pagerLabel = `${pageLabel || 'Page'} ${page}`.trim(); return pager ? (
  • = (props) => { onClick={handleClick} onKeyDown={handleKeyPress} tabIndex={0} + role="button" + aria-label={pagerLabel} + aria-current={active ? 'page' : undefined} > {pager}
  • diff --git a/src/Pagination.tsx b/src/Pagination.tsx index 452203d4..38b459f3 100644 --- a/src/Pagination.tsx +++ b/src/Pagination.tsx @@ -116,12 +116,15 @@ const Pagination: React.FC = (props) => { function getItemIcon( icon: React.ReactNode | React.ComponentType, - label: string, + _label: string, + title?: string, ) { let iconNode = icon || ( + + ); + } + + const pager = itemRender(page, 'page', {page}); return pager ? (
  • diff --git a/src/Pagination.tsx b/src/Pagination.tsx index 53f0076c..f7074ef6 100644 --- a/src/Pagination.tsx +++ b/src/Pagination.tsx @@ -16,6 +16,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) { @@ -114,25 +116,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 = 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 { @@ -279,15 +318,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; @@ -295,15 +340,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; @@ -345,6 +396,7 @@ const Pagination: React.FC = (props) => { const pagerProps: PagerProps = { rootPrefixCls: prefixCls, + defaultItemRender: isDefaultRender, onClick: handleChange, onKeyPress: runIfEnterOrSpace, showTitle, @@ -448,54 +500,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 = jumpPrevContent ? ( -
  • - {jumpPrevContent} -
  • - ) : null; + if (isDefaultRender) { + jumpPrev = ( +
  • + {renderDefaultControlButton( + 'jump-prev', + jumpPrevIcon, + prevItemTitle, + showTitle ? prevItemTitle : undefined, + jumpPrevHandle, + )} +
  • + ); - jumpNext = 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); @@ -564,16 +646,16 @@ const Pagination: React.FC = (props) => { const prevDisabled = !hasPrev || !allPages; prev = (
  • {prev}
  • @@ -594,16 +676,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 c29e01ee..f1698ed1 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="上一页" >
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - - - - - - + + + + + +
  • - +
  • - - - - - + + + + +
  • - - - - - + + + + +
  • - +
  • - +
  • - +
  • - - - - - - + + + + + +
  • - +
  • - - - - - + + + + +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • -
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • - +
  • @@ -3981,99 +3258,72 @@ exports[`Example showTitle 1`] = ` >
  • - +
  • - +
  • - +
  • - +
  • @@ -4097,135 +3347,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 3d458466c21fac690c56c8f02992c50ad6d1f61e Mon Sep 17 00:00:00 2001 From: ZQDesigned <2990918167@qq.com> Date: Thu, 9 Jul 2026 00:55:01 +0800 Subject: [PATCH 11/15] fix: follow up pagination a11y review --- src/Pager.tsx | 2 +- src/Pagination.tsx | 30 +++++++++++++++++------------- tests/index.test.tsx | 34 +++++++++++++--------------------- 3 files changed, 31 insertions(+), 35 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 5903a208..aac25455 100644 --- a/src/Pagination.tsx +++ b/src/Pagination.tsx @@ -643,16 +643,18 @@ const Pagination: React.FC = (props) => { const prevDisabled = !hasPrev || !allPages; prev = (
  • {prev}
  • @@ -661,28 +663,30 @@ const Pagination: React.FC = (props) => { let next = renderNext(nextPage); if (next) { - let nextDisabled: boolean, nextTabIndex: number | null; + 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 6aeae4ca13c3c448a34809a6477367f4b92830a4 Mon Sep 17 00:00:00 2001 From: ZQDesigned <2990918167@qq.com> Date: Thu, 9 Jul 2026 00:58:05 +0800 Subject: [PATCH 12/15] 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 aac25455..386567ca 100644 --- a/src/Pagination.tsx +++ b/src/Pagination.tsx @@ -159,7 +159,7 @@ const Pagination: React.FC = (props) => { + ); } diff --git a/src/Pagination.tsx b/src/Pagination.tsx index 386567ca..5457303b 100644 --- a/src/Pagination.tsx +++ b/src/Pagination.tsx @@ -479,12 +479,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 8985bf8c..0cdb81bb 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 900ba877405d3586a15af532a9a544d5fba1df02 Mon Sep 17 00:00:00 2001 From: ZQDesigned <2990918167@qq.com> Date: Wed, 5 Aug 2026 21:27:26 +0800 Subject: [PATCH 14/15] 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 8fab48edad9f6ee60dca2e3374fe7b8220649c5f Mon Sep 17 00:00:00 2001 From: ZQDesigned <2990918167@qq.com> Date: Sun, 9 Aug 2026 20:35:56 +0800 Subject: [PATCH 15/15] 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); + }, + ); });