feat: ConfigProvider support classNames and styles for treeSelect (#53229)

* feat: ConfigProvider support classNames and styles for treeSelect

* fix

* fix

* fix

* update zIndex

* fix

* add test
This commit is contained in:
thinkasany
2025-03-25 19:14:35 +08:00
committed by GitHub
parent ffe0cce347
commit d5f0fc1941
19 changed files with 242 additions and 21 deletions

View File

@@ -287,7 +287,8 @@ export type InputNumberConfig = ComponentStyleConfig & Pick<InputNumberProps, 'v
export type CascaderConfig = ComponentStyleConfig & Pick<CascaderProps, 'variant'>;
export type TreeSelectConfig = ComponentStyleConfig & Pick<TreeSelectProps, 'variant'>;
export type TreeSelectConfig = ComponentStyleConfig &
Pick<TreeSelectProps, 'variant' | 'classNames' | 'styles'>;
export type TreeConfig = ComponentStyleConfig & Pick<TreeProps, 'classNames' | 'styles'>;

View File

@@ -325,7 +325,7 @@ const Page: React.FC<{ placement: Placement }> = ({ placement }) => {
<TreeSelect
showSearch
style={{ width: '100%' }}
popupStyle={{ maxHeight: 400, overflow: 'auto' }}
styles={{ popup: { maxHeight: 400, overflow: 'auto' } }}
placeholder="Please select"
allowClear
treeDefaultExpandAll

View File

@@ -171,6 +171,7 @@ const {
| popconfirm | Set Popconfirm common props | { className?: string, style?: React.CSSProperties, classNames?:[Popconfirm\["classNames"\]](/components/popconfirm#api), styles?: [Popconfirm\["styles"\]](/components/popconfirm#api), arrow: boolean \| { pointAtCenter: boolean } } | - | 5.23.0, `arrow`: 6.0.0 |
| transfer | Set Transfer common props | { className?: string, style?: React.CSSProperties, selectionsIcon?: React.ReactNode } | - | 5.7.0, `selectionsIcon`: 5.14.0 |
| tree | Set Tree common props | { className?: string, style?: React.CSSProperties, classNames?: [TreeConfig\["classNames"\]](/components/tree#semantic-dom), styles?: [TreeConfig\["styles"\]](/components/tree#semantic-dom) } | - | 5.7.0, `classNames` and `styles`: 6.0.0 |
| treeSelect | Set TreeSelect common props | { className?: string, style?: React.CSSProperties, classNames?: [TreeSelectConfig\["classNames"\]](/components/tree-select#semantic-dom), styles?: [TreeSelectConfig\["styles"\]](/components/tree-select#semantic-dom) } | - | |
| typography | Set Typography common props | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| upload | Set Upload common props | { className?: string, style?: React.CSSProperties, classNames?:[UploadConfig\["classNames"\]](/components/upload#semantic-dom), styles?: [UploadConfig\["styles"\]](/components/upload#semantic-dom) } | - | 5.7.0, `classNames` and `styles`: 6.0.0 |
| wave | Config wave effect | { disabled?: boolean, showEffect?: (node: HTMLElement, info: { className, token, component }) => void } | - | 5.8.0 |

View File

@@ -173,6 +173,7 @@ const {
| popconfirm | 设置 Popconfirm 组件的通用属性 | { className?: string, style?: React.CSSProperties, classNames?:[Popconfirm\["classNames"\]](/components/popconfirm-cn#api), styles?: [Popconfirm\["styles"\]](/components/popconfirm-cn#api), arrow: boolean \| { pointAtCenter: boolean } } | - | 5.23.0, `arrow`: 6.0.0 |
| transfer | 设置 Transfer 组件的通用属性 | { className?: string, style?: React.CSSProperties, selectionsIcon?: React.ReactNode } | - | 5.7.0, `selectionsIcon`: 5.14.0 |
| tree | 设置 Tree 组件的通用属性 | { className?: string, style?: React.CSSProperties, classNames?: [TreeConfig\["classNames"\]](/components/tree-cn#semantic-dom), styles?: [TreeConfig\["styles"\]](/components/tree-cn#semantic-dom) } | - | 5.7.0, `classNames``styles`: 6.0.0 |
| treeSelect | 设置 TreeSelect 组件的通用属性 | { className?: string, style?: React.CSSProperties, classNames?: [TreeSelectConfig\["classNames"\]](/components/tree-select-cn#semantic-dom), styles?: [TreeSelectConfig\["styles"\]](/components/tree-select-cn#semantic-dom) } | - | |
| typography | 设置 Typography 组件的通用属性 | { className?: string, style?: React.CSSProperties } | - | 5.7.0 |
| upload | 设置 Upload 组件的通用属性 | { className?: string, style?: React.CSSProperties, classNames?:[UploadConfig\["classNames"\]](/components/upload-cn#semantic-dom), styles?: [UploadConfig\["styles"\]](/components/upload-cn#semantic-dom) } | - | 5.7.0, `classNames``styles`: 6.0.0 |
| wave | 设置水波纹特效 | { disabled?: boolean, showEffect?: (node: HTMLElement, info: { className, token, component }) => void } | - | 5.8.0 |

View File

@@ -36,7 +36,7 @@ const Block = (prop: any) => {
placement="bottomLeft"
suffixIcon={<SmileOutlined />}
defaultValue="thinkasany"
styles={{ root: { zIndex: 1000, width: 200 } }}
styles={{ root: { zIndex: 1, width: 200 }, popup: { zIndex: 1 } }}
getPopupContainer={() => divRef.current}
options={[
{ value: 'thinkasany', label: 'thinkasany' },

View File

@@ -175,7 +175,7 @@ const App: React.FC = () => (
showSearch
style={{ width: '60%' }}
value="leaf1"
popupStyle={{ maxHeight: 400, overflow: 'auto' }}
styles={{ popup: { maxHeight: 400, overflow: 'auto' } }}
placeholder="Please select"
allowClear
treeDefaultExpandAll

View File

@@ -1,4 +1,5 @@
import React from 'react';
import { SmileOutlined } from '@ant-design/icons';
import TreeSelect, { TreeNode } from '..';
import { resetWarned } from '../../_util/warning';
@@ -90,4 +91,84 @@ describe('TreeSelect', () => {
errSpy.mockRestore();
});
it('support classNames and styles', () => {
const treeData = [
{
value: 'parent 1',
title: 'parent 1',
children: [
{
value: 'parent 1-0',
title: 'parent 1-0',
children: [
{
value: 'leaf1',
title: 'my leaf',
},
{
value: 'leaf2',
title: 'your leaf',
},
],
},
],
},
];
const customClassNames = {
root: 'test-root',
prefix: 'test-prefix',
input: 'test-input',
suffix: 'test-suffix',
item: 'test-item',
itemTitle: 'test-item-title',
popup: 'test-popup',
};
const customStyles = {
root: { backgroundColor: 'red' },
prefix: { color: 'green' },
input: { color: 'blue' },
suffix: { color: 'yellow' },
item: { color: 'black' },
itemTitle: { color: 'purple' },
popup: { color: 'orange' },
};
const { container } = render(
<TreeSelect
classNames={customClassNames}
styles={customStyles}
showSearch
prefix="Prefix"
open
suffixIcon={<SmileOutlined />}
placeholder="Please select"
treeDefaultExpandAll
treeData={treeData}
/>,
);
const prefix = container.querySelector('.ant-select-prefix');
const input = container.querySelector('.ant-select-selection-search-input');
const suffix = container.querySelector('.ant-select-arrow');
const popup = container.querySelector('.ant-tree-select-dropdown');
const itemTitle = container.querySelector('.ant-select-tree-title');
const root = container.querySelector('.ant-tree-select-dropdown');
const selectRoot = container.querySelector('.ant-tree-select');
const item = container.querySelector(`.${customClassNames.item}`);
expect(prefix).toHaveClass(customClassNames.prefix);
expect(input).toHaveClass(customClassNames.input);
expect(suffix).toHaveClass(customClassNames.suffix);
expect(popup).toHaveClass(customClassNames.popup);
expect(itemTitle).toHaveClass(customClassNames.itemTitle);
expect(root).toHaveClass(customClassNames.root);
expect(selectRoot).toHaveClass(customClassNames.root);
expect(prefix).toHaveStyle(customStyles.prefix);
expect(input).toHaveStyle(customStyles.input);
expect(suffix).toHaveStyle(customStyles.suffix);
expect(popup).toHaveStyle(customStyles.popup);
expect(itemTitle).toHaveStyle(customStyles.itemTitle);
expect(root).toHaveStyle(customStyles.root);
expect(selectRoot).toHaveStyle(customStyles.root);
expect(item).toHaveStyle(customStyles.item);
});
});

View File

@@ -0,0 +1,106 @@
import React from 'react';
import { SmileOutlined } from '@ant-design/icons';
import { TreeSelect } from 'antd';
import SemanticPreview from '../../../.dumi/components/SemanticPreview';
import useLocale from '../../../.dumi/hooks/useLocale';
const locales = {
cn: {
root: '根元素',
prefix: '前缀元素',
input: '输入框元素',
suffix: '后缀元素',
item: '条目元素',
itemTitle: '标题元素',
popup: '弹出菜单元素',
},
en: {
root: 'Root element',
prefix: 'Prefix element',
input: 'Input element',
suffix: 'Suffix element',
item: 'Item element',
itemTitle: 'title element',
popup: 'Popup element',
},
};
const icon = <SmileOutlined />;
const treeData = [
{
value: 'parent 1',
title: 'parent 1',
children: [
{
value: 'parent 1-0',
title: 'parent 1-0',
children: [
{
value: 'leaf1',
title: 'my leaf',
},
{
value: 'leaf2',
title: 'your leaf',
},
],
},
],
},
];
const Block = (props: any) => {
const divRef = React.useRef<HTMLDivElement>(null);
const [value, setValue] = React.useState<string>();
const onChange = (newValue: string) => {
setValue(newValue);
};
return (
<div ref={divRef}>
<TreeSelect
{...props}
getPopupContainer={() => divRef.current}
showSearch
placement="bottomLeft"
prefix="Prefix"
open
suffixIcon={icon}
styles={{
root: { zIndex: 1 },
popup: {
zIndex: 1,
maxHeight: 400,
overflow: 'auto',
},
}}
value={value}
placeholder="Please select"
allowClear
treeDefaultExpandAll
onChange={onChange}
treeData={treeData}
/>
</div>
);
};
const App: React.FC = () => {
const [locale] = useLocale(locales);
return (
<SemanticPreview
semantics={[
{ name: 'root', desc: locale.root, version: '6.0.0' },
{ name: 'prefix', desc: locale.prefix, version: '6.0.0' },
{ name: 'input', desc: locale.input, version: '6.0.0' },
{ name: 'suffix', desc: locale.suffix, version: '6.0.0' },
{ name: 'popup', desc: locale.popup, version: '6.0.0' },
{ name: 'item', desc: locale.item, version: '6.0.0' },
{ name: 'itemTitle', desc: locale.itemTitle, version: '6.0.0' },
]}
>
<Block />
</SemanticPreview>
);
};
export default App;

View File

@@ -43,7 +43,7 @@ const App: React.FC = () => {
treeDataSimpleMode
style={{ width: '100%' }}
value={value}
popupStyle={{ maxHeight: 400, overflow: 'auto' }}
styles={{ popup: { maxHeight: 400, overflow: 'auto' } }}
placeholder="Please select"
onChange={onChange}
loadData={onLoadData}

View File

@@ -66,7 +66,7 @@ const App: React.FC = () => {
showSearch
style={{ width: '100%' }}
value={value}
popupStyle={{ maxHeight: 400, overflow: 'auto' }}
styles={{ popup: { maxHeight: 400, overflow: 'auto' } }}
placeholder="Please select"
allowClear
treeDefaultExpandAll

View File

@@ -55,7 +55,7 @@ const App: React.FC = () => {
showSearch
style={{ width: '100%' }}
value={value}
popupStyle={{ maxHeight: 400, overflow: 'auto' }}
styles={{ popup: { maxHeight: 400, overflow: 'auto' } }}
placeholder="Please select"
allowClear
treeDefaultExpandAll

View File

@@ -46,7 +46,7 @@ const App: React.FC = () => {
showSearch
style={{ width: '100%' }}
value={value}
popupStyle={{ maxHeight: 400, overflow: 'auto' }}
styles={{ popup: { maxHeight: 400, overflow: 'auto' } }}
placeholder="Please select"
allowClear
multiple

View File

@@ -56,7 +56,7 @@ const App: React.FC = () => {
<TreeSelect
showSearch
popupStyle={{ maxHeight: 400, overflow: 'auto', minWidth: 300 }}
styles={{ popup: { maxHeight: 400, overflow: 'auto', minWidth: 300 } }}
placeholder="Please select"
popupMatchSelectWidth={false}
placement={placement}

View File

@@ -51,7 +51,7 @@ const App: React.FC = () => {
suffixIcon={icon}
style={{ width: '100%' }}
value={value}
popupStyle={{ maxHeight: 400, overflow: 'auto' }}
styles={{ popup: { maxHeight: 400, overflow: 'auto' } }}
placeholder="Please select"
allowClear
treeDefaultExpandAll
@@ -65,7 +65,7 @@ const App: React.FC = () => {
prefix="Prefix"
style={{ width: '100%' }}
value={value}
popupStyle={{ maxHeight: 400, overflow: 'auto' }}
styles={{ popup: { maxHeight: 400, overflow: 'auto' } }}
placeholder="Please select"
allowClear
treeDefaultExpandAll

View File

@@ -34,7 +34,7 @@ const App: React.FC = () => {
<TreeSelect
style={{ width: '100%' }}
value={value}
popupStyle={{ maxHeight: 400, overflow: 'auto' }}
styles={{ popup: { maxHeight: 400, overflow: 'auto' } }}
treeData={treeData}
placeholder="Please select"
treeDefaultExpandAll

View File

@@ -115,6 +115,10 @@ Common props ref[Common props](/docs/react/common-props)
| title | Content showed on the treeNodes | ReactNode | `---` | |
| value | Will be treated as `treeNodeFilterProp` by default, should be unique in the tree | string | - | |
## Semantic DOM
<code src="./demo/_semantic.tsx" simplify="true"></code>
## Design Token
<ComponentTokenTable component="TreeSelect"></ComponentTokenTable>

View File

@@ -1,6 +1,4 @@
import * as React from 'react';
import omit from '@rc-component/util/lib/omit';
import classNames from 'classnames';
import type { BaseSelectRef } from '@rc-component/select';
import type { Placement } from '@rc-component/select/lib/BaseSelect';
import type { TreeSelectProps as RcTreeSelectProps } from '@rc-component/tree-select';
@@ -11,7 +9,10 @@ import RcTreeSelect, {
TreeNode,
} from '@rc-component/tree-select';
import type { DataNode } from '@rc-component/tree-select/lib/interface';
import omit from '@rc-component/util/lib/omit';
import classNames from 'classnames';
import useMergeSemantic from '../_util/hooks/useMergeSemantic';
import { useZIndex } from '../_util/hooks/useZIndex';
import type { SelectCommonPlacement } from '../_util/motion';
import { getTransitionName } from '../_util/motion';
@@ -21,6 +22,7 @@ import { getMergedStatus, getStatusClassNames } from '../_util/statusUtils';
import { devUseWarning } from '../_util/warning';
import { ConfigContext } from '../config-provider';
import type { Variant } from '../config-provider';
import { useComponentConfig } from '../config-provider/context';
import DefaultRenderEmpty from '../config-provider/defaultRenderEmpty';
import DisabledContext from '../config-provider/DisabledContext';
import useCSSVarCls from '../config-provider/hooks/useCSSVarCls';
@@ -49,6 +51,7 @@ export interface LabeledValue {
export type SelectValue = RawValue | RawValue[] | LabeledValue | LabeledValue[];
type SemanticName = 'root' | 'prefix' | 'input' | 'suffix' | 'item' | 'itemTitle' | 'popup';
export interface TreeSelectProps<ValueType = any, OptionType extends DataNode = DataNode>
extends Omit<
RcTreeSelectProps<ValueType, OptionType>,
@@ -60,6 +63,8 @@ export interface TreeSelectProps<ValueType = any, OptionType extends DataNode =
| 'treeLine'
| 'switcherIcon'
> {
classNames?: Partial<Record<SemanticName, string>>;
styles?: Partial<Record<SemanticName, React.CSSProperties>>;
suffixIcon?: React.ReactNode;
size?: SizeType;
disabled?: boolean;
@@ -118,18 +123,25 @@ const InternalTreeSelect = <ValueType = any, OptionType extends DataNode = DataN
popupMatchSelectWidth,
allowClear,
variant: customVariant,
popupStyle,
tagRender,
maxCount,
showCheckedStrategy,
treeCheckStrictly,
style,
classNames: treeSelectClassNames,
styles,
...restProps
} = props;
const {
getPopupContainer: getContextPopupContainer,
getPrefixCls,
renderEmpty,
getPopupContainer: getContextPopupContainer,
direction,
styles: contextStyles,
classNames: contextClassNames,
} = useComponentConfig('treeSelect');
const {
renderEmpty,
virtual,
popupMatchSelectWidth: contextPopupMatchSelectWidth,
popupOverflow,
@@ -175,6 +187,11 @@ const InternalTreeSelect = <ValueType = any, OptionType extends DataNode = DataN
const [variant, enableVariantCls] = useVariant('treeSelect', customVariant, bordered);
const [mergedClassNames, mergedStyles] = useMergeSemantic(
[contextClassNames, treeSelectClassNames],
[contextStyles, styles],
);
const mergedDropdownClassName = classNames(
popupClassName,
`${treeSelectPrefixCls}-dropdown`,
@@ -182,6 +199,8 @@ const InternalTreeSelect = <ValueType = any, OptionType extends DataNode = DataN
[`${treeSelectPrefixCls}-dropdown-rtl`]: direction === 'rtl',
},
rootClassName,
mergedClassNames?.root,
mergedClassNames?.popup,
cssVarCls,
rootCls,
treeSelectRootCls,
@@ -272,6 +291,7 @@ const InternalTreeSelect = <ValueType = any, OptionType extends DataNode = DataN
compactItemClassnames,
className,
rootClassName,
mergedClassNames?.root,
cssVarCls,
rootCls,
treeSelectRootCls,
@@ -288,10 +308,12 @@ const InternalTreeSelect = <ValueType = any, OptionType extends DataNode = DataN
);
// ============================ zIndex ============================
const [zIndex] = useZIndex('SelectLike', popupStyle?.zIndex as number);
const [zIndex] = useZIndex('SelectLike', mergedStyles?.popup?.zIndex as number);
return (
<RcTreeSelect
classNames={mergedClassNames}
styles={mergedStyles}
virtual={virtual}
disabled={mergedDisabled}
{...selectProps}
@@ -300,6 +322,7 @@ const InternalTreeSelect = <ValueType = any, OptionType extends DataNode = DataN
ref={ref}
prefixCls={prefixCls}
className={mergedClassName}
style={{ ...mergedStyles?.root, ...style }}
listHeight={listHeight}
listItemHeight={listItemHeight}
treeCheckable={
@@ -317,7 +340,7 @@ const InternalTreeSelect = <ValueType = any, OptionType extends DataNode = DataN
getPopupContainer={getPopupContainer || getContextPopupContainer}
treeMotion={null}
popupClassName={mergedDropdownClassName}
popupStyle={{ ...popupStyle, zIndex }}
popupStyle={{ ...mergedStyles?.root, ...mergedStyles?.popup, zIndex }}
choiceTransitionName={getTransitionName(rootPrefixCls, '', choiceTransitionName)}
transitionName={getTransitionName(rootPrefixCls, 'slide-up', transitionName)}
treeExpandAction={treeExpandAction}

View File

@@ -116,6 +116,10 @@ demo:
| title | 树节点显示的内容 | ReactNode | `---` | |
| value | 默认根据此属性值进行筛选(其值在整个树范围内唯一) | string | - | |
## Semantic DOM
<code src="./demo/_semantic.tsx" simplify="true"></code>
## 主题变量Design Token
<ComponentTokenTable component="TreeSelect"></ComponentTokenTable>

View File

@@ -131,7 +131,7 @@
"@rc-component/qrcode": "~1.0.0",
"@rc-component/resize-observer": "^1.0.0",
"@rc-component/segmented": "~1.1.0",
"@rc-component/select": "~1.0.1",
"@rc-component/select": "~1.0.2",
"@rc-component/switch": "~1.0.0",
"@rc-component/table": "~1.2.7",
"@rc-component/tabs": "~1.3.0",
@@ -139,7 +139,7 @@
"@rc-component/tooltip": "~1.1.0",
"@rc-component/tour": "~2.1.3",
"@rc-component/tree": "~1.0.1",
"@rc-component/tree-select": "~1.1.0",
"@rc-component/tree-select": "~1.1.1",
"@rc-component/trigger": "~3.1.0",
"@rc-component/util": "^1.0.1",
"classnames": "^2.5.1",