diff --git a/.umirc.ts b/.umirc.ts
deleted file mode 100644
index 19135896..00000000
--- a/.umirc.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-// more config: https://d.umijs.org/config
-import { defineConfig } from 'dumi';
-
-export default defineConfig({
- title: 'rc-collapse',
- favicon: 'https://avatars0.githubusercontent.com/u/9441414?s=200&v=4',
- logo: 'https://avatars0.githubusercontent.com/u/9441414?s=200&v=4',
- outputPath: '.doc',
- exportStatic: {},
- styles: [],
-});
diff --git a/README.md b/README.md
index d712a747..898e7272 100644
--- a/README.md
+++ b/README.md
@@ -107,6 +107,14 @@ ReactDOM.render(App, container);
- |
specify whether the panel of children is collapsible or the area of collapsible. |
+
+ items |
+
+ interface.ts#ItemType
+ |
+ - |
+ collapse items content |
+
@@ -116,6 +124,8 @@ If `accordion` is true, only one panel can be open. Opening another panel will c
### Collapse.Panel props
+> **deprecated** use `items` instead, will be removed in `v4.0.0`
+
diff --git a/docs/demo/basic.md b/docs/demo/basic.md
new file mode 100644
index 00000000..acad05fc
--- /dev/null
+++ b/docs/demo/basic.md
@@ -0,0 +1,8 @@
+---
+title: Basic
+nav:
+ title: Demo
+ path: /demo
+---
+
+
diff --git a/docs/examples/basic.tsx b/docs/examples/basic.tsx
new file mode 100644
index 00000000..4b456378
--- /dev/null
+++ b/docs/examples/basic.tsx
@@ -0,0 +1,27 @@
+import type { CollapseProps } from 'rc-collapse';
+import Collapse from 'rc-collapse';
+import * as React from 'react';
+import '../../assets/index.less';
+
+const App = () => {
+ const items: CollapseProps['items'] = [
+ {
+ label: 'title',
+ children: 'content',
+ },
+ {
+ label: 'title 2',
+ children: 'content 2',
+ collapsible: 'disabled',
+ },
+ {
+ label: 'title 3',
+ children: 'content 3',
+ onItemClick: console.log,
+ },
+ ];
+
+ return ;
+};
+
+export default App;
diff --git a/src/Collapse.tsx b/src/Collapse.tsx
index 1c51e11c..36267d29 100644
--- a/src/Collapse.tsx
+++ b/src/Collapse.tsx
@@ -1,8 +1,9 @@
import classNames from 'classnames';
-import toArray from 'rc-util/lib/Children/toArray';
import useMergedState from 'rc-util/lib/hooks/useMergedState';
+import warning from 'rc-util/lib/warning';
import React from 'react';
-import type { CollapsePanelProps, CollapseProps, CollapsibleType } from './interface';
+import useItems from './hooks/useItems';
+import type { CollapseProps } from './interface';
import CollapsePanel from './Panel';
function getActiveKeysArray(activeKey: React.Key | React.Key[]) {
@@ -22,13 +23,14 @@ const Collapse = React.forwardRef((props, ref) =>
style,
accordion,
className,
- children: rawChildren,
+ children,
collapsible,
openMotion,
expandIcon,
activeKey: rawActiveKey,
defaultActiveKey,
onChange,
+ items,
} = props;
const collapseClassName = classNames(prefixCls, className);
@@ -40,7 +42,7 @@ const Collapse = React.forwardRef((props, ref) =>
postState: getActiveKeysArray,
});
- const onClickItem = (key: React.Key) =>
+ const onItemClick = (key: React.Key) =>
setActiveKey(() => {
if (accordion) {
return activeKey[0] === key ? [] : [key];
@@ -56,65 +58,21 @@ const Collapse = React.forwardRef((props, ref) =>
});
// ======================== Children ========================
- const getNewChild = (child: React.ReactElement, index: number) => {
- if (!child) return null;
-
- const key = child.key || String(index);
-
- const {
- header,
- headerClass,
- destroyInactivePanel: childDestroyInactivePanel,
- collapsible: childCollapsible,
- onItemClick: childOnItemClick,
- } = child.props;
-
- let isActive = false;
- if (accordion) {
- isActive = activeKey[0] === key;
- } else {
- isActive = activeKey.indexOf(key) > -1;
- }
-
- const mergeCollapsible: CollapsibleType = childCollapsible ?? collapsible;
-
- const handleItemClick = (value: React.Key) => {
- if (mergeCollapsible === 'disabled') return;
- onClickItem(value);
- childOnItemClick?.(value);
- };
-
- const childProps = {
- key,
- panelKey: key,
- header,
- headerClass,
- isActive,
- prefixCls,
- destroyInactivePanel: childDestroyInactivePanel ?? destroyInactivePanel,
- openMotion,
- accordion,
- children: child.props.children,
- onItemClick: handleItemClick,
- expandIcon,
- collapsible: mergeCollapsible,
- };
-
- // https://github.com/ant-design/ant-design/issues/20479
- if (typeof child.type === 'string') {
- return child;
- }
-
- Object.keys(childProps).forEach((propName) => {
- if (typeof childProps[propName] === 'undefined') {
- delete childProps[propName];
- }
- });
-
- return React.cloneElement(child, childProps);
- };
+ warning(
+ !children,
+ '`children` will be removed in next major version. Please use `items` instead.',
+ );
- const children = toArray(rawChildren).map(getNewChild);
+ const mergedChildren = useItems(items, children, {
+ prefixCls,
+ accordion,
+ openMotion,
+ expandIcon,
+ collapsible,
+ destroyInactivePanel,
+ onItemClick,
+ activeKey,
+ });
// ======================== Render ========================
return (
@@ -124,9 +82,14 @@ const Collapse = React.forwardRef((props, ref) =>
style={style}
role={accordion ? 'tablist' : undefined}
>
- {children}
+ {mergedChildren}
);
});
-export default Object.assign(Collapse, { Panel: CollapsePanel });
+export default Object.assign(Collapse, {
+ /**
+ * @deprecated use `items` instead, will be removed in `v4.0.0`
+ */
+ Panel: CollapsePanel,
+});
diff --git a/src/hooks/useItems.tsx b/src/hooks/useItems.tsx
new file mode 100644
index 00000000..2bc5db7b
--- /dev/null
+++ b/src/hooks/useItems.tsx
@@ -0,0 +1,162 @@
+import toArray from 'rc-util/lib/Children/toArray';
+import React from 'react';
+import type { CollapsePanelProps, CollapseProps, ItemType } from '../interface';
+import CollapsePanel from '../Panel';
+
+type Props = Pick &
+ Pick & {
+ activeKey: React.Key[];
+ };
+
+const convertItemsToNodes = (items: ItemType[], props: Props) => {
+ const {
+ prefixCls,
+ accordion,
+ collapsible,
+ destroyInactivePanel,
+ onItemClick,
+ activeKey,
+ openMotion,
+ expandIcon,
+ } = props;
+
+ return items.map((item, index) => {
+ const {
+ children,
+ label,
+ key: rawKey,
+ collapsible: rawCollapsible,
+ onItemClick: rawOnItemClick,
+ destroyInactivePanel: rawDestroyInactivePanel,
+ ...restProps
+ } = item;
+
+ // You may be puzzled why you want to convert them all into strings, me too.
+ // Maybe: https://github.com/react-component/collapse/blob/aac303a8b6ff30e35060b4f8fecde6f4556fcbe2/src/Collapse.tsx#L15
+ const key = String(rawKey ?? index);
+ const mergeCollapsible = rawCollapsible ?? collapsible;
+ const mergeDestroyInactivePanel = rawDestroyInactivePanel ?? destroyInactivePanel;
+
+ const handleItemClick = (value: React.Key) => {
+ if (mergeCollapsible === 'disabled') return;
+ onItemClick(value);
+ rawOnItemClick?.(value);
+ };
+
+ let isActive = false;
+ if (accordion) {
+ isActive = activeKey[0] === key;
+ } else {
+ isActive = activeKey.indexOf(key) > -1;
+ }
+
+ return (
+
+ {children}
+
+ );
+ });
+};
+
+/**
+ * @deprecated The next major version will be removed
+ */
+const getNewChild = (
+ child: React.ReactElement,
+ index: number,
+ props: Props,
+) => {
+ if (!child) return null;
+
+ const {
+ prefixCls,
+ accordion,
+ collapsible,
+ destroyInactivePanel,
+ onItemClick,
+ activeKey,
+ openMotion,
+ expandIcon,
+ } = props;
+
+ const key = child.key || String(index);
+
+ const {
+ header,
+ headerClass,
+ destroyInactivePanel: childDestroyInactivePanel,
+ collapsible: childCollapsible,
+ onItemClick: childOnItemClick,
+ } = child.props;
+
+ let isActive = false;
+ if (accordion) {
+ isActive = activeKey[0] === key;
+ } else {
+ isActive = activeKey.indexOf(key) > -1;
+ }
+
+ const mergeCollapsible = childCollapsible ?? collapsible;
+
+ const handleItemClick = (value: React.Key) => {
+ if (mergeCollapsible === 'disabled') return;
+ onItemClick(value);
+ childOnItemClick?.(value);
+ };
+
+ const childProps = {
+ key,
+ panelKey: key,
+ header,
+ headerClass,
+ isActive,
+ prefixCls,
+ destroyInactivePanel: childDestroyInactivePanel ?? destroyInactivePanel,
+ openMotion,
+ accordion,
+ children: child.props.children,
+ onItemClick: handleItemClick,
+ expandIcon,
+ collapsible: mergeCollapsible,
+ };
+
+ // https://github.com/ant-design/ant-design/issues/20479
+ if (typeof child.type === 'string') {
+ return child;
+ }
+
+ Object.keys(childProps).forEach((propName) => {
+ if (typeof childProps[propName] === 'undefined') {
+ delete childProps[propName];
+ }
+ });
+
+ return React.cloneElement(child, childProps);
+};
+
+function useItems(
+ items?: ItemType[],
+ rawChildren?: React.ReactNode,
+ props?: Props,
+): React.ReactElement[] {
+ if (Array.isArray(items)) {
+ return convertItemsToNodes(items, props);
+ }
+
+ return toArray(rawChildren).map((child, index) => getNewChild(child, index, props));
+}
+
+export default useItems;
diff --git a/src/index.tsx b/src/index.tsx
index 8cda2ed2..8e963c68 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -3,4 +3,8 @@ import Collapse from './Collapse';
export type { CollapsePanelProps, CollapseProps } from './interface';
export default Collapse;
+
+/**
+ * @deprecated use `items` instead, will be removed in `v4.0.0`
+ */
export const { Panel } = Collapse;
diff --git a/src/interface.ts b/src/interface.ts
index 84540e54..838446a0 100644
--- a/src/interface.ts
+++ b/src/interface.ts
@@ -3,6 +3,22 @@ import type * as React from 'react';
export type CollapsibleType = 'header' | 'icon' | 'disabled';
+export interface ItemType
+ extends Omit<
+ CollapsePanelProps,
+ | 'header' // alias of label
+ | 'prefixCls'
+ | 'panelKey' // alias of key
+ | 'isActive'
+ | 'accordion'
+ | 'openMotion'
+ | 'expandIcon'
+ > {
+ key?: CollapsePanelProps['panelKey'];
+ label?: CollapsePanelProps['header'];
+ ref?: React.RefObject;
+}
+
export interface CollapseProps {
prefixCls?: string;
activeKey?: React.Key | React.Key[];
@@ -16,6 +32,11 @@ export interface CollapseProps {
expandIcon?: (props: object) => React.ReactNode;
collapsible?: CollapsibleType;
children?: React.ReactNode;
+ /**
+ * Collapse items content
+ * @since 3.6.0
+ */
+ items?: ItemType[];
}
export interface CollapsePanelProps extends React.DOMAttributes {
diff --git a/tests/__snapshots__/index.spec.tsx.snap b/tests/__snapshots__/index.spec.tsx.snap
new file mode 100644
index 00000000..ddbe32ae
--- /dev/null
+++ b/tests/__snapshots__/index.spec.tsx.snap
@@ -0,0 +1,111 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`collapse props items should work with nested 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/tests/index.spec.tsx b/tests/index.spec.tsx
index 295e7a2c..533df08b 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-util/lib/KeyCode';
import React, { Fragment } from 'react';
import Collapse, { Panel } from '../src/index';
-import type { CollapseProps } from '../src/interface';
+import type { CollapseProps, ItemType } from '../src/interface';
describe('collapse', () => {
let changeHook: jest.Mock | null;
@@ -243,23 +243,15 @@ describe('collapse', () => {
expect(container.querySelectorAll('.rc-collapse-content-inactive').length).toBeFalsy();
});
- describe('prop: accordion', () => {
+ function runAccordionTest(element: React.ReactElement) {
let collapse: RenderResult;
beforeEach(() => {
- collapse = render(
-
-
- first
-
-
- second
-
-
- third
-
- ,
- );
+ collapse = render(element);
+ });
+
+ afterEach(() => {
+ collapse.unmount();
});
it('accordion content, should default open zero item', () => {
@@ -317,6 +309,22 @@ describe('collapse', () => {
expect(item).toBeTruthy();
expect(item!.getAttribute('role')).toBe('tabpanel');
});
+ }
+
+ describe('prop: accordion', () => {
+ runAccordionTest(
+
+
+ first
+
+
+ second
+
+
+ third
+
+ ,
+ );
});
describe('forceRender', () => {
@@ -690,4 +698,132 @@ describe('collapse', () => {
);
expect(container.querySelector('.rc-collapse-item').style.color).toBe('red');
});
+
+ describe('props items', () => {
+ const items: ItemType[] = [
+ {
+ key: '1',
+ label: 'collapse 1',
+ children: 'first',
+ collapsible: 'disabled',
+ },
+ {
+ key: '2',
+ label: 'collapse 2',
+ children: 'second',
+ extra: ExtraSpan,
+ },
+ {
+ key: '3',
+ label: 'collapse 3',
+ className: 'important',
+ children: 'third',
+ },
+ ];
+
+ runNormalTest(
+ test{'>'}} items={items} />,
+ );
+
+ runAccordionTest(
+ ,
+ );
+
+ it('should work with onItemClick', () => {
+ const onItemClick = jest.fn();
+ const { container } = render(
+ ,
+ );
+ fireEvent.click(container.querySelector('.rc-collapse-header'));
+ expect(onItemClick).toHaveBeenCalled();
+ expect(onItemClick).lastCalledWith('0');
+ });
+
+ it('should work with collapsible', () => {
+ const onItemClick = jest.fn();
+ const onChangeFn = jest.fn();
+ const { container } = render(
+ ,
+ );
+
+ fireEvent.click(container.querySelector('.rc-collapse-header'));
+ expect(onItemClick).not.toHaveBeenCalled();
+
+ fireEvent.click(
+ container.querySelector('.rc-collapse-item:nth-child(2) .rc-collapse-expand-icon'),
+ );
+ expect(onItemClick).toHaveBeenCalled();
+ expect(onChangeFn).toBeCalledTimes(1);
+ expect(onChangeFn).lastCalledWith(['1']);
+ });
+
+ it('should work with nested', () => {
+ const { container } = render(
+ ,
+ },
+ ]}
+ />,
+ );
+ expect(container.firstChild).toMatchSnapshot();
+ });
+
+ it('should not support expandIcon', () => {
+ const { container } = render(
+ p}
+ items={[
+ {
+ label: 'title',
+ expandIcon: () => c,
+ } as any,
+ ]}
+ />,
+ );
+
+ expect(container.querySelectorAll('.custom-icon')).toHaveLength(1);
+ expect(container.querySelector('.custom-icon')?.innerHTML).toBe('p');
+ });
+ });
});