From 099e06df675d8164ef87fe8b3aa5e33d792360d4 Mon Sep 17 00:00:00 2001 From: yoyo837 Date: Wed, 14 Oct 2026 22:33:05 +0800 Subject: [PATCH 1/2] fix: move pagination control semantics off list items --- README.md | 16 + assets/index.less | 3 + package.json | 1 + src/Pager.tsx | 17 +- src/Pagination.tsx | 37 +- src/PaginationItem.tsx | 90 + tests/__snapshots__/demo.test.tsx.snap | 2107 ++++++++-------------- tests/__snapshots__/simple.test.tsx.snap | 10 +- tests/accessibility.test.tsx | 191 ++ tests/index.test.tsx | 120 +- tests/itemRender.test.tsx | 38 +- tests/jumper.test.tsx | 2 +- 12 files changed, 1215 insertions(+), 1417 deletions(-) create mode 100644 src/PaginationItem.tsx create mode 100644 tests/accessibility.test.tsx diff --git a/README.md b/README.md index f058da0a..5749bcc6 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,22 @@ 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 returns a native `button` or `a`, Pagination reuses +that control instead of wrapping it. Native buttons use browser Enter/Space +activation; links with `href` retain link behavior. Other custom content keeps +its existing delegated interaction for compatibility. Custom components should +provide their own accessible control semantics; Pagination cannot assume that +they forward DOM props or safely wrap them in another interactive element. + +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 diff --git a/assets/index.less b/assets/index.less index 64ceac6d..f1c57c2a 100644 --- a/assets/index.less +++ b/assets/index.less @@ -83,6 +83,7 @@ } &:focus, + &:focus-within, &:hover { border-color: @primary-color; transition: all 0.3s; @@ -101,11 +102,13 @@ } &:focus, + &:focus-within, &:hover { border-color: #40a9ff; } &:focus a, + &:focus-within a, &:hover a { color: #40a9ff; } diff --git a/package.json b/package.json index 2768693b..fd7c42c3 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/Pager.tsx b/src/Pager.tsx index 5a512afd..b7b3b3c5 100644 --- a/src/Pager.tsx +++ b/src/Pager.tsx @@ -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 { rootPrefixCls: string; @@ -11,6 +12,7 @@ export interface PagerProps extends Pick { className?: string; style?: React.CSSProperties; showTitle: boolean; + disabled?: boolean; onClick?: (page: number) => void; onKeyPress?: ( e: React.KeyboardEvent, @@ -28,6 +30,7 @@ const Pager: React.FC = (props) => { className, style, showTitle, + disabled, onClick, onKeyPress, itemRender, @@ -52,29 +55,23 @@ const Pager: React.FC = (props) => { onKeyPress(e, onClick, page); }; - const pager = itemRender( - page, - 'page', - , - ); + const pager = itemRender(page, 'page', {page}); const pagerLabel = `${pageLabel} ${page}`.trim(); return pager ? ( -
  • {pager} -
  • + ) : null; }; diff --git a/src/Pagination.tsx b/src/Pagination.tsx index 2ca06bd8..b88499f9 100644 --- a/src/Pagination.tsx +++ b/src/Pagination.tsx @@ -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; @@ -126,8 +127,6 @@ const Pagination: React.FC = (props) => { ) : (
  • @@ -1936,7 +1713,6 @@ exports[`Example itemRender 1`] = ` aria-disabled="false" aria-label="下一页" class="rc-pagination-next" - role="button" tabindex="0" >
    @@ -1960,134 +1736,114 @@ exports[`Example jumper 1`] = ` >
  • @@ -2839,7 +2547,6 @@ exports[`Example lessPages 1`] = `
  • @@ -2881,7 +2586,6 @@ exports[`Example lessPages 1`] = ` aria-disabled="false" aria-label="下一页" class="rc-pagination-next" - role="button" tabindex="0" >
  • @@ -2981,7 +2677,6 @@ exports[`Example lessPages 1`] = `
  • @@ -3023,7 +2716,6 @@ exports[`Example lessPages 1`] = ` aria-disabled="false" aria-label="下一页" class="rc-pagination-next" - role="button" tabindex="0" >
  • @@ -3845,12 +3452,11 @@ exports[`Example showTitle 1`] = ` >
  • @@ -3961,12 +3554,11 @@ exports[`Example showTotal 1`] = `
  • + ), + nextIcon: ( + + ), + jumpPrevIcon: ( + + Previous jump icon + + ), + jumpNextIcon: ( + + Next jump icon + + ), +}; + +describe('accessible pagination controls', () => { + it.each([ + { total: 1000, defaultCurrent: 50 }, + { total: 1000, defaultCurrent: 50, ...antdIcons }, + { total: 1000, defaultCurrent: 1 }, + { total: 1000, defaultCurrent: 100 }, + { total: 1000, defaultCurrent: 50, disabled: true, ...antdIcons }, + { total: 1000, defaultCurrent: 1, simple: true }, + { total: 1000, defaultCurrent: 100, simple: { readOnly: true } }, + { total: 0 }, + { + total: 1000, + defaultCurrent: 50, + itemRender: (_, __, element) => element, + }, + { + total: 1000, + defaultCurrent: 50, + itemRender: (page) => , + }, + { + total: 1000, + defaultCurrent: 50, + itemRender: (page) => {page}, + }, + ])('passes axe for %j', async (props) => { + const { container } = render( {}} />); + await checkAccessibility(container); + container.querySelectorAll('li').forEach((item) => { + expect(item).not.toHaveAttribute('role', 'button'); + expect(item).not.toHaveAttribute('tabindex'); + }); + expect(container.querySelector('button button')).toBeNull(); + }); + + it.each(['prev', 'next', 'jump-prev', 'jump-next', 'item-51'])( + 'activates %s exactly once for each mouse/Enter/Space action', + async (type) => { + const onChange = jest.fn(); + const { container } = render( + , + ); + const control = container.querySelector( + `.rc-pagination-${type} > :is(button, a)`, + ); + control.focus(); + await userEvent.keyboard('{Enter}'); + expect(onChange).toHaveBeenCalledTimes(1); + await userEvent.keyboard(' '); + expect(onChange).toHaveBeenCalledTimes(type === 'item-51' ? 1 : 2); + // Selecting the current page is intentionally a no-op. + if (type !== 'item-51') { + await userEvent.click(control); + expect(onChange).toHaveBeenCalledTimes(3); + } + }, + ); + + it('has one focus target per item and exposes its current/disabled state', async () => { + const { container } = render( {}} />); + await userEvent.tab(); + expect(container.querySelector('.rc-pagination-item-1 a')).toHaveFocus(); + await userEvent.tab(); + expect(container.querySelector('.rc-pagination-item-2 a')).toHaveFocus(); + expect(container.querySelector('.rc-pagination-item-1 a')).toHaveAttribute( + 'aria-current', + 'page', + ); + expect( + container.querySelector('.rc-pagination-prev button'), + ).toBeDisabled(); + }); + + it('prevents navigation from a disabled custom link', () => { + const onChange = jest.fn(); + const { getByRole } = render( + {page}} + />, + ); + expect(fireEvent.click(getByRole('link', { name: '页 2' }))).toBe(false); + expect(onChange).not.toHaveBeenCalled(); + }); + + it('preserves delegated keyboard behavior for opaque custom content', () => { + const onChange = jest.fn(); + const { container } = render( + {page}} + />, + ); + const item = container.querySelector('.rc-pagination-item-2'); + expect(item).toHaveAttribute('tabindex', '0'); + fireEvent.keyDown(item, { key: 'Enter' }); + expect(onChange).toHaveBeenCalledTimes(1); + }); + it('composes custom click handlers without duplicate activation', async () => { + const onClick = jest.fn(); + const onChange = jest.fn(); + const { container } = render( + } + />, + ); + const control = container.querySelector( + '.rc-pagination-item-2 button', + ); + control.focus(); + await userEvent.keyboard('{Enter}'); + expect(onClick).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenCalledTimes(1); + }); + + it('respects custom controls that stop propagation or are disabled', async () => { + const onChange = jest.fn(); + const { container } = render( + ( + + )} + />, + ); + await userEvent.click( + container.querySelector('.rc-pagination-item-2 button'), + ); + expect(onChange).not.toHaveBeenCalled(); + expect( + container.querySelector('.rc-pagination-item-3 button'), + ).toBeDisabled(); + }); +}); diff --git a/tests/index.test.tsx b/tests/index.test.tsx index 7cba57e2..127f46eb 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -1,6 +1,7 @@ import type { RenderResult } from '@testing-library/react'; import { render, fireEvent } from '@testing-library/react'; import React from 'react'; +import userEvent from '@testing-library/user-event'; import Pagination from '../src'; import { resetWarned } from '@rc-component/util'; import { sizeChangerRender } from './commonUtil'; @@ -21,7 +22,7 @@ describe('Default Pagination', () => { it('onChange should be forbidden when total is default', () => { const pagers = $$('.rc-pagination-item'); - fireEvent.click(pagers[0]); + fireEvent.click(pagers[0].querySelector('button, a') || pagers[0]); expect(onChange).not.toHaveBeenCalled(); }); }); @@ -66,10 +67,9 @@ describe('Uncontrolled Pagination', () => { wrapper.container.querySelector('.rc-pagination-item-active'), ).toHaveTextContent('1'); expect($$('.rc-pagination-item')[0]).toHaveTextContent('1'); - expect($$('.rc-pagination-item')[0]).toHaveAttribute( - 'aria-current', - 'page', - ); + expect( + $$('.rc-pagination-item')[0].querySelector('button, a'), + ).toHaveAttribute('aria-current', 'page'); expect($$('.rc-pagination-item')[0]).toHaveAttribute('title', '1'); }); @@ -98,14 +98,18 @@ describe('Uncontrolled Pagination', () => { }); it('should expose role and label for navigation items', () => { - const prevButton = wrapper.container.querySelector('.rc-pagination-prev'); - const nextButton = wrapper.container.querySelector('.rc-pagination-next'); + const prevButton = wrapper.container.querySelector( + '.rc-pagination-prev button', + ); + const nextButton = wrapper.container.querySelector( + '.rc-pagination-next button', + ); const currentPage = wrapper.container.querySelector( - '.rc-pagination-item-active', + '.rc-pagination-item-active a', ); - expect(prevButton).toHaveAttribute('role', 'button'); + expect(prevButton.tagName).toBe('BUTTON'); expect(prevButton).toHaveAttribute('aria-label'); - expect(nextButton).toHaveAttribute('role', 'button'); + expect(nextButton.tagName).toBe('BUTTON'); expect(nextButton).toHaveAttribute('aria-label'); expect(currentPage).toHaveAttribute('role', 'button'); expect(currentPage).toHaveAttribute('aria-current', 'page'); @@ -116,7 +120,7 @@ describe('Uncontrolled Pagination', () => { expect(pagers).toHaveLength(3); const page2 = pagers[1]; expect(page2).toHaveClass('rc-pagination-item-2'); - fireEvent.click(page2); + fireEvent.click(page2.querySelector('button, a') || page2); expect( wrapper.container.querySelector('.rc-pagination-item-active'), ).toHaveTextContent('2'); @@ -126,7 +130,7 @@ describe('Uncontrolled Pagination', () => { it('should response next page', () => { const nextButton = wrapper.container.querySelector('.rc-pagination-next'); - fireEvent.click(nextButton); + fireEvent.click(nextButton.querySelector('button, a') || nextButton); expect( wrapper.container.querySelector('.rc-pagination-item-active'), ).toHaveTextContent('2'); @@ -197,9 +201,9 @@ describe('Uncontrolled Pagination', () => { ); expect(totalText).toHaveTextContent('1 - 10 of 25 items'); const nextButton = wrapper.container.querySelector('.rc-pagination-next'); - fireEvent.click(nextButton); + fireEvent.click(nextButton.querySelector('button, a') || nextButton); expect(totalText).toHaveTextContent('11 - 20 of 25 items'); - fireEvent.click(nextButton); + fireEvent.click(nextButton.querySelector('button, a') || nextButton); expect(totalText).toHaveTextContent('21 - 25 of 25 items'); }); @@ -213,9 +217,10 @@ describe('Uncontrolled Pagination', () => { warnSpy.mockRestore(); }); - it('should response keyboard event', () => { + it('should response keyboard event', async () => { const pagers = $$('.rc-pagination-item'); - fireEvent.keyDown(pagers[2], { key: 'Enter', keyCode: 13, which: 13 }); + pagers[2].querySelector('button, a').focus(); + await userEvent.keyboard('{Enter}'); expect( wrapper.container.querySelector('.rc-pagination-item-active'), ).toHaveTextContent('3'); @@ -250,7 +255,7 @@ describe('Controlled Pagination', () => { it('should not response mouse click', () => { const nextButton = wrapper.container.querySelector('.rc-pagination-next'); - fireEvent.click(nextButton); + fireEvent.click(nextButton.querySelector('button, a') || nextButton); expect( wrapper.container.querySelector('.rc-pagination-item-active'), ).toHaveTextContent('2'); @@ -611,7 +616,7 @@ 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]); + fireEvent.click(pagers[0].querySelector('button, a') || pagers[0]); expect(onChange).toHaveBeenCalledWith(3, 10); }); }); @@ -619,7 +624,12 @@ 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 $ = (selector: string) => + wrapper.container.querySelector(selector + ' > :is(button, a)'); + async function activate(control: Element, key: string) { + (control as HTMLElement).focus(); + await userEvent.keyboard(key); + } beforeEach(() => { wrapper = render( @@ -632,105 +642,101 @@ describe('keyboard support', () => { onChange.mockReset(); }); - it('should work for prev page', () => { + it('should work for prev page', async () => { const prevButton = $('li.rc-pagination-prev'); expect(prevButton).toBeTruthy(); - fireEvent.click(prevButton); - fireEvent.click(prevButton); + fireEvent.click(prevButton.querySelector('button, a') || prevButton); + fireEvent.click(prevButton.querySelector('button, a') || prevButton); - fireEvent.keyDown(prevButton, { key: 'Enter', keyCode: 13, which: 13 }); - fireEvent.keyDown(prevButton, { key: 'Enter', keyCode: 13, which: 13 }); + await activate(prevButton, '{Enter}'); + await activate(prevButton, '{Enter}'); expect(onChange).toHaveBeenLastCalledWith(46, 10); }); - it('should work for prev page with space key', () => { + it('should work for prev page with space key', async () => { const prevButton = $('li.rc-pagination-prev'); expect(prevButton).toBeTruthy(); - fireEvent.keyDown(prevButton, { key: ' ', keyCode: 32, which: 32 }); - fireEvent.keyDown(prevButton, { key: 'Spacebar', keyCode: 32, which: 32 }); + await activate(prevButton, ' '); + await activate(prevButton, ' '); expect(onChange).toHaveBeenLastCalledWith(48, 10); }); - it('should work for next page', () => { + it('should work for next page', async () => { const nextButton = $('li.rc-pagination-next'); expect(nextButton).toBeTruthy(); - fireEvent.keyDown(nextButton, { key: 'Enter', keyCode: 13, which: 13 }); - fireEvent.keyDown(nextButton, { key: 'Enter', keyCode: 13, which: 13 }); + await activate(nextButton, '{Enter}'); + await activate(nextButton, '{Enter}'); - fireEvent.click(nextButton); - fireEvent.click(nextButton); + fireEvent.click(nextButton.querySelector('button, a') || nextButton); + fireEvent.click(nextButton.querySelector('button, a') || nextButton); expect(onChange).toHaveBeenLastCalledWith(54, 10); }); - it('should work for next page with space key', () => { + it('should work for next page with space key', async () => { const nextButton = $('li.rc-pagination-next'); expect(nextButton).toBeTruthy(); - fireEvent.keyDown(nextButton, { key: ' ', keyCode: 32, which: 32 }); - fireEvent.keyDown(nextButton, { key: 'Spacebar', keyCode: 32, which: 32 }); + await activate(nextButton, ' '); + await activate(nextButton, ' '); expect(onChange).toHaveBeenLastCalledWith(52, 10); }); - it('should work for next page with enter keyCode only', () => { + it('should work for next page with native Enter activation', async () => { const nextButton = $('li.rc-pagination-next'); expect(nextButton).toBeTruthy(); - fireEvent.keyDown(nextButton, { keyCode: 13, which: 13 }); + await activate(nextButton, '{Enter}'); expect(onChange).toHaveBeenLastCalledWith(51, 10); }); - it('should work for jump prev page', () => { + it('should work for jump prev page', async () => { const jumpPrevButton = $('li.rc-pagination-jump-prev'); expect(jumpPrevButton).toBeTruthy(); - fireEvent.keyDown(jumpPrevButton, { key: 'Enter', keyCode: 13, which: 13 }); - fireEvent.click(jumpPrevButton); + await activate(jumpPrevButton, '{Enter}'); + fireEvent.click( + jumpPrevButton.querySelector('button, a') || jumpPrevButton, + ); expect(onChange).toHaveBeenLastCalledWith(40, 10); }); - it('should work for jump prev page with space key', () => { + it('should work for jump prev page with space key', async () => { const jumpPrevButton = $('li.rc-pagination-jump-prev'); expect(jumpPrevButton).toBeTruthy(); - fireEvent.keyDown(jumpPrevButton, { key: ' ', keyCode: 32, which: 32 }); - fireEvent.keyDown(jumpPrevButton, { - key: 'Spacebar', - keyCode: 32, - which: 32, - }); + await activate(jumpPrevButton, ' '); + await activate(jumpPrevButton, ' '); expect(onChange).toHaveBeenLastCalledWith(40, 10); }); - it('should work for jump next page', () => { + it('should work for jump next page', async () => { const jumpNextButton = $('li.rc-pagination-jump-next'); expect(jumpNextButton).toBeTruthy(); - fireEvent.click(jumpNextButton); - fireEvent.keyDown(jumpNextButton, { key: 'Enter', keyCode: 13, which: 13 }); + fireEvent.click( + jumpNextButton.querySelector('button, a') || jumpNextButton, + ); + await activate(jumpNextButton, '{Enter}'); expect(onChange).toHaveBeenLastCalledWith(60, 10); }); - it('should work for jump next page with space key', () => { + it('should work for jump next page with space key', async () => { const jumpNextButton = $('li.rc-pagination-jump-next'); expect(jumpNextButton).toBeTruthy(); - fireEvent.keyDown(jumpNextButton, { key: ' ', keyCode: 32, which: 32 }); - fireEvent.keyDown(jumpNextButton, { - key: 'Spacebar', - keyCode: 32, - which: 32, - }); + await activate(jumpNextButton, ' '); + await activate(jumpNextButton, ' '); expect(onChange).toHaveBeenLastCalledWith(60, 10); }); diff --git a/tests/itemRender.test.tsx b/tests/itemRender.test.tsx index 50c426f2..b9884b80 100644 --- a/tests/itemRender.test.tsx +++ b/tests/itemRender.test.tsx @@ -26,21 +26,31 @@ describe('itemRender', () => { const jumpNext = $$('.rc-pagination-jump-next'); const active = $$('.rc-pagination-item-active'); - expect(prev.innerHTML).toBe( - `${currentPage - 1}`, + [prev, next, jumpPrev, jumpNext, active].forEach((item) => { + const link = item.querySelector('a'); + expect(link).toHaveAttribute('href'); + expect(link).toHaveAttribute('aria-label'); + expect(link).toHaveAttribute('tabindex', '0'); + expect(item).not.toHaveAttribute('tabindex'); + expect(item).not.toHaveAttribute('role'); + }); + expect(prev.querySelector('a')).toHaveAttribute( + 'href', + `#${currentPage - 1}`, ); - expect(next.innerHTML).toBe( - `${currentPage + 1}`, + expect(next.querySelector('a')).toHaveAttribute( + 'href', + `#${currentPage + 1}`, ); - expect(jumpPrev.innerHTML).toBe( - `${currentPage - 5}`, + expect(jumpPrev.querySelector('a')).toHaveAttribute( + 'href', + `#${currentPage - 5}`, ); - expect(jumpNext.innerHTML).toBe( - `${currentPage + 5}`, - ); - expect(active.innerHTML).toBe( - `${currentPage}`, + expect(jumpNext.querySelector('a')).toHaveAttribute( + 'href', + `#${currentPage + 5}`, ); + expect(active.querySelector('a')).toHaveAttribute('aria-current', 'page'); }); it('should support empty custom itemRender', () => { @@ -105,7 +115,9 @@ describe('itemRender', () => { ); const prev = container.querySelector('.rc-pagination-prev'); const next = container.querySelector('.rc-pagination-next'); - expect(prev.innerHTML).toBe('0'); - expect(next.innerHTML).toBe('2'); + expect(prev.querySelector('a')).toHaveAttribute('aria-disabled', 'true'); + expect(prev.querySelector('a')).toHaveAttribute('tabindex', '-1'); + expect(prev.querySelector('a')).not.toHaveAttribute('disabled'); + expect(next.querySelector('a')).toHaveAttribute('aria-disabled', 'false'); }); }); diff --git a/tests/jumper.test.tsx b/tests/jumper.test.tsx index f87dbcb7..71825bbc 100644 --- a/tests/jumper.test.tsx +++ b/tests/jumper.test.tsx @@ -186,7 +186,7 @@ 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'); - fireEvent.click(nextButton); + fireEvent.click(nextButton.querySelector('button, a')); const input = wrapper.container.querySelector('input'); fireEvent.change(input, { target: { value: '&' } }); fireEvent.keyUp(input, { key: 'Enter', keyCode: 13, which: 13 }); From 4144185ad91e5fe9f5d06453ec3f3b8b213f1e42 Mon Sep 17 00:00:00 2001 From: yoyo837 Date: Wed, 14 Oct 2026 22:46:32 +0800 Subject: [PATCH 2/2] fix: preserve quick-jump behavior when focusing inner controls --- src/Options.tsx | 5 ++-- tests/accessibility.test.tsx | 48 ++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/Options.tsx b/src/Options.tsx index aa570dc0..45bcb2c6 100644 --- a/src/Options.tsx +++ b/src/Options.tsx @@ -73,8 +73,9 @@ const Options: React.FC = (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; } diff --git a/tests/accessibility.test.tsx b/tests/accessibility.test.tsx index f43e68be..66cca984 100644 --- a/tests/accessibility.test.tsx +++ b/tests/accessibility.test.tsx @@ -188,4 +188,52 @@ describe('accessible pagination controls', () => { container.querySelector('.rc-pagination-item-3 button'), ).toBeDisabled(); }); + it.each(['prev', 'next', 'jump-prev', 'jump-next'])( + 'retains Enter/Space handling for opaque %s content', + (type) => { + const onChange = jest.fn(); + const { container } = render( + {page}} + />, + ); + const item = container.querySelector(`.rc-pagination-${type}`); + fireEvent.keyDown(item, { key: 'Enter' }); + expect(onChange).toHaveBeenCalledTimes(1); + fireEvent.keyDown(item, { key: ' ' }); + expect(onChange).toHaveBeenCalledTimes(2); + }, + ); + + it('allows a custom anchor to cancel delegated keyboard activation', () => { + const onChange = jest.fn(); + const onKeyDown = jest.fn((event: React.KeyboardEvent) => + event.preventDefault(), + ); + const { container } = render( + {page}} + />, + ); + fireEvent.keyDown(container.querySelector('.rc-pagination-item-2 a'), { + key: 'Enter', + }); + expect(onKeyDown).toHaveBeenCalledTimes(1); + expect(onChange).not.toHaveBeenCalled(); + }); + it('does not submit the quick-jump input when focusing a page control', async () => { + const onChange = jest.fn(); + const { getByRole } = render( + , + ); + await userEvent.type(getByRole('textbox'), '3'); + await userEvent.click(getByRole('button', { name: '页 2' })); + expect(onChange).toHaveBeenCalledTimes(1); + expect(onChange).toHaveBeenLastCalledWith(2, 10); + }); });