+
{children}
);
diff --git a/packages/react/src/collapse-transition/style/index.scss b/packages/react/src/collapse-transition/style/index.scss
index 8c6bdf051..9806c82b4 100644
--- a/packages/react/src/collapse-transition/style/index.scss
+++ b/packages/react/src/collapse-transition/style/index.scss
@@ -1,4 +1,6 @@
.ty-collapse-transition {
overflow: hidden;
- transition: 250ms height, 250ms padding-top, 250ms padding-bottom;
+ transition:
+ height var(--ty-collapse-motion-duration, 240ms)
+ var(--ty-collapse-motion-easing, ease);
}
diff --git a/packages/react/src/collapse/__tests__/__snapshots__/collapse.test.tsx.snap b/packages/react/src/collapse/__tests__/__snapshots__/collapse.test.tsx.snap
deleted file mode 100644
index 28e3c24e7..000000000
--- a/packages/react/src/collapse/__tests__/__snapshots__/collapse.test.tsx.snap
+++ /dev/null
@@ -1,100 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`
should match the snapshot 1`] = `
-
-
-
-`;
diff --git a/packages/react/src/collapse/__tests__/collapse.test.tsx b/packages/react/src/collapse/__tests__/collapse.test.tsx
index ddb783de3..9da75f535 100644
--- a/packages/react/src/collapse/__tests__/collapse.test.tsx
+++ b/packages/react/src/collapse/__tests__/collapse.test.tsx
@@ -1,45 +1,104 @@
import React from 'react';
-import { render, fireEvent } from '@testing-library/react';
+import { fireEvent, render, screen } from '@testing-library/react';
import Collapse from '../index';
+const items = [
+ { key: 'a', label: 'Header A', children: 'Content A' },
+ { key: 'b', label: 'Header B', children: 'Content B' },
+];
+
describe('
', () => {
- it('should match the snapshot', () => {
- const { asFragment } = render(
-
- Content 1
- Content 2
-
- );
- expect(asFragment()).toMatchSnapshot();
+ it('should render correctly', () => {
+ const { container } = render(
);
+ expect(container.firstChild).toHaveClass('ty-collapse');
});
- it('should render correctly', () => {
- const { container } = render(
-
- Content
-
+ it('should open from defaultValue using item keys', () => {
+ render(
);
+ const trigger = screen.getByRole('button', { name: 'Header B' });
+ expect(trigger).toHaveAttribute('aria-expanded', 'true');
+ });
+
+ it('should toggle panels in uncontrolled mode', () => {
+ render(
);
+ const trigger = screen.getByRole('button', { name: 'Header A' });
+
+ fireEvent.click(trigger);
+ expect(trigger).toHaveAttribute('aria-expanded', 'true');
+
+ fireEvent.click(trigger);
+ expect(trigger).toHaveAttribute('aria-expanded', 'false');
+ });
+
+ it('should only emit changes in controlled mode', () => {
+ const onValueChange = jest.fn();
+
+ render(
);
+
+ const triggerA = screen.getByRole('button', { name: 'Header A' });
+ const triggerB = screen.getByRole('button', { name: 'Header B' });
+
+ fireEvent.click(triggerB);
+
+ expect(onValueChange).toHaveBeenCalledWith(['a', 'b']);
+ expect(triggerA).toHaveAttribute('aria-expanded', 'true');
+ expect(triggerB).toHaveAttribute('aria-expanded', 'false');
+ });
+
+ it('should keep only one expanded item when multiple is false', () => {
+ const onValueChange = jest.fn();
+
+ render(
+
);
- expect(container.firstChild).toHaveClass('ty-collapse');
+
+ fireEvent.click(screen.getByRole('button', { name: 'Header B' }));
+
+ expect(onValueChange).toHaveBeenCalledWith(['b']);
});
- it('should render panel headers', () => {
- const { getByText } = render(
-
- Content 1
- Content 2
-
+ it('should respect disabled panels', () => {
+ render(
+
);
- expect(getByText('Header 1')).toBeInTheDocument();
- expect(getByText('Header 2')).toBeInTheDocument();
+
+ expect(screen.getByRole('button', { name: 'Header A' })).toBeDisabled();
});
- it('should toggle panel on click', () => {
- const { getByText } = render(
-
- Content
-
+ it('should support icon-only collapsible panels', () => {
+ render(
+
);
- fireEvent.click(getByText('Toggle'));
- expect(getByText('Content')).toBeInTheDocument();
+
+ expect(screen.getByText('Header A').closest('button')).toBeNull();
+ fireEvent.click(screen.getByRole('button'));
+ expect(screen.getByRole('region')).toBeInTheDocument();
+ });
+
+ it('should call onItemClick before toggling', () => {
+ const onItemClick = jest.fn();
+
+ render(
);
+
+ fireEvent.click(screen.getByRole('button', { name: 'Header A' }));
+ expect(onItemClick).toHaveBeenCalledWith('a', expect.any(Object));
});
});
diff --git a/packages/react/src/collapse/collapse-context.ts b/packages/react/src/collapse/collapse-context.ts
deleted file mode 100644
index 555082c01..000000000
--- a/packages/react/src/collapse/collapse-context.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import React from 'react';
-
-type Props = {
- activeKeys: string[];
- onItemClick?: (itemKey: string) => void;
-};
-
-export const CollapseContext = React.createContext
({
- activeKeys: [],
-});
diff --git a/packages/react/src/collapse/collapse-panel.tsx b/packages/react/src/collapse/collapse-panel.tsx
old mode 100755
new mode 100644
index b1710f6bc..829844433
--- a/packages/react/src/collapse/collapse-panel.tsx
+++ b/packages/react/src/collapse/collapse-panel.tsx
@@ -1,104 +1,175 @@
-import React, { useContext, useId, useRef } from 'react';
+import React, { useEffect, useId, useState } from 'react';
import classNames from 'classnames';
-import { ConfigContext } from '../config-provider/config-context';
-import { getPrefixCls } from '../_utils/general';
-import { CollapsePanelProps } from './types';
import { ArrowDown } from '../_utils/components';
import CollapseTransition from '../collapse-transition';
-import { CollapseContext } from './collapse-context';
-
-/**
- * Allow to parse active status to a node
- * @param node
- * @param isActive
- */
-const richNode = (node: React.ReactNode | ((isActive: boolean) => React.ReactNode), isActive: boolean) => {
- return typeof node === 'function' ? node(isActive) : node;
+import {
+ CollapseCollapsible,
+ CollapseExpandIconRender,
+ CollapseItem,
+ CollapseRenderState,
+} from './types';
+
+type CollapsePanelProps = {
+ prefixCls: string;
+ item: CollapseItem;
+ active: boolean;
+ disabled?: boolean;
+ collapsible?: CollapseCollapsible;
+ showArrow: boolean;
+ expandIcon?: React.ReactNode | CollapseExpandIconRender;
+ expandIconPosition: 'start' | 'end';
+ forceRender?: boolean;
+ destroyOnHidden?: boolean;
+ itemClassName?: string;
+ itemStyle?: React.CSSProperties;
+ headerClassName?: string;
+ bodyClassName?: string;
+ onItemClick?: (key: string, event: React.MouseEvent) => void;
+ onToggle: (key: string, event: React.MouseEvent) => void;
+};
+
+const renderContent = React.ReactNode)>(
+ content: T,
+ state: CollapseRenderState
+) => {
+ return typeof content === 'function' ? content(state) : content;
};
-const CollapsePanel = (props: CollapsePanelProps): React.ReactElement => {
- const {
- showArrow = true,
- itemKey,
- header,
- disabled,
- extra,
- deletable,
- onHeaderOnClick,
- className,
- style,
- children,
- prefixCls: customisedCls,
- } = props;
- const itemRef = useRef(null);
+const CollapsePanel = ({
+ prefixCls,
+ item,
+ active,
+ disabled = false,
+ collapsible = 'header',
+ showArrow,
+ expandIcon,
+ expandIconPosition,
+ forceRender = false,
+ destroyOnHidden = false,
+ itemClassName,
+ itemStyle,
+ headerClassName,
+ bodyClassName,
+ onItemClick,
+ onToggle,
+}: CollapsePanelProps): React.ReactElement => {
+ const [bodyMounted, setBodyMounted] = useState(active || forceRender);
const panelId = useId();
const headerId = useId();
- const configContext = useContext(ConfigContext);
- const { activeKeys, onItemClick } = useContext(CollapseContext);
- const prefixCls = getPrefixCls('collapse-item', configContext.prefixCls, customisedCls);
- const active = activeKeys.includes(itemKey);
- const cls = classNames(prefixCls, className, {
- [`${prefixCls}_active`]: active,
- });
- const headerOnClick = (e: React.MouseEvent) => {
- if (!disabled) {
- onHeaderOnClick && onHeaderOnClick(e);
- onItemClick && onItemClick(itemKey);
+ useEffect(() => {
+ if (active || forceRender) {
+ setBodyMounted(true);
}
+ }, [active, forceRender]);
+
+ const requestedCollapsible = showArrow || collapsible !== 'icon' ? collapsible : 'header';
+ const itemDisabled = disabled || item.disabled || requestedCollapsible === 'disabled';
+ const layoutCollapsible: CollapseCollapsible =
+ requestedCollapsible === 'icon' ? 'icon' : 'header';
+ const renderState: CollapseRenderState = {
+ active,
+ disabled: itemDisabled,
+ panelKey: item.key,
};
- /**
- * Remove a item from collapse only the header is enabled
- * @param e
- * @private
- */
- const removeItem = (e: React.MouseEvent) => {
- e.stopPropagation();
- if (!disabled) {
- const node = itemRef.current;
- node && node.parentNode?.removeChild(node);
- }
+ const itemCls = classNames(`${prefixCls}-item`, itemClassName, item.className, {
+ [`${prefixCls}-item_active`]: active,
+ [`${prefixCls}-item_disabled`]: itemDisabled,
+ [`${prefixCls}-item_icon-only`]: layoutCollapsible === 'icon',
+ });
+
+ const iconCls = classNames(`${prefixCls}-item__arrow`, {
+ [`${prefixCls}-item__arrow_active`]: active,
+ });
+
+ const triggerToggle = (event: React.MouseEvent) => {
+ if (itemDisabled) return;
+ onItemClick?.(item.key, event);
+ onToggle(item.key, event);
};
- const renderHeader = () => {
- const headerCls = classNames(`${prefixCls}__header`, {
- [`${prefixCls}__header_disabled`]: disabled,
- });
- const arrowCls = classNames(`${prefixCls}__arrow`, {
- [`${prefixCls}__arrow_active`]: active,
- });
+ const renderExpandIcon = () => {
+ if (!showArrow) return null;
- const hasExtra = deletable || extra;
+ const iconNode =
+ typeof expandIcon === 'function'
+ ? expandIcon(renderState)
+ : expandIcon ?? ;
- return (
-
+ const iconButtonCls = classNames(`${prefixCls}-item__icon-button`, {
+ [`${prefixCls}-item__icon-button_disabled`]: itemDisabled,
+ });
+
+ if (layoutCollapsible === 'icon') {
+ return (
- {hasExtra && (
-
- {deletable ? ✕ : richNode(extra, active)}
-
- )}
-
- );
+ );
+ }
+
+ return {iconNode} ;
};
+ const headerContent = renderContent(item.label, renderState);
+ const extraContent = item.extra ? renderContent(item.extra, renderState) : null;
+ const shouldRenderBody = bodyMounted || active || forceRender;
+
return (
-
- {renderHeader()}
-
- {richNode(children, active)}
-
+
+
+ {layoutCollapsible === 'header' ? (
+
+ ) : (
+ <>
+ {expandIconPosition === 'start' && renderExpandIcon()}
+
+ {expandIconPosition === 'end' && renderExpandIcon()}
+ >
+ )}
+
+ {extraContent &&
{extraContent}
}
+
+
+ {shouldRenderBody && (
+
setBodyMounted(false) : undefined}
+ >
+
+ {item.children}
+
+
+ )}
);
};
diff --git a/packages/react/src/collapse/collapse.tsx b/packages/react/src/collapse/collapse.tsx
index e7776637f..547744fdc 100644
--- a/packages/react/src/collapse/collapse.tsx
+++ b/packages/react/src/collapse/collapse.tsx
@@ -1,99 +1,116 @@
-import React, { useState, useEffect, useContext } from 'react';
+import React, { useContext, useEffect, useMemo, useState } from 'react';
import classNames from 'classnames';
import { ConfigContext } from '../config-provider/config-context';
-import { CollapseContext } from './collapse-context';
import { getPrefixCls } from '../_utils/general';
-import { CollapsePanelProps, CollapseProps } from './types';
+import CollapsePanel from './collapse-panel';
+import { CollapseProps, CollapseValue } from './types';
-/**
- * Format active key to array
- * @param activeKey
- */
-const toArray = (activeKey: string | string[]): string[] => {
- return Array.isArray(activeKey) ? activeKey : [activeKey];
+const normalizeValue = (value: string[] | undefined, multiple: boolean): CollapseValue => {
+ if (!value || value.length === 0) return [];
+ return multiple ? [...new Set(value)] : [value[0]];
};
-const Collapse = React.forwardRef
(
- (props: CollapseProps, ref): React.ReactElement => {
- const {
- showArrow = true,
- bordered = true,
- deletable = false,
- accordion = false,
- defaultActiveKey = [],
- prefixCls: customisedCls,
- activeKey,
- onChange,
- className,
- children,
- ...otherProps
- } = props;
- let currentActiveKey: string | string[] = defaultActiveKey;
- if (activeKey) {
- currentActiveKey = activeKey;
+const Collapse = React.forwardRef((props, ref): React.ReactElement => {
+ const {
+ items,
+ value,
+ defaultValue,
+ onValueChange,
+ multiple = true,
+ bordered = true,
+ size = 'md',
+ showArrow = true,
+ expandIcon,
+ expandIconPosition = 'start',
+ disabled = false,
+ collapsible = 'header',
+ destroyOnHidden = false,
+ forceRender = false,
+ itemClassName,
+ itemStyle,
+ headerClassName,
+ bodyClassName,
+ onItemClick,
+ prefixCls: customisedCls,
+ className,
+ ...otherProps
+ } = props;
+ const configContext = useContext(ConfigContext);
+ const prefixCls = getPrefixCls('collapse', configContext.prefixCls, customisedCls);
+ const isControlled = value !== undefined;
+ const [innerValue, setInnerValue] = useState(() =>
+ normalizeValue(defaultValue, multiple)
+ );
+
+ useEffect(() => {
+ if (!isControlled) return;
+ setInnerValue(normalizeValue(value, multiple));
+ }, [isControlled, value, multiple]);
+
+ const currentValue = isControlled ? normalizeValue(value, multiple) : innerValue;
+
+ const itemMap = useMemo(() => new Set(items.map((item) => item.key)), [items]);
+ const activeValue = useMemo(
+ () => currentValue.filter((key) => itemMap.has(key)),
+ [currentValue, itemMap]
+ );
+
+ const cls = classNames(prefixCls, className, {
+ [`${prefixCls}_borderless`]: !bordered,
+ [`${prefixCls}_${size}`]: size,
+ });
+
+ const updateValue = (nextValue: CollapseValue) => {
+ const normalized = normalizeValue(nextValue, multiple);
+
+ if (!isControlled) {
+ setInnerValue(normalized);
}
- const [activeItems, setActiveItems] = useState(toArray(currentActiveKey));
- const configContext = useContext(ConfigContext);
- const prefixCls = getPrefixCls('collapse', configContext.prefixCls, customisedCls);
- const cls = classNames(prefixCls, className, {
- [`${prefixCls}_borderless`]: !bordered,
- });
- const updateActiveItems = (items: string[]) => {
- if (!('activeKey' in props)) {
- // only for defaultKey
- setActiveItems(items);
- }
- onChange && onChange(items);
- };
+ onValueChange?.(normalized);
+ };
- const handleOnItemClick = (itemKey: string) => {
- let items = activeItems;
- if (accordion) {
- items = items[0] === itemKey ? [] : [itemKey];
- } else {
- items = [...activeItems];
- const index = items.indexOf(itemKey);
- const isActive = index > -1;
- if (isActive) {
- // remove active state
- items.splice(index, 1);
- } else {
- items.push(itemKey);
- }
- }
- updateActiveItems(items);
- };
+ const handleToggle = (key: string) => {
+ const isActive = activeValue.includes(key);
+
+ if (multiple) {
+ updateValue(
+ isActive
+ ? activeValue.filter((activeKey) => activeKey !== key)
+ : [...activeValue, key]
+ );
+ return;
+ }
- useEffect(() => {
- // Update state from updated props
- activeKey && setActiveItems(toArray(activeKey));
- }, [activeKey]);
+ updateValue(isActive ? [] : [key]);
+ };
- return (
-
-
- {React.Children.map(children, (child, idx) => {
- const childElement = child as React.FunctionComponentElement;
- if (childElement.type.displayName === 'CollapsePanel') {
- const itemProps: Partial = {
- deletable,
- showArrow,
- itemKey: `${idx}`,
- };
- return React.cloneElement(childElement, itemProps);
- }
- return child;
- })}
-
-
- );
- }
-);
+ return (
+
+ {items.map((item) => (
+
+ ))}
+
+ );
+});
Collapse.displayName = 'Collapse';
diff --git a/packages/react/src/collapse/demo/Accordion.tsx b/packages/react/src/collapse/demo/Accordion.tsx
index ed23e4e99..5b3872d4b 100644
--- a/packages/react/src/collapse/demo/Accordion.tsx
+++ b/packages/react/src/collapse/demo/Accordion.tsx
@@ -1,24 +1,32 @@
import React from 'react';
import { Collapse } from '@tiny-design/react';
-export default function AccordionDemo() {
- const { Panel } = Collapse;
-
- const text = `A dog is a type of domesticated animal.
+const text = `A dog is a type of domesticated animal.
Known for its loyalty and faithfulness,
it can be found as a welcome guest in many households across the world.`;
+export default function AccordionDemo() {
return (
-
-
- {text}
-
-
- {text}
-
-
- {text}
-
-
+ {text},
+ },
+ {
+ key: 'chapter-2',
+ label: 'Chapter 2',
+ children: {text}
,
+ },
+ {
+ key: 'chapter-3',
+ label: 'Chapter 3',
+ children: {text}
,
+ },
+ ]}
+ />
);
}
diff --git a/packages/react/src/collapse/demo/Basic.tsx b/packages/react/src/collapse/demo/Basic.tsx
index 7dcdad8e1..e32b1bd85 100644
--- a/packages/react/src/collapse/demo/Basic.tsx
+++ b/packages/react/src/collapse/demo/Basic.tsx
@@ -1,28 +1,32 @@
import React from 'react';
import { Collapse } from '@tiny-design/react';
-export default function BasicDemo() {
- const { Panel } = Collapse;
-
- const callback = (key: string | string[]) => {
- console.log(key);
- };
-
- const text = `A dog is a type of domesticated animal.
+const text = `A dog is a type of domesticated animal.
Known for its loyalty and faithfulness,
it can be found as a welcome guest in many households across the world.`;
+export default function BasicDemo() {
return (
-
-
- {text}
-
-
- {text}
-
-
- {text}
-
-
+ {text},
+ },
+ {
+ key: 'details',
+ label: 'Details',
+ children: {text}
,
+ },
+ {
+ key: 'disabled',
+ label: 'Disabled panel',
+ disabled: true,
+ children: {text}
,
+ },
+ ]}
+ />
);
}
diff --git a/packages/react/src/collapse/demo/Borderless.tsx b/packages/react/src/collapse/demo/Borderless.tsx
index c02b7aada..a49666a23 100644
--- a/packages/react/src/collapse/demo/Borderless.tsx
+++ b/packages/react/src/collapse/demo/Borderless.tsx
@@ -1,24 +1,30 @@
import React from 'react';
-import { Collapse } from '@tiny-design/react';
+import { Collapse, Tag } from '@tiny-design/react';
export default function BorderlessDemo() {
- const { Panel } = Collapse;
-
- const text = `A dog is a type of domesticated animal.
-Known for its loyalty and faithfulness,
-it can be found as a welcome guest in many households across the world.`;
-
return (
-
-
- {text}
-
-
- {text}
-
-
- {text}
-
-
+ Stable,
+ children: 'Borderless mode keeps the layout lighter when Collapse is used inside cards or side panels.',
+ },
+ {
+ key: 'spacing',
+ label: 'Spacing rhythm',
+ children: 'Size presets affect both the trigger row and the content body spacing.',
+ },
+ {
+ key: 'motion',
+ label: 'Motion tokens',
+ children: 'Motion is driven by the shared transition token instead of a hard-coded timeout.',
+ },
+ ]}
+ />
);
-}
\ No newline at end of file
+}
diff --git a/packages/react/src/collapse/demo/Deletable.tsx b/packages/react/src/collapse/demo/Deletable.tsx
index 8a86bbbae..20ebf54c3 100644
--- a/packages/react/src/collapse/demo/Deletable.tsx
+++ b/packages/react/src/collapse/demo/Deletable.tsx
@@ -1,24 +1,83 @@
import React from 'react';
-import { Collapse } from '@tiny-design/react';
+import { Collapse, Button } from '@tiny-design/react';
+
+type DemoItem = {
+ key: string;
+ label: string;
+ children: string;
+};
export default function DeletableDemo() {
- const { Panel } = Collapse;
+ const [items, setItems] = React.useState([
+ {
+ key: 'architecture',
+ label: 'Architecture',
+ children: 'Use controlled state to remove items from the source array instead of mutating DOM nodes.',
+ },
+ {
+ key: 'performance',
+ label: 'Performance',
+ children: 'The active value is filtered alongside the item list so removed panels do not leave stale state behind.',
+ },
+ {
+ key: 'preload',
+ label: 'Preload',
+ children: 'This pattern works with any async source because the Collapse only consumes items data.',
+ },
+ ]);
+ const [value, setValue] = React.useState(['architecture', 'performance']);
+
+ const removeItem = (key: string) => {
+ setItems((currentItems) => currentItems.filter((item) => item.key !== key));
+ setValue((currentValue) => currentValue.filter((activeKey) => activeKey !== key));
+ };
- const text = `A dog is a type of domesticated animal.
-Known for its loyalty and faithfulness,
-it can be found as a welcome guest in many households across the world.`;
+ const resetItems = () => {
+ setItems([
+ {
+ key: 'architecture',
+ label: 'Architecture',
+ children: 'Use controlled state to remove items from the source array instead of mutating DOM nodes.',
+ },
+ {
+ key: 'performance',
+ label: 'Performance',
+ children: 'The active value is filtered alongside the item list so removed panels do not leave stale state behind.',
+ },
+ {
+ key: 'preload',
+ label: 'Preload',
+ children: 'This pattern works with any async source because the Collapse only consumes items data.',
+ },
+ ]);
+ setValue(['architecture', 'performance']);
+ };
return (
-
-
- {text}
-
-
- {text}
-
-
- {text}
-
-
+ <>
+
+ Reset panels
+
+
+ ({
+ ...item,
+ extra: (
+ {
+ event.stopPropagation();
+ removeItem(item.key);
+ }}
+ >
+ Delete
+
+ ),
+ }))}
+ />
+ >
);
-}
\ No newline at end of file
+}
diff --git a/packages/react/src/collapse/demo/Extra.tsx b/packages/react/src/collapse/demo/Extra.tsx
index b82143d91..536724af9 100644
--- a/packages/react/src/collapse/demo/Extra.tsx
+++ b/packages/react/src/collapse/demo/Extra.tsx
@@ -1,42 +1,40 @@
import React from 'react';
-import { Collapse, Button, Badge, Tag } from '@tiny-design/react';
+import { Collapse, Badge, Button, Tag } from '@tiny-design/react';
export default function ExtraDemo() {
- const { Panel } = Collapse;
-
return (
-
- {
- e.stopPropagation();
- alert('Settings clicked');
- }}
- >
- Settings
-
- }
- >
- Panel content with an extra action button.
-
- }
- >
- Panel content with a badge indicator.
-
- New}
- >
- Panel content with a tag.
-
-
+ `Recent activity${active ? ' opened' : ''}`,
+ extra: ,
+ children: 'Header and extra content can be rendered from the item definition without composition wrappers.',
+ },
+ {
+ key: 'release',
+ label: 'Release notes',
+ extra: ({ active }) => {active ? 'Live' : 'Draft'} ,
+ children: 'Both label and extra accept render functions that receive the active and disabled state.',
+ },
+ {
+ key: 'settings',
+ label: 'Panel with action',
+ extra: (
+ {
+ event.stopPropagation();
+ alert('Settings clicked');
+ }}
+ >
+ Settings
+
+ ),
+ children: 'Interactive controls inside extra should stop propagation if they should not toggle the panel.',
+ },
+ ]}
+ />
);
-}
\ No newline at end of file
+}
diff --git a/packages/react/src/collapse/demo/Nested.tsx b/packages/react/src/collapse/demo/Nested.tsx
index 5c7f2cde8..10f4cc7ad 100644
--- a/packages/react/src/collapse/demo/Nested.tsx
+++ b/packages/react/src/collapse/demo/Nested.tsx
@@ -2,27 +2,33 @@ import React from 'react';
import { Collapse } from '@tiny-design/react';
export default function NestedDemo() {
- const { Panel } = Collapse;
-
- const text = `A dog is a type of domesticated animal.
-Known for its loyalty and faithfulness,
-it can be found as a welcome guest in many households across the world.`;
-
return (
-
-
-
-
- {text}
-
-
-
-
- {text}
-
-
- {text}
-
-
+
+ ),
+ },
+ {
+ key: 'parent-2',
+ label: 'Independent panel',
+ children: 'Nested content no longer relies on child index cloning, so ordering stays stable.',
+ },
+ ]}
+ />
);
-}
\ No newline at end of file
+}
diff --git a/packages/react/src/collapse/index.md b/packages/react/src/collapse/index.md
old mode 100755
new mode 100644
index dce241454..fdd1cdf6b
--- a/packages/react/src/collapse/index.md
+++ b/packages/react/src/collapse/index.md
@@ -13,49 +13,39 @@ import NestedSource from './demo/Nested.tsx?raw';
# Collapse
-A content area which can be collapsed and expanded.
-
-## Scenario
-
-Can be used to group or hide complex regions to keep the page clean.
-
-`Accordion` is a special kind of `Collapse`, which allows only one panel to be expanded at a time.
+Structured disclosure for dense information.
## Usage
-```jsx
+```tsx
import { Collapse } from 'tiny-design';
-
-const { Panel } = Collapse;
```
+`Collapse` is now fully items-driven. Each panel is defined by an item object and expanded state is modeled as `string[]`.
+
## Examples
-### Basic Collapse
-
-By default, any number of panels can be expanded at a time. The first panel is expanded in this example.
+### Basic
-### Accordion
+### Single Open
-Only one panel can be expanded at a time.
+Set `multiple={false}` to limit the expanded state to one panel.
-### Nested panel
-
-`Collapse` is nested inside the `Collapse`.
+### Nested
@@ -66,8 +56,6 @@ Only one panel can be expanded at a time.
### Borderless
-A borderless style of Collapse.
-
@@ -75,16 +63,16 @@ A borderless style of Collapse.
### Deletable
-`Panel` can be deleted.
+Use `extra` plus controlled state to remove panels from the source array.
-### Extra Content
+### Dynamic Header Content
-Add extra elements in the panel header corner with the `extra` prop.
+`label`, `extra`, and `expandIcon` can react to panel state.
@@ -96,24 +84,51 @@ Add extra elements in the panel header corner with the `extra` prop.
### Collapse
-| Property | Description | Type | Default |
-| ----------------- | --------------------------------------------------------- | --------------------------------- | --------- |
-| defaultActiveKey | initial active panel | string | string[] | [] |
-| activeKey | keys of the active panel | string | string[] | - |
-| accordion | accordion mode | boolean | false |
-| deletable | panel can be deleted | boolean | false |
-| showArrow | display arrow icon | boolean | true |
-| bordered | render borders around the collapse block | boolean | true |
-| onChange | callback function executed when active panel is changed | (keys: string | string[]) => void | - |
-
-### Collapse.Panel
-
-| Property | Description | Type | Default |
-| ----------------- | --------------------------------------------- | --------------------------------- | --------- |
-| itemKey | unique key identifying the panel | string | - |
-| header | title of the panel | ReactNode | - |
-| disabled | panel cannot be opened or closed if set true | boolean | - |
-| extra | extra element in the corner | ReactNode | - |
-| deletable | whether the panel can be deleted | boolean | - |
-| showArrow | display arrow icon | boolean | - |
-| onHeaderOnClick | callback when the header is clicked | (e: React.MouseEvent) => void | - |
\ No newline at end of file
+| Property | Description | Type | Default |
+| --- | --- | --- | --- |
+| items | Panel definitions | `CollapseItem[]` | - |
+| value | Controlled expanded keys | `string[]` | - |
+| defaultValue | Initial expanded keys | `string[]` | `[]` |
+| onValueChange | Callback when expanded keys change | `(value: string[]) => void` | - |
+| multiple | Allow more than one panel to stay open | `boolean` | `true` |
+| bordered | Show the outer border | `boolean` | `true` |
+| size | Preset spacing and font size | `'sm' \| 'md' \| 'lg'` | `'md'` |
+| showArrow | Render the expand icon slot | `boolean` | `true` |
+| expandIcon | Custom expand icon node or render function | `ReactNode \| ((state) => ReactNode)` | - |
+| expandIconPosition | Placement of the expand icon | `'start' \| 'end'` | `'start'` |
+| disabled | Disable all panels | `boolean` | `false` |
+| collapsible | Default trigger area for panels | `'header' \| 'icon' \| 'disabled'` | `'header'` |
+| destroyOnHidden | Unmount panel content after close transition | `boolean` | `false` |
+| forceRender | Pre-render every panel body | `boolean` | `false` |
+| itemClassName | Shared class applied to every panel item | `string` | - |
+| itemStyle | Shared style applied to every panel item | `CSSProperties` | - |
+| headerClassName | Shared class applied to every panel header | `string` | - |
+| bodyClassName | Shared class applied to every panel body | `string` | - |
+| onItemClick | Callback when a panel trigger is activated | `(key: string, event: React.MouseEvent) => void` | - |
+
+### CollapseItem
+
+| Property | Description | Type | Default |
+| --- | --- | --- | --- |
+| key | Unique panel identifier | `string` | - |
+| label | Header content or render function | `ReactNode \| ((state) => ReactNode)` | - |
+| children | Panel content | `ReactNode` | - |
+| extra | Extra content rendered at the header edge | `ReactNode \| ((state) => ReactNode)` | - |
+| disabled | Disable a single panel | `boolean` | `false` |
+| collapsible | Override the trigger area for a single panel | `'header' \| 'icon' \| 'disabled'` | inherited |
+| forceRender | Pre-render this panel body | `boolean` | inherited |
+| destroyOnHidden | Unmount this panel after close transition | `boolean` | inherited |
+| className | Panel item class name | `string` | - |
+| style | Panel item inline style | `CSSProperties` | - |
+
+### Render State
+
+Render callbacks receive:
+
+```ts
+type CollapseRenderState = {
+ active: boolean;
+ disabled: boolean;
+ panelKey: string;
+};
+```
diff --git a/packages/react/src/collapse/index.tsx b/packages/react/src/collapse/index.tsx
old mode 100755
new mode 100644
index d716365a5..fa64adf8c
--- a/packages/react/src/collapse/index.tsx
+++ b/packages/react/src/collapse/index.tsx
@@ -1,11 +1,4 @@
import Collapse from './collapse';
-import CollapsePanel from './collapse-panel';
-type ICollapse = typeof Collapse & {
- Panel: typeof CollapsePanel;
-};
-
-const DefaultCollapse = Collapse as ICollapse;
-DefaultCollapse.Panel = CollapsePanel;
-
-export default DefaultCollapse;
+export default Collapse;
+export type * from './types';
diff --git a/packages/react/src/collapse/index.zh_CN.md b/packages/react/src/collapse/index.zh_CN.md
index 6ed5cdb9e..45d799931 100644
--- a/packages/react/src/collapse/index.zh_CN.md
+++ b/packages/react/src/collapse/index.zh_CN.md
@@ -13,49 +13,39 @@ import NestedSource from './demo/Nested.tsx?raw';
# Collapse 折叠面板
-可以折叠/展开的内容区域。
-
-## 使用场景
-
-可用于对复杂区域进行分组或隐藏,保持页面简洁。
-
-`Accordion`(手风琴)是一种特殊的 `Collapse`,同一时间只允许展开一个面板。
+用于组织密集信息的分层展开组件。
## 使用方式
-```jsx
+```tsx
import { Collapse } from 'tiny-design';
-
-const { Panel } = Collapse;
```
+新版 `Collapse` 完全采用 `items` 数据驱动,展开状态统一建模为 `string[]`。
+
## 代码示例
-### 基本折叠
-
-默认情况下,可以同时展开任意数量的面板。此示例中第一个面板是展开的。
+### 基本用法
-### 手风琴
+### 单项展开
-同一时间只能展开一个面板。
+设置 `multiple={false}` 后,同一时间只保留一个展开项。
-### 嵌套面板
-
-`Collapse` 可以嵌套在 `Collapse` 内部。
+### 嵌套
@@ -66,8 +56,6 @@ const { Panel } = Collapse;
### 无边框
-无边框样式的折叠面板。
-
@@ -75,16 +63,16 @@ const { Panel } = Collapse;
### 可删除
-面板可以被删除。
+通过 `extra` 配合受控状态,从数据源中移除面板。
-### 额外内容
+### 动态头部内容
-使用 `extra` 属性在面板头部角落添加额外元素。
+`label`、`extra` 和 `expandIcon` 都可以根据面板状态动态渲染。
@@ -96,24 +84,51 @@ const { Panel } = Collapse;
### Collapse
-| 属性 | 说明 | 类型 | 默认值 |
-| ----------------- | ----------------------------------------------------- | --------------------------------- | --------- |
-| defaultActiveKey | 初始展开的面板 | string | string[] | [] |
-| activeKey | 当前展开面板的 key | string | string[] | - |
-| accordion | 手风琴模式 | boolean | false |
-| deletable | 面板可删除 | boolean | false |
-| showArrow | 显示箭头图标 | boolean | true |
-| bordered | 在折叠区域周围渲染边框 | boolean | true |
-| onChange | 展开面板变化时的回调函数 | (keys: string | string[]) => void | - |
-
-### Collapse.Panel
-
-| 属性 | 说明 | 类型 | 默认值 |
-| ----------------- | --------------------------------------------- | --------------------------------- | --------- |
-| itemKey | 面板的唯一标识 key | string | - |
-| header | 面板标题 | ReactNode | - |
-| disabled | 设为 true 时面板无法展开或折叠 | boolean | - |
-| extra | 角落的额外元素 | ReactNode | - |
-| deletable | 面板是否可删除 | boolean | - |
-| showArrow | 显示箭头图标 | boolean | - |
-| onHeaderOnClick | 点击头部时的回调 | (e: React.MouseEvent) => void | - |
\ No newline at end of file
+| 属性 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| items | 面板配置数组 | `CollapseItem[]` | - |
+| value | 当前展开项,受控模式 | `string[]` | - |
+| defaultValue | 初始展开项 | `string[]` | `[]` |
+| onValueChange | 展开项变化回调 | `(value: string[]) => void` | - |
+| multiple | 是否允许同时展开多个面板 | `boolean` | `true` |
+| bordered | 是否显示外层边框 | `boolean` | `true` |
+| size | 预设尺寸 | `'sm' \| 'md' \| 'lg'` | `'md'` |
+| showArrow | 是否渲染展开图标区域 | `boolean` | `true` |
+| expandIcon | 自定义展开图标节点或渲染函数 | `ReactNode \| ((state) => ReactNode)` | - |
+| expandIconPosition | 展开图标位置 | `'start' \| 'end'` | `'start'` |
+| disabled | 是否禁用全部面板 | `boolean` | `false` |
+| collapsible | 默认触发区域 | `'header' \| 'icon' \| 'disabled'` | `'header'` |
+| destroyOnHidden | 收起后在过渡结束时卸载内容 | `boolean` | `false` |
+| forceRender | 是否预渲染全部面板内容 | `boolean` | `false` |
+| itemClassName | 统一的面板项类名 | `string` | - |
+| itemStyle | 统一的面板项样式 | `CSSProperties` | - |
+| headerClassName | 统一的头部类名 | `string` | - |
+| bodyClassName | 统一的内容区类名 | `string` | - |
+| onItemClick | 点击面板触发器时的回调 | `(key: string, event: React.MouseEvent) => void` | - |
+
+### CollapseItem
+
+| 属性 | 说明 | 类型 | 默认值 |
+| --- | --- | --- | --- |
+| key | 面板唯一标识 | `string` | - |
+| label | 头部内容或渲染函数 | `ReactNode \| ((state) => ReactNode)` | - |
+| children | 面板内容 | `ReactNode` | - |
+| extra | 头部右侧额外内容 | `ReactNode \| ((state) => ReactNode)` | - |
+| disabled | 是否禁用当前面板 | `boolean` | `false` |
+| collapsible | 覆盖当前面板的触发区域 | `'header' \| 'icon' \| 'disabled'` | 继承父级 |
+| forceRender | 预渲染当前面板内容 | `boolean` | 继承父级 |
+| destroyOnHidden | 收起后在过渡结束时卸载当前面板内容 | `boolean` | 继承父级 |
+| className | 面板项类名 | `string` | - |
+| style | 面板项内联样式 | `CSSProperties` | - |
+
+### Render State
+
+渲染函数会收到:
+
+```ts
+type CollapseRenderState = {
+ active: boolean;
+ disabled: boolean;
+ panelKey: string;
+};
+```
diff --git a/packages/react/src/collapse/style/index.scss b/packages/react/src/collapse/style/index.scss
index 9a00e2ecf..910b21a69 100644
--- a/packages/react/src/collapse/style/index.scss
+++ b/packages/react/src/collapse/style/index.scss
@@ -1,118 +1,242 @@
@use "../../style/variables" as *;
.#{$prefix}-collapse {
+ --ty-collapse-bg: var(--ty-color-bg-container);
+ --ty-collapse-border-color: var(--ty-color-border);
+ --ty-collapse-borderless-divider-color: var(--ty-color-border-secondary);
+ --ty-collapse-radius: var(--ty-border-radius);
+ --ty-collapse-header-bg: transparent;
+ --ty-collapse-header-hover-bg: var(--ty-color-fill);
+ --ty-collapse-header-active-bg: var(--ty-color-fill);
+ --ty-collapse-header-disabled-bg: transparent;
+ --ty-collapse-header-color: var(--ty-color-text);
+ --ty-collapse-header-disabled-color: var(--ty-color-text-quaternary);
+ --ty-collapse-header-min-height: 44px;
+ --ty-collapse-header-padding-inline: 16px;
+ --ty-collapse-header-padding-block: 14px;
+ --ty-collapse-header-gap: 12px;
+ --ty-collapse-icon-color: currentcolor;
+ --ty-collapse-icon-active-color: currentcolor;
+ --ty-collapse-icon-disabled-color: var(--ty-color-text-quaternary);
+ --ty-collapse-icon-slot-size: 20px;
+ --ty-collapse-icon-size: 10px;
+ --ty-collapse-extra-color: var(--ty-color-text-secondary);
+ --ty-collapse-extra-disabled-color: var(--ty-color-text-quaternary);
+ --ty-collapse-extra-gap: 8px;
+ --ty-collapse-extra-font-size: 12px;
+ --ty-collapse-body-bg: var(--ty-color-bg-container);
+ --ty-collapse-body-color: var(--ty-color-text-secondary);
+ --ty-collapse-body-padding-inline: 16px;
+ --ty-collapse-body-padding-block: 16px;
+ --ty-collapse-font-size: 14px;
+ --ty-collapse-line-height: 1.5;
+ --ty-collapse-focus-ring: var(--ty-control-outline, var(--ty-color-primary));
+ --ty-collapse-motion-duration: var(--ty-motion-duration-mid, 240ms);
+ --ty-collapse-motion-easing: var(--ty-motion-ease-standard, ease);
+
box-sizing: border-box;
- border-radius: var(--ty-collapse-radius, var(--ty-border-radius));
- color: var(--ty-collapse-color, var(--ty-color-text));
- font-size: var(--ty-collapse-font-size, 14px);
- border: 1px solid var(--ty-collapse-border, var(--ty-color-border));
- border-bottom: 0;
- background-color: var(--ty-collapse-bg, var(--ty-color-fill));
+ border: 1px solid var(--ty-collapse-border-color);
+ border-radius: var(--ty-collapse-radius);
+ background: var(--ty-collapse-bg);
+ color: var(--ty-collapse-header-color);
+ font-size: var(--ty-collapse-font-size);
+ line-height: var(--ty-collapse-line-height);
overflow: hidden;
&-item {
- box-sizing: border-box;
- border-bottom: 1px solid var(--ty-collapse-border, var(--ty-color-border));
+ border-bottom: 1px solid var(--ty-collapse-border-color);
+ background: inherit;
&:last-child {
- border-radius: 0 0 var(--ty-collapse-radius, var(--ty-border-radius)) var(--ty-collapse-radius, var(--ty-border-radius));
+ border-bottom: 0;
+ }
+
+ &_disabled {
+ .#{$prefix}-collapse-item__header {
+ cursor: not-allowed;
+ background: var(--ty-collapse-header-disabled-bg);
+
+ &:hover {
+ background: var(--ty-collapse-header-disabled-bg);
+ }
+ }
+
+ .#{$prefix}-collapse-item__label {
+ color: var(--ty-collapse-header-disabled-color);
+ }
- .#{$prefix}-collapse-item__content {
- border-radius: 0 0 var(--ty-collapse-radius, var(--ty-border-radius)) var(--ty-collapse-radius, var(--ty-border-radius));
+ .#{$prefix}-collapse-item__arrow,
+ .#{$prefix}-collapse-item__extra {
+ color: var(--ty-collapse-icon-disabled-color);
}
}
&__header {
display: flex;
align-items: center;
- box-sizing: border-box;
- position: relative;
- color: var(--ty-collapse-header-color, var(--ty-color-text));
- line-height: var(--ty-collapse-header-line-height, 22px);
- transition: all var(--ty-collapse-content-transition-duration, 300ms);
+ gap: var(--ty-collapse-header-gap);
+ min-height: var(--ty-collapse-header-min-height);
+ min-width: 0;
+ padding-inline-end: var(--ty-collapse-header-padding-inline);
+ background: var(--ty-collapse-header-bg);
+ transition:
+ background-color var(--ty-collapse-motion-duration) var(--ty-collapse-motion-easing),
+ color var(--ty-collapse-motion-duration) var(--ty-collapse-motion-easing);
&:hover {
- background-color: var(--ty-collapse-header-hover-bg, #efefef);
+ background: var(--ty-collapse-header-hover-bg);
}
+ }
- &_disabled {
- color: var(--ty-collapse-header-disabled-color, var(--ty-color-text-quaternary));
- cursor: not-allowed;
- }
+ &_active > .#{$prefix}-collapse-item__header {
+ background: var(--ty-collapse-header-active-bg);
}
&__toggle {
- display: flex;
- align-items: center;
flex: 1;
min-width: 0;
- box-sizing: border-box;
- padding: var(--ty-collapse-toggle-padding, 12px 16px);
- cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: var(--ty-collapse-header-gap);
+ min-height: var(--ty-collapse-header-min-height);
+ padding:
+ var(--ty-collapse-header-padding-block)
+ var(--ty-collapse-header-padding-inline);
background: none;
- border: none;
- width: 100%;
- text-align: left;
- font-size: inherit;
- font-family: inherit;
+ border: 0;
color: inherit;
+ font: inherit;
line-height: inherit;
+ text-align: left;
+ cursor: pointer;
+
+ &:disabled {
+ cursor: not-allowed;
+ color: inherit;
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--ty-collapse-focus-ring);
+ outline-offset: -2px;
+ }
}
- &__header_disabled &__toggle {
- cursor: not-allowed;
+ &__icon-slot,
+ &__icon-button {
+ flex: 0 0 auto;
+ width: var(--ty-collapse-icon-slot-size);
+ height: var(--ty-collapse-icon-slot-size);
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--ty-collapse-icon-color);
+ }
+
+ &__icon-button {
+ padding: 0;
+ background: none;
+ border: 0;
+ color: inherit;
+ cursor: pointer;
+ border-radius: 50%;
+
+ &:focus-visible {
+ outline: 2px solid var(--ty-collapse-focus-ring);
+ outline-offset: 2px;
+ }
+
+ &_disabled {
+ cursor: not-allowed;
+ }
}
&__arrow {
- margin-right: var(--ty-collapse-arrow-gap, 10px);
+ color: inherit;
transform: rotate(-90deg);
- text-align: center;
- color: currentcolor;
- transition: all var(--ty-collapse-content-transition-duration, 300ms);
+ transition: transform var(--ty-collapse-motion-duration) var(--ty-collapse-motion-easing);
&_active {
+ color: var(--ty-collapse-icon-active-color);
transform: rotate(0deg);
}
}
- &__title {
+ &__label {
flex: 1;
+ min-width: 0;
+ font-size: inherit;
+ line-height: inherit;
+ color: var(--ty-collapse-header-color);
+
+ &_static {
+ padding-block: var(--ty-collapse-header-padding-block);
+ }
}
&__extra {
- display: flex;
+ flex: 0 0 auto;
+ display: inline-flex;
align-items: center;
- color: inherit;
- font-size: var(--ty-collapse-extra-font-size, 11px);
- padding-right: var(--ty-collapse-extra-padding-inline-end, 16px);
+ gap: var(--ty-collapse-extra-gap);
margin-left: auto;
+ color: var(--ty-collapse-extra-color);
+ font-size: var(--ty-collapse-extra-font-size);
}
- &__content {
- overflow: hidden;
- color: var(--ty-collapse-content-color, var(--ty-color-text-secondary));
- background-color: var(--ty-collapse-content-bg, var(--ty-color-bg-container));
- border-top: 1px solid var(--ty-collapse-border, var(--ty-color-border));
- padding: var(--ty-collapse-content-padding, 16px);
+ &__body-wrapper {
+ border-top: 1px solid var(--ty-collapse-border-color);
+ }
+
+ &__body {
+ padding:
+ var(--ty-collapse-body-padding-block)
+ var(--ty-collapse-body-padding-inline);
+ color: var(--ty-collapse-body-color);
+ background: var(--ty-collapse-body-bg);
box-sizing: border-box;
- transition: height var(--ty-collapse-content-transition-duration, 300ms);
}
}
&_borderless {
border: 0;
- background-color: var(--ty-collapse-borderless-bg, var(--ty-color-bg-container));
- }
+ background: var(--ty-collapse-borderless-bg, transparent);
- &_borderless > .#{$prefix}-collapse-item {
- &:last-child {
- border-radius: 0;
+ > .#{$prefix}-collapse-item {
+ border-bottom-color: var(--ty-collapse-borderless-divider-color);
+ }
+
+ > .#{$prefix}-collapse-item > .#{$prefix}-collapse-item__body-wrapper {
+ border-top: 0;
}
}
- &_borderless > .#{$prefix}-collapse-item > .#{$prefix}-collapse-item__content {
- border-radius: 0;
- border-top: 0;
- padding-top: var(--ty-collapse-borderless-content-padding-top, 4px);
+ &_sm {
+ --ty-collapse-font-size: 13px;
+ --ty-collapse-header-min-height: 36px;
+ --ty-collapse-header-padding-inline: 12px;
+ --ty-collapse-header-padding-block: 10px;
+ --ty-collapse-body-padding-inline: 12px;
+ --ty-collapse-body-padding-block: 12px;
+ --ty-collapse-icon-slot-size: 18px;
+ }
+
+ &_md {
+ --ty-collapse-font-size: 14px;
+ --ty-collapse-header-min-height: 44px;
+ --ty-collapse-header-padding-inline: 16px;
+ --ty-collapse-header-padding-block: 14px;
+ --ty-collapse-body-padding-inline: 16px;
+ --ty-collapse-body-padding-block: 16px;
+ --ty-collapse-icon-slot-size: 20px;
+ }
+
+ &_lg {
+ --ty-collapse-font-size: 15px;
+ --ty-collapse-header-min-height: 52px;
+ --ty-collapse-header-padding-inline: 18px;
+ --ty-collapse-header-padding-block: 16px;
+ --ty-collapse-body-padding-inline: 18px;
+ --ty-collapse-body-padding-block: 18px;
+ --ty-collapse-icon-slot-size: 22px;
}
}
diff --git a/packages/react/src/collapse/types.ts b/packages/react/src/collapse/types.ts
index 9e189b0e1..858bad324 100644
--- a/packages/react/src/collapse/types.ts
+++ b/packages/react/src/collapse/types.ts
@@ -1,27 +1,52 @@
-import React, { ReactNode } from 'react';
-import { BaseProps } from '../_utils/props';
+import React, { CSSProperties, ReactNode } from 'react';
+import { BaseProps, SizeType } from '../_utils/props';
-export interface CollapsePanelProps extends BaseProps {
- itemKey: string;
- header: ReactNode;
+export type CollapseValue = string[];
+export type CollapseCollapsible = 'header' | 'icon' | 'disabled';
+
+export type CollapseRenderState = {
+ active: boolean;
+ disabled: boolean;
+ panelKey: string;
+};
+
+export type CollapseExpandIconRender = (args: CollapseRenderState) => ReactNode;
+export type CollapseHeaderRender = (args: CollapseRenderState) => ReactNode;
+export type CollapseExtraRender = (args: CollapseRenderState) => ReactNode;
+
+export interface CollapseItem {
+ key: string;
+ label: ReactNode | CollapseHeaderRender;
+ children: ReactNode;
+ extra?: ReactNode | CollapseExtraRender;
disabled?: boolean;
- extra?: ReactNode;
- deletable?: boolean;
- showArrow?: boolean;
- onHeaderOnClick?: (e: React.MouseEvent) => void;
- children?: ReactNode;
+ collapsible?: CollapseCollapsible;
+ forceRender?: boolean;
+ destroyOnHidden?: boolean;
+ className?: string;
+ style?: CSSProperties;
}
export interface CollapseProps
extends BaseProps,
- Omit, 'onChange'> {
- defaultActiveKey?: string | string[];
- activeKey?: string | string[];
- /** Only open one panel */
- accordion?: boolean;
- /** Allow to delete */
- deletable?: boolean;
- showArrow?: boolean;
+ Omit, 'children' | 'onChange' | 'defaultValue'> {
+ items: CollapseItem[];
+ value?: CollapseValue;
+ defaultValue?: CollapseValue;
+ onValueChange?: (value: CollapseValue) => void;
+ multiple?: boolean;
bordered?: boolean;
- onChange?: (keys: string | string[]) => void;
+ size?: SizeType;
+ expandIcon?: ReactNode | CollapseExpandIconRender;
+ expandIconPosition?: 'start' | 'end';
+ showArrow?: boolean;
+ disabled?: boolean;
+ collapsible?: CollapseCollapsible;
+ destroyOnHidden?: boolean;
+ forceRender?: boolean;
+ itemClassName?: string;
+ itemStyle?: CSSProperties;
+ headerClassName?: string;
+ bodyClassName?: string;
+ onItemClick?: (key: string, event: React.MouseEvent) => void;
}
diff --git a/packages/react/src/tree/__tests__/__snapshots__/tree.test.tsx.snap b/packages/react/src/tree/__tests__/__snapshots__/tree.test.tsx.snap
index fabac959b..82a7ffed4 100644
--- a/packages/react/src/tree/__tests__/__snapshots__/tree.test.tsx.snap
+++ b/packages/react/src/tree/__tests__/__snapshots__/tree.test.tsx.snap
@@ -39,7 +39,7 @@ exports[` should match the snapshot 1`] = `