From c1f51a89250944e723bb214748059497d1376980 Mon Sep 17 00:00:00 2001 From: domdom Date: Wed, 28 Oct 2026 11:52:10 +0100 Subject: [PATCH] fix(a11y): toggle panel with Space and ignore keys from nested controls The header (role="button" or "tab") only toggled on Enter (WCAG 2.2 SC 2.1.1). Space now toggles it without scrolling the page. Key events from nested controls, such as a button in `extra`, are left to those controls. Based on #457. ref ant-design/ant-design#59684 Co-authored-by: Ferit Cem <92086876+feritcemkahraman@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 --- src/Panel.tsx | 8 ++++++++ tests/index.spec.tsx | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/Panel.tsx b/src/Panel.tsx index 9373570..6c17c2b 100644 --- a/src/Panel.tsx +++ b/src/Panel.tsx @@ -39,8 +39,16 @@ const CollapsePanel = React.forwardRef((prop onItemClick?.(panelKey); }, onKeyDown: (e: React.KeyboardEvent) => { + // Leave key events from nested controls (e.g. a button in `extra`) to those controls + if (e.target !== e.currentTarget) { + return; + } if (e.key === 'Enter' || e.keyCode === KeyCode.ENTER || e.which === KeyCode.ENTER) { onItemClick?.(panelKey); + } else if (e.key === ' ') { + // Prevent the page from scrolling, like a native button + e.preventDefault(); + onItemClick?.(panelKey); } }, role: accordion ? 'tab' : 'button', diff --git a/tests/index.spec.tsx b/tests/index.spec.tsx index c91c7d9..7c5d420 100644 --- a/tests/index.spec.tsx +++ b/tests/index.spec.tsx @@ -445,6 +445,47 @@ describe('collapse', () => { ); }); + it('should toggle panel when press space', () => { + const { container } = render( + + + first + + + second + + , + ); + const [header, disabledHeader] = container.querySelectorAll('.rc-collapse-header'); + + fireEvent.keyDown(disabledHeader, { key: ' ' }); + jest.runAllTimers(); + expect(container.querySelectorAll('.rc-collapse-panel-active')).toHaveLength(0); + + // `fireEvent` returns false when the default action (page scroll) is prevented + expect(fireEvent.keyDown(header, { key: ' ' })).toBe(false); + jest.runAllTimers(); + expect(container.querySelector('.rc-collapse-panel')).toHaveClass('rc-collapse-panel-active'); + + fireEvent.keyDown(header, { key: ' ' }); + jest.runAllTimers(); + expect(container.querySelectorAll('.rc-collapse-panel-active')).toHaveLength(0); + }); + + it('should leave keyboard events on a nested extra button to the button', () => { + const { getByRole, container } = render( + Action, children: 'Content' }]} + />, + ); + const button = getByRole('button', { name: 'Action' }); + + expect(fireEvent.keyDown(button, { key: ' ' })).toBe(true); + fireEvent.keyDown(button, { key: 'Enter', keyCode: KeyCode.ENTER }); + jest.runAllTimers(); + expect(container.querySelectorAll('.rc-collapse-panel-active')).toHaveLength(0); + }); + it('should link the header to its panel with aria-controls', () => { const { container } = render(