diff --git a/assets/index.less b/assets/index.less index 4093e0f..d20d87f 100644 --- a/assets/index.less +++ b/assets/index.less @@ -39,7 +39,8 @@ border-top: none; } - > .@{prefixCls}-header { + > .@{prefixCls}-header, + > .@{prefixCls}-header-wrapper > .@{prefixCls}-header { display: flex; align-items: center; line-height: 22px; @@ -76,10 +77,14 @@ } } - & > &-item-disabled > .@{prefixCls}-header { - cursor: not-allowed; - color: #999; - background-color: #f3f3f3; + + & > &-item-disabled { + > .@{prefixCls}-header, + > .@{prefixCls}-header-wrapper > .@{prefixCls}-header { + cursor: not-allowed; + color: #999; + background-color: #f3f3f3; + } } &-panel { @@ -105,7 +110,8 @@ } & > &-item-active { - > .@{prefixCls}-header { + > .@{prefixCls}-header, + > .@{prefixCls}-header-wrapper > .@{prefixCls}-header { .arrow { position: relative; top: 2px; diff --git a/src/Collapse.tsx b/src/Collapse.tsx index 1b0b099..3456915 100644 --- a/src/Collapse.tsx +++ b/src/Collapse.tsx @@ -6,6 +6,7 @@ import useItems from './hooks/useItems'; import type { CollapseProps } from './interface'; import CollapsePanel from './Panel'; import pickAttrs from '@rc-component/util/lib/pickAttrs'; +import useId from '@rc-component/util/lib/hooks/useId'; function getActiveKeysArray(activeKey: React.Key | React.Key[]) { let currentActiveKey = activeKey; @@ -34,8 +35,11 @@ const Collapse = React.forwardRef((props, ref) => items, classNames: customizeClassNames, styles, + headingLevel, + id, } = props; + const collapseId = useId(id); const collapseClassName = classNames(prefixCls, className); const [activeKey, setActiveKey] = useMergedState([], { @@ -77,6 +81,8 @@ const Collapse = React.forwardRef((props, ref) => activeKey, classNames: customizeClassNames, styles, + headingLevel, + parentId: collapseId, }); // ======================== Render ======================== @@ -87,6 +93,7 @@ const Collapse = React.forwardRef((props, ref) => style={style} role={accordion ? 'tablist' : undefined} {...pickAttrs(props, { aria: true, data: true })} + id={collapseId} > {mergedChildren} diff --git a/src/Panel.tsx b/src/Panel.tsx index f5e7bb2..5e70920 100644 --- a/src/Panel.tsx +++ b/src/Panel.tsx @@ -25,6 +25,8 @@ const CollapsePanel = React.forwardRef((prop openMotion, destroyOnHidden, children, + headingLevel, + id, ...resetProps } = props; @@ -87,17 +89,23 @@ const CollapsePanel = React.forwardRef((prop // ======================== Render ======================== return ( -
-
- {showArrow && iconNode} - - {header} - - {ifExtraExist &&
{extra}
} +
+
+
+ {showArrow && iconNode} + + {header} + + {ifExtraExist &&
{extra}
} +
((prop return (
& Pick & { activeKey: React.Key[]; + parentId?: string; }; const convertItemsToNodes = (items: ItemType[], props: Props) => { @@ -23,6 +30,8 @@ const convertItemsToNodes = (items: ItemType[], props: Props) => { expandIcon, classNames: collapseClassNames, styles, + headingLevel, + parentId, } = props; return items.map((item, index) => { @@ -73,6 +82,8 @@ const convertItemsToNodes = (items: ItemType[], props: Props) => { collapsible={mergeCollapsible} onItemClick={handleItemClick} destroyOnHidden={mergedDestroyOnHidden} + headingLevel={headingLevel} + id={`${parentId}__item-${key}`} > {children} @@ -103,6 +114,8 @@ const getNewChild = ( expandIcon, classNames: collapseClassNames, styles, + headingLevel, + parentId, } = props; const key = child.key || String(index); @@ -148,6 +161,8 @@ const getNewChild = ( onItemClick: handleItemClick, expandIcon, collapsible: mergeCollapsible, + headingLevel, + id: `${parentId}__item-${key}`, }; // https://github.com/ant-design/ant-design/issues/20479 diff --git a/src/interface.ts b/src/interface.ts index b420b7e..25a4a48 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -2,6 +2,7 @@ import type { CSSMotionProps } from '@rc-component/motion'; import type * as React from 'react'; export type CollapsibleType = 'header' | 'icon' | 'disabled'; +export type HeadingLevelType = 1 | 2 | 3 | 4 | 5 | 6; export interface ItemType extends Omit< @@ -39,6 +40,8 @@ export interface CollapseProps { items?: ItemType[]; classNames?: Partial>; styles?: Partial>; + headingLevel?: HeadingLevelType; + id?: string; } export type SemanticName = 'header' | 'title' | 'body' | 'icon'; @@ -65,4 +68,5 @@ export interface CollapsePanelProps extends React.DOMAttributes role?: string; collapsible?: CollapsibleType; children?: React.ReactNode; + headingLevel?: HeadingLevelType; } diff --git a/tests/__snapshots__/index.spec.tsx.snap b/tests/__snapshots__/index.spec.tsx.snap index 8e1a456..2c0dc15 100644 --- a/tests/__snapshots__/index.spec.tsx.snap +++ b/tests/__snapshots__/index.spec.tsx.snap @@ -3,108 +3,137 @@ exports[`collapse props items should work with nested 1`] = `
diff --git a/tests/index.spec.tsx b/tests/index.spec.tsx index f0d8117..659b6d5 100644 --- a/tests/index.spec.tsx +++ b/tests/index.spec.tsx @@ -3,7 +3,7 @@ import { fireEvent, render } from '@testing-library/react'; import KeyCode from '@rc-component/util/lib/KeyCode'; import React, { Fragment } from 'react'; import Collapse, { Panel } from '../src/index'; -import type { CollapseProps, ItemType } from '../src/interface'; +import type { CollapseProps, HeadingLevelType, ItemType } from '../src/interface'; describe('collapse', () => { let changeHook: jest.Mock | null; @@ -79,11 +79,11 @@ describe('collapse', () => { }); it('click should toggle panel state', () => { - const header = collapse.container.querySelectorAll('.rc-collapse-header')?.[1]; - fireEvent.click(header); + const getHeader = () => collapse.container.querySelectorAll('.rc-collapse-header')?.[1]; + fireEvent.click(getHeader()); jest.runAllTimers(); expect(collapse.container.querySelectorAll('.rc-collapse-panel-active')).toHaveLength(1); - fireEvent.click(header); + fireEvent.click(getHeader()); jest.runAllTimers(); expect(collapse.container.querySelector('.rc-collapse-panel-inactive')?.innerHTML).toBe( '
second
', @@ -205,6 +205,136 @@ describe('collapse', () => { }); }); + describe('prop: id', () => { + const runIdTest = (element: any, id?: string) => { + const { container } = render(element); + let testId = id; + if (!testId) { + testId = container.querySelector('.rc-collapse').getAttribute('id'); + expect(testId).toBeTruthy(); + } else { + expect(container.querySelector(`[id="${testId}"]`)).toHaveClass('rc-collapse'); + } + + expect(container.querySelector(`[id="${testId}__item-1"]`)).toHaveClass('rc-collapse-item'); + + const header = container.querySelector(`[id="${testId}__item-1__header"]`); + expect(header).toHaveClass('rc-collapse-header'); + expect(header).toHaveAttribute('aria-controls', `${testId}__item-1__content`); + + fireEvent.click(header); + jest.runAllTimers(); + + const panelContent = container.querySelector(`[id="${testId}__item-1__content"]`); + expect(panelContent).toHaveClass('rc-collapse-panel'); + expect(panelContent).toHaveAttribute('aria-labelledby', `${testId}__item-1__header`); + }; + + it('applies default id to subcomponents - using composition', () => { + const element = ( + + + first + + + ); + + runIdTest(element); + }); + + it('applies the passed id to subcomponents - using composition', () => { + const element = ( + + + first + + + ); + + runIdTest(element, 'collapse-test-id'); + }); + + it('applies default id to subcomponents - using items prop', () => { + const element = ( + + ); + + runIdTest(element); + }); + + it('applies the passed id to subcomponents - using items prop', () => { + const element = ( + + ); + + runIdTest(element, 'collapse-test-id'); + }); + }); + + describe('prop: headingLevel', () => { + const runHeadingLevelTest = (element: any, headingLevel: HeadingLevelType) => { + const { container } = render(element); + const header = container.querySelector('.rc-collapse-header'); + expect(header.parentElement.tagName).toEqual('DIV'); + if (headingLevel) { + expect(Number(header.parentElement.getAttribute('aria-level'))).toEqual(headingLevel); + } + }; + + const headingElements: HeadingLevelType[] = [1, 2, 3, 4, 5, 6, undefined]; + test.each(headingElements)( + 'correctly creates element when headingLevel=%p - using composition', + (headingLevel) => { + const element = ( + + + first + + + ); + + runHeadingLevelTest(element, headingLevel); + }, + ); + + test.each(headingElements)( + 'correctly creates element when headingLevel=%p - using items prop', + (headingLevel) => { + const element = ( + + ); + + runHeadingLevelTest(element, headingLevel); + }, + ); + }); + it('should support extra whit number 0', () => { const { container } = render( @@ -808,6 +938,7 @@ describe('collapse', () => { it('should work with nested', () => { const { container } = render(