From bc48aeb4d56549dae3b093f70698f7141fa84a20 Mon Sep 17 00:00:00 2001 From: domdom Date: Tue, 27 Oct 2026 19:44:49 +0100 Subject: [PATCH 1/2] fix(a11y): link panel header to its content with aria-controls The header (role="button" or "tab") was not linked to the panel it controls (WCAG 2.2 SC 4.1.2). aria-controls is only set while the panel is expanded, because collapsed content is not mounted. ref ant-design/ant-design#59684 Co-Authored-By: Claude Opus 5.5 --- src/Panel.tsx | 5 +++++ src/PanelContent.tsx | 2 ++ tests/__snapshots__/index.spec.tsx.snap | 2 +- tests/index.spec.tsx | 29 +++++++++++++++++++++++++ 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/Panel.tsx b/src/Panel.tsx index c489ee2c..49f80f4d 100644 --- a/src/Panel.tsx +++ b/src/Panel.tsx @@ -32,6 +32,8 @@ const CollapsePanel = React.forwardRef((prop const ifExtraExist = isNonNullable(extra) && typeof extra !== 'boolean'; + const panelId = React.useId(); + const collapsibleProps = { onClick: () => { onItemClick?.(panelKey); @@ -43,6 +45,8 @@ const CollapsePanel = React.forwardRef((prop }, role: accordion ? 'tab' : 'button', ['aria-expanded']: isActive, + // The panel content is not mounted while collapsed, so only reference it when expanded + ['aria-controls']: isActive ? panelId : undefined, ['aria-disabled']: disabled, tabIndex: disabled ? -1 : 0, }; @@ -117,6 +121,7 @@ const CollapsePanel = React.forwardRef((prop styles={styles} isActive={isActive} forceRender={forceRender} + id={panelId} role={accordion ? 'tabpanel' : undefined} > {children} diff --git a/src/PanelContent.tsx b/src/PanelContent.tsx index 81d88b81..a38f9655 100644 --- a/src/PanelContent.tsx +++ b/src/PanelContent.tsx @@ -13,6 +13,7 @@ const PanelContent = React.forwardRef< style, children, isActive, + id, role, classNames: customizeClassNames, styles, @@ -42,6 +43,7 @@ const PanelContent = React.forwardRef< className, )} style={style} + id={id} role={role} >
diff --git a/tests/__snapshots__/index.spec.tsx.snap b/tests/__snapshots__/index.spec.tsx.snap index 8c6cefc1..8e1a4565 100644 --- a/tests/__snapshots__/index.spec.tsx.snap +++ b/tests/__snapshots__/index.spec.tsx.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing +// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`collapse props items should work with nested 1`] = `
{ ); }); + it('should link the header to its panel with aria-controls', () => { + const { container } = render( + + + first + + , + ); + const panelId = container.querySelector('.rc-collapse-panel')!.id; + + expect(panelId).toBeTruthy(); + expect(container.querySelector('.rc-collapse-header')).toHaveAttribute( + 'aria-controls', + panelId, + ); + }); + + it('should not set aria-controls on a collapsed header', () => { + const { container } = render( + + + first + + , + ); + + expect(container.querySelector('.rc-collapse-header')).not.toHaveAttribute('aria-controls'); + }); + describe('wrapped in Fragment', () => { const expandIcon = () => test{'>'}; const element = ( From a51e11d3cc8bd92efb42ff8a0c1ec8a856a7fb37 Mon Sep 17 00:00:00 2001 From: domdom Date: Wed, 28 Oct 2026 09:52:13 +0100 Subject: [PATCH 2/2] refactor: use useId from @rc-component/util for React 16/17 compatibility Co-Authored-By: Claude Opus 5.5 --- src/Panel.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Panel.tsx b/src/Panel.tsx index 49f80f4d..9373570f 100644 --- a/src/Panel.tsx +++ b/src/Panel.tsx @@ -1,6 +1,6 @@ import { clsx } from 'clsx'; import CSSMotion from '@rc-component/motion'; -import { isNonNullable, isReactRenderable, KeyCode } from '@rc-component/util'; +import { isNonNullable, isReactRenderable, KeyCode, useId } from '@rc-component/util'; import React from 'react'; import type { CollapsePanelProps } from './interface'; import PanelContent from './PanelContent'; @@ -32,7 +32,7 @@ const CollapsePanel = React.forwardRef((prop const ifExtraExist = isNonNullable(extra) && typeof extra !== 'boolean'; - const panelId = React.useId(); + const panelId = useId(); const collapsibleProps = { onClick: () => {