From 9f7f9cbe6ba3f8ff9119ee1fff52c647734b6652 Mon Sep 17 00:00:00 2001 From: Tao Lei <108162283+zizairufengLT@users.noreply.github.com> Date: Mon, 3 Jun 2024 10:47:32 +0800 Subject: [PATCH] refactor(TreeSelect): convert to TypeScript, improve docs and tests, close #4620 (#4837) --- .../tree-select/__docs__/adaptor/index.jsx | 112 --- .../tree-select/__docs__/adaptor/index.tsx | 144 ++++ .../__docs__/demo/accessibility/index.tsx | 11 +- .../tree-select/__docs__/demo/basic/index.tsx | 11 +- .../tree-select/__docs__/demo/check/index.tsx | 11 +- .../__docs__/demo/control/index.tsx | 17 +- .../tree-select/__docs__/demo/data/index.tsx | 11 +- .../__docs__/demo/inline/index.tsx | 17 +- .../demo/non-existent-value/index.tsx | 11 +- .../__docs__/demo/pro-search/index.tsx | 22 +- .../__docs__/demo/search/index.tsx | 17 +- .../__docs__/demo/select/index.tsx | 20 +- .../__docs__/demo/virtual-tree/index.tsx | 24 +- .../tree-select/__docs__/index.en-us.md | 88 +- components/tree-select/__docs__/index.md | 93 ++- .../__docs__/theme/{index.jsx => index.tsx} | 121 ++- .../{index-spec.js => index-spec.tsx} | 780 +++++++++--------- components/tree-select/index.d.ts | 252 ------ .../tree-select/{index.jsx => index.tsx} | 13 +- .../mobile/{index.jsx => index.tsx} | 1 + components/tree-select/{style.js => style.ts} | 0 .../{tree-select.jsx => tree-select.tsx} | 400 ++++----- components/tree-select/types.ts | 375 +++++++++ components/tree/__docs__/index.en-us.md | 84 +- components/tree/__docs__/index.md | 84 +- components/tree/types.ts | 8 + components/tree/view/util.ts | 1 + 27 files changed, 1397 insertions(+), 1331 deletions(-) delete mode 100644 components/tree-select/__docs__/adaptor/index.jsx create mode 100644 components/tree-select/__docs__/adaptor/index.tsx rename components/tree-select/__docs__/theme/{index.jsx => index.tsx} (53%) rename components/tree-select/__tests__/{index-spec.js => index-spec.tsx} (52%) delete mode 100644 components/tree-select/index.d.ts rename components/tree-select/{index.jsx => index.tsx} (52%) rename components/tree-select/mobile/{index.jsx => index.tsx} (77%) rename components/tree-select/{style.js => style.ts} (100%) rename components/tree-select/{tree-select.jsx => tree-select.tsx} (72%) create mode 100644 components/tree-select/types.ts diff --git a/components/tree-select/__docs__/adaptor/index.jsx b/components/tree-select/__docs__/adaptor/index.jsx deleted file mode 100644 index c0db1fcfc1..0000000000 --- a/components/tree-select/__docs__/adaptor/index.jsx +++ /dev/null @@ -1,112 +0,0 @@ -import React from 'react'; -import { Types, parseData, NodeType } from '@alifd/adaptor-helper'; -import { TreeSelect } from '@alifd/next'; - -const createDataSouce = (list, keys = { selected: [], expanded: [] }, level = 0, prefix='') => { - const array = []; - let index = 0; - - list.forEach((item) => { - const key = `${prefix || level }-${index++}`; - - if (item.children && item.children.length > 0) { - item.children = createDataSouce(item.children, keys, level + 1, key); - } - array.push({ - label: item.value, - value: key, - disabled: item.state === 'disabled', - key, - children: item.children - }); - - if (item.state === 'active') { - if (item.children && item.children.length > 0) { - keys.expanded.push(key); - } else { - keys.selected.push(key); - } - } - - return; - }); - - return array; -}; - -export default { - name: 'TreeSelect', - editor: () => ({ - props: [{ - name: 'state', - label: 'Status', - type: Types.enum, - options: ['normal', 'expanded', 'disabled'], - default: 'normal' - }, { - name: 'size', - type: Types.enum, - options: ['large', 'medium', 'small'], - default: 'medium' - }, { - name: 'width', - type: Types.number, - default: 300, - }, { - name: 'border', - type: Types.bool, - default: true - }, { - name: 'checkbox', - type: Types.bool, - default: false - }, { - name: 'label', - type: Types.string, - default: '' - }, { - name: 'placeholder', - type: Types.string, - default: 'Please Select' - }], - data: { - active: true, - disabled: true, - default: '*Trunk\n\t-Branch\n\t\t*Branch\n\t\t\tLeaf\n\t\tLeaf\n\t*Branch\n\t\tLeaf\n\t\tLeaf' - } - }), - adaptor: ({ state, size, width, border, checkbox, label, placeholder, data, style, ...others }) => { - const list = parseData(data).filter(({ type }) => type === NodeType.node); - const keys = { selected: [], expanded: [] }; - const dataSource = createDataSouce(list, keys); - - const props = { - ...others, - style: { width, ...style }, - size, - dataSource, - key: new Date().getTime(), - multiple: checkbox, - treeCheckable: checkbox, - treeDefaultExpandAll: true, - hasBorder: border, - disabled: state === 'disabled', - visible: state === 'expanded', - label, - placeholder, - popupContainer: node => node, - popupProps: { needAdjust: false }, - value: checkbox ? keys.selected : keys.selected[0], - }; - - return ( - - ); - }, - demoOptions: (demo) => { - if (demo.node.props.state === 'expanded') { - demo.height = 300; - } - return demo; - } -}; diff --git a/components/tree-select/__docs__/adaptor/index.tsx b/components/tree-select/__docs__/adaptor/index.tsx new file mode 100644 index 0000000000..55e3ff1156 --- /dev/null +++ b/components/tree-select/__docs__/adaptor/index.tsx @@ -0,0 +1,144 @@ +import React from 'react'; +import { Types, parseData, NodeType } from '@alifd/adaptor-helper'; +import { TreeSelect } from '@alifd/next'; +import type { TreeSelectProps } from '@alifd/next/lib/tree-select'; + +const createDataSouce = ( + list: any[], + keys: { selected: string[]; expanded: string[] } = { selected: [], expanded: [] }, + level = 0, + prefix = '' +) => { + const array: any[] = []; + let index = 0; + + list.forEach(item => { + const key = `${prefix || level}-${index++}`; + + if (item.children && item.children.length > 0) { + item.children = createDataSouce(item.children, keys, level + 1, key); + } + array.push({ + label: item.value, + value: key, + disabled: item.state === 'disabled', + key, + children: item.children, + }); + + if (item.state === 'active') { + if (item.children && item.children.length > 0) { + keys.expanded.push(key); + } else { + keys.selected.push(key); + } + } + + return; + }); + + return array; +}; + +export default { + name: 'TreeSelect', + editor: () => ({ + props: [ + { + name: 'state', + label: 'Status', + type: Types.enum, + options: ['normal', 'expanded', 'disabled'], + default: 'normal', + }, + { + name: 'size', + type: Types.enum, + options: ['large', 'medium', 'small'], + default: 'medium', + }, + { + name: 'width', + type: Types.number, + default: 300, + }, + { + name: 'border', + type: Types.bool, + default: true, + }, + { + name: 'checkbox', + type: Types.bool, + default: false, + }, + { + name: 'label', + type: Types.string, + default: '', + }, + { + name: 'placeholder', + type: Types.string, + default: 'Please Select', + }, + ], + data: { + active: true, + disabled: true, + default: + '*Trunk\n\t-Branch\n\t\t*Branch\n\t\t\tLeaf\n\t\tLeaf\n\t*Branch\n\t\tLeaf\n\t\tLeaf', + }, + }), + adaptor: ({ + state, + size, + width, + border, + checkbox, + label, + placeholder, + data, + style, + ...others + }: TreeSelectProps & { + state: 'normal' | 'expanded' | 'disabled'; + width: number; + border: boolean; + shape: 'normal' | 'line'; + select: 'node' | 'label'; + checkbox: boolean; + data: string; + }) => { + const list = parseData(data).filter(({ type }: any) => type === NodeType.node); + const keys = { selected: [], expanded: [] }; + const dataSource = createDataSouce(list, keys); + + const props = { + ...others, + style: { width, ...style }, + size, + dataSource, + key: new Date().getTime(), + multiple: checkbox, + treeCheckable: checkbox, + treeDefaultExpandAll: true, + hasBorder: border, + disabled: state === 'disabled', + visible: state === 'expanded', + label, + placeholder, + popupContainer: (node: HTMLElement) => node, + popupProps: { needAdjust: false }, + value: checkbox ? keys.selected : keys.selected[0], + }; + + return ; + }, + demoOptions: (demo: any) => { + if (demo.node.props.state === 'expanded') { + demo.height = 300; + } + return demo; + }, +}; diff --git a/components/tree-select/__docs__/demo/accessibility/index.tsx b/components/tree-select/__docs__/demo/accessibility/index.tsx index be017d8919..20925b418e 100644 --- a/components/tree-select/__docs__/demo/accessibility/index.tsx +++ b/components/tree-select/__docs__/demo/accessibility/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { TreeSelect } from '@alifd/next'; +import type { TreeSelectProps } from '@alifd/next/lib/tree-select'; const data = [ { @@ -37,15 +38,9 @@ const data = [ ]; class Demo extends React.Component { - constructor(props) { - super(props); - - this.handleChange = this.handleChange.bind(this); - } - - handleChange(value, data) { + handleChange: TreeSelectProps['onChange'] = (value, data) => { console.log(value, data); - } + }; render() { return ( diff --git a/components/tree-select/__docs__/demo/basic/index.tsx b/components/tree-select/__docs__/demo/basic/index.tsx index e7a4100d76..077c9fcee0 100644 --- a/components/tree-select/__docs__/demo/basic/index.tsx +++ b/components/tree-select/__docs__/demo/basic/index.tsx @@ -2,19 +2,14 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { TreeSelect } from '@alifd/next'; +import type { TreeSelectProps } from '@alifd/next/lib/tree-select'; const TreeNode = TreeSelect.Node; class Demo extends React.Component { - constructor(props) { - super(props); - - this.handleChange = this.handleChange.bind(this); - } - - handleChange(value, data) { + handleChange: TreeSelectProps['onChange'] = (value, data) => { console.log(value, data); - } + }; render() { return ( diff --git a/components/tree-select/__docs__/demo/check/index.tsx b/components/tree-select/__docs__/demo/check/index.tsx index f144a48fb2..a92961881b 100644 --- a/components/tree-select/__docs__/demo/check/index.tsx +++ b/components/tree-select/__docs__/demo/check/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { TreeSelect } from '@alifd/next'; +import type { TreeSelectProps } from '@alifd/next/lib/tree-select'; const treeData = [ { @@ -36,15 +37,9 @@ const treeData = [ ]; class Demo extends React.Component { - constructor(props) { - super(props); - - this.handleChange = this.handleChange.bind(this); - } - - handleChange(value, data) { + handleChange: TreeSelectProps['onChange'] = (value, data) => { console.log(value, data); - } + }; render() { return ( diff --git a/components/tree-select/__docs__/demo/control/index.tsx b/components/tree-select/__docs__/demo/control/index.tsx index 8ed49df2e2..451865bbd6 100644 --- a/components/tree-select/__docs__/demo/control/index.tsx +++ b/components/tree-select/__docs__/demo/control/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { TreeSelect } from '@alifd/next'; +import type { TreeSelectProps } from '@alifd/next/lib/tree-select'; const treeData = [ { @@ -36,22 +37,16 @@ const treeData = [ ]; class Demo extends React.Component { - constructor(props) { - super(props); + state = { + value: ['4', '6'], + }; - this.state = { - value: ['4', '6'], - }; - - this.handleChange = this.handleChange.bind(this); - } - - handleChange(value, data) { + handleChange: TreeSelectProps['onChange'] = (value, data) => { console.log(value, data); this.setState({ value, }); - } + }; render() { return ( diff --git a/components/tree-select/__docs__/demo/data/index.tsx b/components/tree-select/__docs__/demo/data/index.tsx index 4a7b1e6da6..7d9aa2e596 100644 --- a/components/tree-select/__docs__/demo/data/index.tsx +++ b/components/tree-select/__docs__/demo/data/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { TreeSelect } from '@alifd/next'; +import type { TreeSelectProps } from '@alifd/next/lib/tree-select'; const treeData = [ { @@ -36,15 +37,9 @@ const treeData = [ }, ]; class Demo extends React.Component { - constructor(props) { - super(props); - - this.handleChange = this.handleChange.bind(this); - } - - handleChange(value, data) { + handleChange: TreeSelectProps['onChange'] = (value, data) => { console.log(value, data); - } + }; render() { return ( diff --git a/components/tree-select/__docs__/demo/inline/index.tsx b/components/tree-select/__docs__/demo/inline/index.tsx index 8610c54cd2..238955ffb9 100644 --- a/components/tree-select/__docs__/demo/inline/index.tsx +++ b/components/tree-select/__docs__/demo/inline/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { TreeSelect } from '@alifd/next'; +import type { TreeSelectProps } from '@alifd/next/lib/tree-select'; const treeData = [ { @@ -44,22 +45,16 @@ const treeData = [ ]; class Demo extends React.Component { - constructor(props) { - super(props); + state = { + value: ['4', '6'], + }; - this.state = { - value: ['4', '6'], - }; - - this.handleChange = this.handleChange.bind(this); - } - - handleChange(value, data) { + handleChange: TreeSelectProps['onChange'] = (value, data) => { console.log(value, data); this.setState({ value, }); - } + }; render() { return ( diff --git a/components/tree-select/__docs__/demo/non-existent-value/index.tsx b/components/tree-select/__docs__/demo/non-existent-value/index.tsx index dbe8094d86..bf5cbaf80e 100644 --- a/components/tree-select/__docs__/demo/non-existent-value/index.tsx +++ b/components/tree-select/__docs__/demo/non-existent-value/index.tsx @@ -2,6 +2,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { TreeSelect } from '@alifd/next'; +import type { TreeSelectProps } from '@alifd/next/lib/tree-select'; const treeData = [ { @@ -37,15 +38,9 @@ const treeData = [ ]; class Demo extends React.Component { - constructor(props) { - super(props); - - this.handleChange = this.handleChange.bind(this); - } - - handleChange(value, data) { + handleChange: TreeSelectProps['onChange'] = (value, data) => { console.log(value, data); - } + }; render() { return ( diff --git a/components/tree-select/__docs__/demo/pro-search/index.tsx b/components/tree-select/__docs__/demo/pro-search/index.tsx index 22a01ad33c..399be1783a 100644 --- a/components/tree-select/__docs__/demo/pro-search/index.tsx +++ b/components/tree-select/__docs__/demo/pro-search/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { TreeSelect } from '@alifd/next'; +import type { TreeSelectProps } from '@alifd/next/lib/tree-select'; const defaultTreeData = [ { @@ -16,22 +17,17 @@ const defaultTreeData = [ ]; class Demo extends React.Component { - constructor(props) { - super(props); + timeId: number; + state = { + value: ['浙江'], + treeData: defaultTreeData, + }; - this.state = { - value: ['浙江'], - treeData: defaultTreeData, - }; - - this.handleSearch = this.handleSearch.bind(this); - } - - handleSearch(searchVal, data) { + handleSearch: TreeSelectProps['onSearch'] = searchVal => { clearTimeout(this.timeId); if (searchVal) { - this.timeId = setTimeout(() => { + this.timeId = window.setTimeout(() => { this.setState({ treeData: [ { @@ -46,7 +42,7 @@ class Demo extends React.Component { treeData: defaultTreeData, }); } - } + }; render() { return ( diff --git a/components/tree-select/__docs__/demo/search/index.tsx b/components/tree-select/__docs__/demo/search/index.tsx index cb0d182c97..c010b02a8f 100644 --- a/components/tree-select/__docs__/demo/search/index.tsx +++ b/components/tree-select/__docs__/demo/search/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { TreeSelect } from '@alifd/next'; +import type { TreeSelectProps } from '@alifd/next/lib/tree-select'; const treeData = [ { @@ -38,19 +39,13 @@ const treeData = [ ]; class Demo extends React.Component { - constructor(props) { - super(props); + state = { + value: ['4', '6'], + }; - this.state = { - value: ['4', '6'], - }; - - this.handleChange = this.handleChange.bind(this); - } - - handleChange(value, data) { + handleChange: TreeSelectProps['onChange'] = (value, data) => { console.log(value, data); - } + }; render() { return ( diff --git a/components/tree-select/__docs__/demo/select/index.tsx b/components/tree-select/__docs__/demo/select/index.tsx index f49b419fa0..e26cae2a77 100644 --- a/components/tree-select/__docs__/demo/select/index.tsx +++ b/components/tree-select/__docs__/demo/select/index.tsx @@ -36,22 +36,11 @@ const dataSource = [ }, ]; class Demo extends React.Component { - constructor(props) { - super(props); + state = { + multiple: false, + }; - this.state = { - multiple: false, - }; - - this.handleCheck = this.handleCheck.bind(this); - this.handleChange = this.handleChange.bind(this); - } - - handleChange(value, data) { - console.log(value, data); - } - - handleCheck(v) { + handleCheck(v: boolean) { this.setState({ multiple: v, }); @@ -70,7 +59,6 @@ class Demo extends React.Component { treeDefaultExpandAll hasClear multiple={multiple} - onSelect={this.handleSelect} dataSource={dataSource} style={{ width: 200 }} /> diff --git a/components/tree-select/__docs__/demo/virtual-tree/index.tsx b/components/tree-select/__docs__/demo/virtual-tree/index.tsx index b88a09635e..b840a093a3 100644 --- a/components/tree-select/__docs__/demo/virtual-tree/index.tsx +++ b/components/tree-select/__docs__/demo/virtual-tree/index.tsx @@ -1,13 +1,15 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { TreeSelect } from '@alifd/next'; +import type { DataNode } from '@alifd/next/es/tree/types'; +import type { TreeSelectProps } from '@alifd/next/lib/tree-select'; function createDataSource(level = 3, count = 5) { const dataSource = []; let num = 0; - const drill = (children, _level, _count) => { - children.forEach((child, i) => { + const drill = (children: DataNode[], _level: number, _count: number) => { + children.forEach(child => { child.children = new Array(_count).fill(null).map((item, k) => { const key = `${child.key}-${k}`; num++; @@ -32,17 +34,9 @@ function createDataSource(level = 3, count = 5) { } class Demo extends React.Component { - constructor() { - super(); - - this.state = { - dataSource: [], - }; - } - - onChange(keys, info) { - console.log('onSelect', keys, info); - } + state = { + dataSource: [], + }; componentDidMount() { this.setState({ @@ -50,6 +44,10 @@ class Demo extends React.Component { }); } + onChange: TreeSelectProps['onChange'] = (keys, info) => { + console.log('onSelect', keys, info); + }; + render() { const dataSource = this.state.dataSource; diff --git a/components/tree-select/__docs__/index.en-us.md b/components/tree-select/__docs__/index.en-us.md index dd78ea5820..242d3ed53a 100644 --- a/components/tree-select/__docs__/index.en-us.md +++ b/components/tree-select/__docs__/index.en-us.md @@ -17,48 +17,52 @@ Like Select, TreeSelect can be used when the selected data structure is a tree s ### TreeSelect -| Param | Descripiton | Type | Default Value | -| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- | ---------------------------------- | ---- | -| children | tree nodes | ReactNode | - | -| size | size of selector

**options**:
'small', 'medium', 'large' | Enum | 'medium' | -| placeholder | placeholder of selector | String | - | -| disabled | whether selector is disabled | Boolean | false | -| hasArrow | whether has arrow icon | Boolean | true | -| hasBorder | whether selector has border | Boolean | true | -| label | custom inline label | ReactNode | - | -| readOnly | whether selector is read only, it can be expanded but cannot be selected under read only mode | Boolean | - | -| autoWidth | whether the dropdown is aligned with the selector | Boolean | true | -| dataSource | data source, this property has a higher priority than children | Array<Object> | - | -| value | (under control) current value | String/Array<String> | - | -| defaultValue | (under uncontrol) default value | String/Array<String> | null | -| onChange | callback triggered when value change

**signatures**:
Function(value: String/Array, data: Object/Array) => void
**params**:
_value_: {String/Array} selected value, a single value is returned when single select, and an array is returned when multiple select
_data_: {Object/Array} selected data, including value, label, pos, and key properties, returns a single value when single select, returns an array when multiple select, parent and child nodes are selected at the same time, only the parent node is returned. | Function | () => {} | -| showSearch | whether to show the search box | Boolean | false | -| onSearch | callback triggered when search

**signatures**:
Function(keyword: String) => void
**params**:
_keyword_: {String} input keyword | Function | () => {} | -| notFoundContent | content without data | ReactNode | 'Not Found' | -| multiple | whether it support multiple selection | Boolean | false | -| treeCheckable | whether the tree in the dropdown supports the checkbox of the node | Boolean | false | -| treeCheckStrictly | whether the checkbox of the node is controlled strictly (selection of parent and child nodes are no longer related) | Boolean | false | -| treeCheckedStrategy | defining the way to backfill when checked node

**options**:
'all' (return all checked nodes)
'parent' (only parent nodes are returned when parent and child nodes are checked)
'child' (only child nodes are returned when parent and child nodes are checked) | Enum | 'parent' | -| treeDefaultExpandAll | whether to expand all nodes by default | Boolean | false | -| treeDefaultExpandedKeys | keys of default expanded nodes | Array<String> | \[] | -| treeLoadData | asynchronous data loading function, Please refer to [Tree's asynchronous loading data Demo](https://fusion.design/pc/component/basic/tree#%E5%BC%82%E6%AD%A5%E5%8A%A0%E8%BD%BD%E6%95%B0%E6%8D%AE)

**signatures**:
Function(node: ReactElement) => void
**params**:
_node_: {ReactElement} clicked node | Function | - | -| treeProps | properties of Tree | Object | {} | -| defaultVisible | whether the dropdown box is displayed in default | Boolean | false | -| visible | whether the dropdown box is displayed currently | Boolean | - | -| onVisibleChange | callback triggered when open or close the dropdown

**signatures**:
Function(visible: Boolean, type: String) => void
**params**:
_visible_: {Boolean} whether is visible
_type_: {String} trigger type | Function | () => {} | -| popupStyle | style of dropdown | Object | - | -| popupClassName | class name of dropdown | String | - | -| popupContainer | container of dropdown | String/Function | - | -| popupProps | properties of Popup | Object | - | -| followTrigger | follow Trigger or not | Boolean | - | -| useVirtual | whether use virtual scroll | Boolean | false | -| tagInline | if display in one line | Boolean | false | 1.25 | -| maxTagPlaceholder | return custom content when hide extra tags, valid when tagInline is true

**signature**:
Function(selectedValues: Array, totalValues: Array) => reactNode
**params**:
_selectedValues_: {Array} current selected values
_totalValues_: {Array} all avaliable values
**returns**:
{reactNode} null
| Function | - | 1.25 | -| preserveNonExistentValue | if reserve value when value/defaultValue not exist in dataSource | Boolean | false | 1.25 | -| autoClearSearch | auto clear search value when choose item | Boolean | true | 1.26 | -| clickToCheck | whether clicking on the text can be checked. When it is true, selectable defaults to false. | Boolean | false | -| valueRender | Methods for rendering Select to display content
** Parameters**:
_item_: {Object} Render node's item
** Parameters**:
_itemPaths_: {Object[]} item full paths
**return value **:
{ReactNode} show content
| Function | item => item.label \|\| item.value | -| useDetailValue | The first parameter of onChange returns the object in dataSource | Boolean | - | +| Param | Description | Type | Default Value | Required | Supported Version | +| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | ------------------------------------- | -------- | ----------------- | +| children | Tree node | React.ReactNode | - | | - | +| size | Select size | 'small' \| 'medium' \| 'large' | 'medium' | | - | +| placeholder | Select placeholder | string | - | | - | +| disabled | Whether to be disabled | boolean | false | | - | +| hasArrow | Whether to show the arrow | boolean | true | | - | +| hasBorder | Whether to show the border | boolean | true | | - | +| hasClear | Whether to show the clear button | boolean | true | | - | +| label | Custom inline label | React.ReactNode | - | | - | +| readOnly | Whether to be read-only (read | boolean | - | | - | +| autoWidth | Whether the drop | boolean | true | | - | +| dataSource | Data source (higher priority than children) | DataSourceItem[] | - | | - | +| value | Current value (Controlled) | DataSourceItem[] \| DataSourceItem | - | | - | +| defaultValue | Default value (Uncontrolled) | SelectProps['defaultValue'] | null | | - | +| preserveNonExistentValue | Whether to display when value/defaultValue does not exist in dataSource | boolean | false | | 1.25 | +| onChange | Callback when the selected value changes | (
value: DataSourceItem[] \| DataSourceItem,
data: ObjectItem[] \| ObjectItem \| null
) => void | () =\> \{\} | | - | +| tagInline | Whether to display on one line (only effective when multiple and treeCheckable are true) | boolean | false | | 1.25 | +| maxTagPlaceholder | Content to display when hiding excess tags (effective when tagInline is true)

**signature**:
**params**:
_selectedValues_: Selected element
_totalValues_: Total pending element, treeCheckedStrategy = 'parent' is undefined
**return**:
ReactNode or HTMLElement | (
selectedValues: ObjectItem[],
totalValues?: ObjectItem[]
) => React.ReactNode \| HTMLElement | - | | 1.25 | +| autoClearSearch | Whether to automatically clear searchValue | boolean | true | | 1.26 | +| showSearch | Whether to show the search box | boolean | false | | - | +| onSearch | Callback when input in search box changes | (keyword: string) => void | () =\> \{\} | | - | +| notFoundContent | Content to display when there is no data | React.ReactNode | 'Not Found' | | - | +| multiple | Whether to support multiple selection | boolean | false | | - | +| treeCheckable | Whether the check box of the tree in the drop | boolean | false | | - | +| treeCheckStrictly | Whether the check box of the tree in the drop-down box is completely controlled (the parent | boolean | false | | - | +| treeCheckedStrategy | Definition of how to fill in when selected | 'all' \| 'parent' \| 'child' | 'parent' | | - | +| treeDefaultExpandAll | Whether the tree in the drop | boolean | false | | - | +| treeDefaultExpandedKeys | The array of keys of the nodes expanded by default in the tree in the drop | Array\ | [] | | - | +| treeLoadData | The function of asynchronous loading data in the tree in the drop | TreeProps['loadData'] | - | | - | +| treeProps | Pass | TreeProps | \{\} | | - | +| defaultVisible | Initial display state of the drop | boolean | false | | - | +| visible | Current display state of the drop | boolean | - | | - | +| onVisibleChange | Callback when the drop | (visible: boolean, type: string) => void | () =\> \{\} | | - | +| popupStyle | Custom style object for the drop | React.CSSProperties | - | | - | +| popupClassName | Custom class name for the drop | string | - | | - | +| popupContainer | Mounting container node for the drop | string \| HTMLElement \| ((target: HTMLElement) => HTMLElement) | - | | - | +| popupProps | Pass | PopupProps | - | | - | +| followTrigger | Whether to follow scrolling | boolean | - | | - | +| isPreview | Whether it is in preview mode | boolean | - | | - | +| renderPreview | Content rendered in preview mode | (data: ObjectItem[], props: TreeSelectProps) => React.ReactNode | - | | - | +| useVirtual | Whether to open virtual scrolling | boolean | false | | - | +| filterLocal | Whether to close local search | boolean | true | | - | +| immutable | Whether it is immutable data | boolean | - | | 1.23 | +| clickToCheck | Whether clicking on the text can be selected | boolean | false | | - | +| valueRender | Method for rendering Select area display content

**signature**:
**params**:
_item_: Extra item
_itemPaths_: Extra item path
**return**:
Display content | (item: TreeSelectState['\_k2n'][Key], itemPaths: ObjectItem[]) => React.ReactNode | (item) =\> item.label \|\| item.value | | - | diff --git a/components/tree-select/__docs__/index.md b/components/tree-select/__docs__/index.md index 62e6a30069..4485d81d0a 100644 --- a/components/tree-select/__docs__/index.md +++ b/components/tree-select/__docs__/index.md @@ -17,53 +17,52 @@ ### TreeSelect -| 参数 | 说明 | 类型 | 默认值 | 版本支持 | -| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | ------------------- | -------- | ----------- | --- | -| children | 树节点 | ReactNode | - | | -| size | 选择框大小

**可选值**:
'small', 'medium', 'large' | Enum | 'medium' | | -| placeholder | 选择框占位符 | String | - | | -| disabled | 是否禁用 | Boolean | false | | -| hasArrow | 是否有下拉箭头 | Boolean | true | | -| hasBorder | 是否有边框 | Boolean | true | | -| hasClear | 是否有清空按钮 | Boolean | false | | -| label | 自定义内联 label | ReactNode | - | | -| readOnly | 是否只读,只读模式下可以展开弹层但不能选择 | Boolean | - | | -| autoWidth | 下拉框是否与选择器对齐 | Boolean | true | | -| dataSource | 数据源,该属性优先级高于 children | Array<Object> | - | | -| preserveNonExistentValue | value/defaultValue 在 dataSource 中不存在时,是否展示 | Boolean | false | 1.25 | -| value | (受控)当前值 | String/Object/Array<any> | - | | -| defaultValue | (非受控)默认值 | String/Object/Array<any> | null | | -| onChange | 选中值改变时触发的回调函数

**签名**:
Function(value: String/Array, data: Object/Array) => void
**参数**:
_value_: {String/Array} 选中的值,单选时返回单个值,多选时返回数组
_data_: {Object/Array} 选中的数据,包括 value, label, pos, key属性,单选时返回单个值,多选时返回数组,父子节点选中关联时,同时选中,只返回父节点 | Function | () => {} | | -| tagInline | 是否一行显示,仅在 multiple 和 treeCheckable 为 true 时生效 | Boolean | false | 1.25 | -| maxTagPlaceholder | 隐藏多余 tag 时显示的内容,在 tagInline 生效时起作用

**签名**:
Function(selectedValues: Array, totalValues: Array) => reactNode
**参数**:
_selectedValues_: {Array} 当前已选中的元素
_totalValues_: {Array} 总待选元素
**返回值**:
{reactNode} null
| Function | - | 1.25 | -| autoClearSearch | 选择时是否自动清空 searchValue | Boolean | true | 1.26 | -| showSearch | 是否显示搜索框 | Boolean | false | | -| filterLocal | 是否使用本地过滤,在数据源为远程的时候需要关闭此项 | Boolean | true | | -| onSearch | 在搜索框中输入时触发的回调函数

**签名**:
Function(keyword: String) => void
**参数**:
_keyword_: {String} 输入的关键字 | Function | () => {} | | -| notFoundContent | 无数据时显示内容 | ReactNode | 'Not Found' | | -| multiple | 是否支持多选 | Boolean | false | | -| treeCheckable | 下拉框中的树是否支持勾选节点的复选框 | Boolean | false | | -| treeCheckStrictly | 下拉框中的树勾选节点复选框是否完全受控(父子节点选中状态不再关联) | Boolean | false | | -| treeCheckedStrategy | 定义选中时回填的方式

**可选值**:
'all'(返回所有选中的节点)
'parent'(父子节点都选中时只返回父节点)
'child'(父子节点都选中时只返回子节点) | Enum | 'parent' | | -| treeDefaultExpandAll | 下拉框中的树是否默认展开所有节点 | Boolean | false | | -| treeDefaultExpandedKeys | 下拉框中的树默认展开节点key的数组 | Array<String> | \[] | | -| treeLoadData | 下拉框中的树异步加载数据的函数,使用请参考[Tree的异步加载数据Demo](https://fusion.design/pc/component/tree?themeid=2#dynamic-container)

**签名**:
Function(node: ReactElement) => void
**参数**:
_node_: {ReactElement} 被点击展开的节点 | Function | - | | -| treeProps | 透传到 Tree 的属性对象 | Object | {} | | -| defaultVisible | 初始下拉框是否显示 | Boolean | false | | -| visible | 当前下拉框是否显示 | Boolean | - | | -| onVisibleChange | 下拉框显示或关闭时触发事件的回调函数

**签名**:
Function(visible: Boolean, type: String) => void
**参数**:
_visible_: {Boolean} 是否显示
_type_: {String} 触发显示关闭的操作类型 | Function | () => {} | | -| popupStyle | 下拉框自定义样式对象 | Object | - | | -| popupClassName | 下拉框样式自定义类名 | String | - | | -| popupContainer | 下拉框挂载的容器节点 | any | - | | -| popupProps | 透传到 Popup 的属性对象 | Object | - | | -| followTrigger | 是否跟随滚动 | Boolean | - | | -| isPreview | 是否为预览态 | Boolean | - | | -| renderPreview | 预览态模式下渲染的内容

**签名**:
Function(value: Array) => void
**参数**:
_value_: {Array} 选择值 { label: , value:} | Function | - | | -| useVirtual | 是否开启虚拟滚动 | Boolean | false | | -| immutable | 是否是不可变数据 | Boolean | - | 1.23 | -| clickToCheck | 点击文本是否可以勾选 | Boolean | false | | -| valueRender | 渲染 Select 展现内容的方法

**签名**:
Function(item: Object, itemPaths: Array) => ReactNode
**参数**:
_item_: {Object} 渲染节点的item
_itemPaths_: {Array} item的全路径数组
**返回值**:
{ReactNode} 展现内容
| Function | item => `item.label | | item.value` | | -| useDetailValue | onChange 第一个参数返回 dataSource 中的对象 | Boolean | - | | +| 参数 | 说明 | 类型 | 默认值 | 是否必填 | 支持版本 | +| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- | ------------------------------------- | -------- | -------- | +| children | 树节点 | React.ReactNode | - | | - | +| size | 选择框大小 | 'small' \| 'medium' \| 'large' | 'medium' | | - | +| placeholder | 选择框占位符 | string | - | | - | +| disabled | 是否禁用 | boolean | false | | - | +| hasArrow | 是否有下拉箭头 | boolean | true | | - | +| hasBorder | 是否有边框 | boolean | true | | - | +| hasClear | 是否有清空按钮 | boolean | true | | - | +| label | 自定义内联 label | React.ReactNode | - | | - | +| readOnly | 是否只读,只读模式下可以展开弹层但不能选择 | boolean | - | | - | +| autoWidth | 下拉框是否与选择器对齐 | boolean | true | | - | +| dataSource | 数据源,该属性优先级高于 children | DataSourceItem[] | - | | - | +| value | (受控)当前值 | DataSourceItem[] \| DataSourceItem | - | | - | +| defaultValue | (非受控)默认值 | SelectProps['defaultValue'] | null | | - | +| preserveNonExistentValue | value/defaultValue 在 dataSource 中不存在时,是否展示 | boolean | false | | 1.25 | +| onChange | 选中值改变时触发的回调函数 | (
value: DataSourceItem[] \| DataSourceItem,
data: ObjectItem[] \| ObjectItem \| null
) => void | () =\> \{\} | | - | +| tagInline | 是否一行显示,仅在 multiple 和 treeCheckable 为 true 时生效 | boolean | false | | 1.25 | +| maxTagPlaceholder | 隐藏多余 tag 时显示的内容,在 tagInline 生效时起作用

**签名**:
**参数**:
_selectedValues_: 当前已选中的元素
_totalValues_: 总待选元素,treeCheckedStrategy = 'parent' 时为 undefined
**返回值**:
ReactNode \| HTMLElement | (
selectedValues: ObjectItem[],
totalValues?: ObjectItem[]
) => React.ReactNode \| HTMLElement | - | | 1.25 | +| autoClearSearch | 是否自动清除 searchValue | boolean | true | | 1.26 | +| showSearch | 是否显示搜索框 | boolean | false | | - | +| onSearch | 在搜索框中输入时触发的回调函数 | (keyword: string) => void | () =\> \{\} | | - | +| notFoundContent | 无数据时显示内容 | React.ReactNode | 'Not Found' | | - | +| multiple | 是否支持多选 | boolean | false | | - | +| treeCheckable | 下拉框中的树是否支持勾选节点的复选框 | boolean | false | | - | +| treeCheckStrictly | 下拉框中的树勾选节点复选框是否完全受控(父子节点选中状态不再关联) | boolean | false | | - | +| treeCheckedStrategy | 定义选中时回填的方式 | 'all' \| 'parent' \| 'child' | 'parent' | | - | +| treeDefaultExpandAll | 下拉框中的树是否默认展开所有节点 | boolean | false | | - | +| treeDefaultExpandedKeys | 下拉框中的树默认展开节点key的数组 | Array\ | [] | | - | +| treeLoadData | 下拉框中的树异步加载数据的函数,使用请参考[Tree的异步加载数据Demo](https://fusion.design/pc/component/basic/tree#%E5%BC%82%E6%AD%A5%E5%8A%A0%E8%BD%BD%E6%95%B0%E6%8D%AE) | TreeProps['loadData'] | - | | - | +| treeProps | 透传到 Tree 的属性对象 | TreeProps | \{\} | | - | +| defaultVisible | 初始下拉框是否显示 | boolean | false | | - | +| visible | 当前下拉框是否显示 | boolean | - | | - | +| onVisibleChange | 下拉框显示或关闭时触发事件的回调函数 | (visible: boolean, type: string) => void | () =\> \{\} | | - | +| popupStyle | 下拉框自定义样式对象 | React.CSSProperties | - | | - | +| popupClassName | 下拉框样式自定义类名 | string | - | | - | +| popupContainer | 下拉框挂载的容器节点 | string \| HTMLElement \| ((target: HTMLElement) => HTMLElement) | - | | - | +| popupProps | 透传到 Popup 的属性对象 | PopupProps | - | | - | +| followTrigger | 是否跟随滚动 | boolean | - | | - | +| isPreview | 是否为预览态 | boolean | - | | - | +| renderPreview | 预览态模式下渲染的内容 | (data: ObjectItem[], props: TreeSelectProps) => React.ReactNode | - | | - | +| useVirtual | 是否开启虚拟滚动 | boolean | false | | - | +| filterLocal | 是否关闭本地搜索 | boolean | true | | - | +| immutable | 是否是不可变数据 | boolean | - | | 1.23 | +| clickToCheck | 点击文本是否可以勾选 | boolean | false | | - | +| valueRender | 渲染 Select 区域展现内容的方法

**签名**:
**参数**:
_item_: 渲染项
_itemPaths_: 渲染项在dataSource内的路径
**返回值**:
ReactNode - 展现内容 | (item: TreeSelectState['\_k2n'][Key], itemPaths: ObjectItem[]) => React.ReactNode | (item) =\> item.label \|\| item.value | | - | diff --git a/components/tree-select/__docs__/theme/index.jsx b/components/tree-select/__docs__/theme/index.tsx similarity index 53% rename from components/tree-select/__docs__/theme/index.jsx rename to components/tree-select/__docs__/theme/index.tsx index d983e28477..72df33d2ad 100644 --- a/components/tree-select/__docs__/theme/index.jsx +++ b/components/tree-select/__docs__/theme/index.tsx @@ -2,8 +2,14 @@ import React from 'react'; import ReactDOM from 'react-dom'; import '../../../demo-helper/style'; import '../../style'; -import { Demo, DemoHead, DemoGroup, initDemo } from '../../../demo-helper'; -import TreeSelect from '../../index'; +import { + Demo, + DemoHead, + DemoGroup, + initDemo, + type DemoFunctionDefineForObject, +} from '../../../demo-helper'; +import TreeSelect, { type TreeSelectProps } from '../../index'; import zhCN from '../../../locale/zh-cn'; import enUS from '../../../locale/en-us'; import ConfigProvider from '../../../config-provider'; @@ -15,68 +21,83 @@ const i18nMap = { trunk: '树干', branch: '数枝', leaf: '叶子', - label: '标签:' + label: '标签:', }, 'en-us': { trunk: 'Trunk', branch: 'Branch', leaf: 'Leaf', - label: 'Label' - } + label: 'Label', + }, }; -class FunctionDemo extends React.Component { - constructor(props) { +interface FunctionGroupButtonProps { + i18n: Record; + treeCheckable?: boolean; +} + +class FunctionDemo extends React.Component< + FunctionGroupButtonProps, + { + demoFunction: Record; + } +> { + constructor(props: FunctionGroupButtonProps) { super(props); this.state = { demoFunction: { hasBorder: { label: '边框', value: 'true', - enum: [{ - label: '显示', - value: 'true' - }, { - label: '隐藏', - value: 'false' - }] + enum: [ + { + label: '显示', + value: 'true', + }, + { + label: '隐藏', + value: 'false', + }, + ], }, inlineLabel: { label: '是否内置标签', value: 'false', - enum: [{ - label: '显示', - value: 'true' - }, { - label: '隐藏', - value: 'false' - }] - } - } + enum: [ + { + label: '显示', + value: 'true', + }, + { + label: '隐藏', + value: 'false', + }, + ], + }, + }, }; this.onFunctionChange = this.onFunctionChange.bind(this); } - onFunctionChange(demoFunction) { + onFunctionChange(demoFunction: Record) { this.setState({ - demoFunction + demoFunction, }); } render() { console.log(this.state.demoFunction); - // eslint-disable-next-line const { treeCheckable, i18n } = this.props; const { demoFunction } = this.state; const hasBorder = demoFunction.hasBorder.value === 'true'; const inlineLabel = demoFunction.inlineLabel.value === 'true'; - const treeSelectProps = { + const treeSelectProps: TreeSelectProps = { treeDefaultExpandAll: true, treeCheckable, hasBorder, style: { width: '200px' }, - popupContainer: target => target.parentNode, + popupContainer: (target: HTMLElement) => target.parentNode as HTMLElement, children: ( @@ -90,14 +111,18 @@ class FunctionDemo extends React.Component { - ) + ), }; if (inlineLabel) { treeSelectProps.label = i18n.label; } return ( - + @@ -106,9 +131,30 @@ class FunctionDemo extends React.Component { - - - + + + @@ -121,16 +167,17 @@ class FunctionDemo extends React.Component { } } -window.renderDemo = function(lang = 'en-us') { +window.renderDemo = function (lang = 'en-us') { const i18n = i18nMap[lang]; - ReactDOM.render(( + ReactDOM.render(
-
- ), document.getElementById('container')); + , + document.getElementById('container') + ); }; window.renderDemo(); diff --git a/components/tree-select/__tests__/index-spec.js b/components/tree-select/__tests__/index-spec.tsx similarity index 52% rename from components/tree-select/__tests__/index-spec.js rename to components/tree-select/__tests__/index-spec.tsx index 3d68238217..846caa1788 100644 --- a/components/tree-select/__tests__/index-spec.js +++ b/components/tree-select/__tests__/index-spec.tsx @@ -1,22 +1,14 @@ import React, { useState } from 'react'; -import ReactTestUtils from 'react-dom/test-utils'; -import Enzyme, { mount } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; -import assert from 'power-assert'; -import { dom, KEYCODE } from '../../util'; +import { debounce } from 'lodash'; +import { KEYCODE } from '../../util'; import TreeSelect from '../index'; import '../style'; - -/* eslint-disable react/jsx-filename-extension */ -/* global describe it afterEach */ -/* global describe it beforeEach */ - -Enzyme.configure({ adapter: new Adapter() }); +import type { DataSourceItem, ObjectItem } from '../../select'; +import type { TreeSelectDataItem } from '../types'; const TreeNode = TreeSelect.Node; -const { hasClass } = dom; -const dataSource = [ +const dataSource: TreeSelectDataItem[] = [ { label: '服装', className: 'k-1', @@ -56,28 +48,29 @@ const dataSource = [ }, ]; -function freeze(dataSource) { +function freeze(dataSource: TreeSelectDataItem[]) { return Object.freeze([ ...dataSource.map(item => { const { children } = item; - item.children = children && freeze(children); + item.children = + children && (freeze(children as TreeSelectDataItem[]) as TreeSelectDataItem[]); return Object.freeze({ ...item }); }), - ]); + ]) as TreeSelectDataItem[]; } -function cloneData(data, valueMap = {}) { - const loop = data => +function cloneData(data: TreeSelectDataItem[], valueMap: Record = {}) { + const loop = (data: TreeSelectDataItem[]) => data.map(item => { - let newItem; + let newItem: TreeSelectDataItem; - if (item.value in valueMap) { - newItem = { ...item, ...valueMap[item.value] }; + if ((item.value as string) in valueMap) { + newItem = { ...item, ...valueMap[item.value as string] }; } else { newItem = { ...item }; } if (newItem.children) { - newItem.children = loop(newItem.children); + newItem.children = loop(newItem.children as TreeSelectDataItem[]); } return newItem; @@ -86,13 +79,13 @@ function cloneData(data, valueMap = {}) { return loop(data); } -function flattenData(dataSource) { - const flattenList = []; - const drill = data => { +function flattenData(dataSource: ObjectItem[]) { + const flattenList: ObjectItem[] = []; + const drill = (data: ObjectItem[]) => { data.forEach(item => { const { children, ...newItem } = item; flattenList.push(newItem); - children && children.length && drill(children); + children && children.length && drill(children as ObjectItem[]); }); }; @@ -101,162 +94,180 @@ function flattenData(dataSource) { return flattenList; } -function assertDataAndNodes(dataSource) { - const labels = Array.prototype.map.call( - document.querySelectorAll('li.next-tree-node .next-tree-node-label'), - item => item.textContent - ); - - assert(flattenData(dataSource).every((item, index) => item.label === labels[index])); +function shouldDataAndNodes(dataSource: ObjectItem[]) { + cy.get('li.next-tree-node .next-tree-node-label').then($el => { + flattenData(dataSource).every((item, index) => + expect(item.label).to.equal($el[index].textContent?.trim()) + ); + }); } -function findTreeNodeByValue(value, container = document) { - return container.querySelector(`.k-${value}`); +function findTreeNodeByValue(value: string) { + return cy.get(`.k-${value}`); } -function createMap(data) { - const map = {}; +function createMap(data: ObjectItem[]) { + const map: Record = {}; - const loop = (data, prefix = '0') => { + const loop = (data: ObjectItem[], prefix = '0') => { data.forEach((item, index) => { const { value, label, children, ...rests } = item; const pos = `${prefix}-${index}`; - map[value] = { ...rests, value, label, pos, key: pos }; + map[value as string] = { ...rests, value, label, pos, key: pos }; if (children && children.length) { - loop(children, pos); + loop(children as ObjectItem[], pos); } - }) - } + }); + }; loop(data); return map; } -function selectTreeNode(value, container) { - ReactTestUtils.Simulate.click(findTreeNodeByValue(value, container).querySelector('.next-tree-node-label')); +function selectTreeNode(value: string) { + findTreeNodeByValue(value).find('.next-tree-node-label').first().click(); } -function checkTreeNode(value) { - const input = findTreeNodeByValue(value).querySelector('.next-checkbox input'); - ReactTestUtils.Simulate.click(input); +function checkTreeNode(value: string) { + findTreeNodeByValue(value).find('.next-checkbox input').click(); } -function assertSelected(value, selected, container) { - assert(hasClass(findTreeNodeByValue(value, container).querySelector('.next-tree-node-inner'), 'next-selected') === selected); +function shouldSelected(value: string, selected: boolean) { + findTreeNodeByValue(value) + .find('.next-tree-node-inner') + .should(selected ? 'have.class' : 'not.have.class', 'next-selected'); } -function assertChecked(value, checked) { - assert(hasClass(findTreeNodeByValue(value).querySelector('.next-checkbox-wrapper'), 'checked') === checked); +function shouldChecked(value: string, checked: boolean) { + findTreeNodeByValue(value) + .find('.next-checkbox-wrapper') + .should(checked ? 'have.class' : 'not.have.class', 'checked'); } -function getLabels(wrapper) { - return wrapper.find('span.next-tag-body').map(node => node.text().trim()); +function getLabels() { + return cy.get('span.next-tag-body').then($el => { + return $el.map((index, el) => { + return Cypress.$(el).text().trim(); + }); + }); } +function shouldHideElement() { + cy.document().then(document => { + const overlay = document.querySelector('.next-overlay-wrapper'); + if (overlay) { + cy.wrap(overlay).should($el => { + expect($el).to.have.css('display', 'none'); + }); + } else { + expect(overlay).to.be.null; + } + }); +} -const _v2n = createMap(dataSource); - -describe('TreeSelect', () => { - let wrapper; - - beforeEach(() => { - const nodeListArr = [].slice.call(document.querySelectorAll('.next-overlay-wrapper')); - - nodeListArr.forEach(node => { - node.parentNode.removeChild(node); +function shouldShowElement() { + cy.document().then(document => { + const overlay = document.querySelector('.next-overlay-wrapper'); + expect(overlay).to.not.be.null; + cy.wrap(overlay).should($el => { + expect($el).to.not.have.css('display', 'none'); }); }); +} - afterEach(() => { - if (wrapper) { - wrapper.unmount(); - wrapper = null; - } - }); +const _v2n = createMap(dataSource); - it('should show dropdown when click select box', done => { - wrapper = mount(); - wrapper.find('.next-select').simulate('click'); - setTimeout(() => { - assert(document.querySelector('.next-tree-select-dropdown')); - done(); - }, 1000); +describe('TreeSelect', () => { + it('should show dropdown when click select box', () => { + cy.mount(); + cy.get('.next-select').trigger('click'); + cy.get('.next-tree-select-dropdown').should('exist'); }); it('should show dropdown when set defaultVisible to true', () => { - wrapper = mount(); - assert(document.querySelector('.next-tree-select-dropdown')); + cy.mount(); + cy.get('.next-tree-select-dropdown').should('exist'); }); it('should render by loop TreeNode', () => { - const loop = data => + const loop = (data: ObjectItem[]) => data.map(item => { return ( - - {item.children ? loop(item.children) : null} + + {item.children ? loop(item.children as ObjectItem[]) : null} ); }); - wrapper = mount( + cy.mount( {loop(dataSource)} - ); - assertDataAndNodes(dataSource); + ).as('Demo'); + shouldDataAndNodes(dataSource); const newDataSource = [...dataSource]; newDataSource.push({ label: '鞋', value: '7', }); - wrapper.setProps({ + + cy.rerender('Demo', { children: loop(newDataSource), }); - assertDataAndNodes(newDataSource); + shouldDataAndNodes(newDataSource); }); it('should render by dataSource', () => { - wrapper = mount(); - assertDataAndNodes(dataSource); + cy.mount().as( + 'Demo' + ); + shouldDataAndNodes(dataSource); const newDataSource = [...dataSource]; newDataSource.push({ label: '鞋', value: '7', }); - wrapper.setProps({ - dataSource: newDataSource, - }); - assertDataAndNodes(newDataSource); + cy.rerender('Demo', { dataSource: newDataSource }); + shouldDataAndNodes(newDataSource); }); it('should render by defaultValue', () => { - wrapper = mount(); - assertSelected('4', true); + cy.mount( + + ).as('Demo'); + shouldSelected('4', true); - wrapper.setProps({ defaultValue: '6' }); - wrapper.update(); - assertSelected('6', false); + cy.rerender('Demo', { + defaultValue: '6', + }); + shouldSelected('6', false); }); it('should render by detail defaultValue', () => { - wrapper = mount( + cy.mount( - ); - assertSelected('4', true); + ).as('Demo'); + shouldSelected('4', true); - wrapper.setProps({ defaultValue: { label: '裙子', value: '6' } }); - wrapper.update(); - assertSelected('6', false); + cy.rerender('Demo', { + defaultValue: { label: '裙子', value: '6' }, + }); + shouldSelected('6', false); }); it('should render by value', () => { - wrapper = mount( + cy.mount( { treeDefaultExpandAll dataSource={dataSource} /> - ); - assertSelected('4', false); - assertSelected('6', true); + ).as('Demo'); + shouldSelected('4', false); + shouldSelected('6', true); const newValue = ['4', '6']; - wrapper.setProps({ value: newValue }); - wrapper.update(); - assertSelected('4', true); - assertSelected('6', true); + cy.rerender('Demo', { + value: newValue, + }); + shouldSelected('4', true); + shouldSelected('6', true); }); it('should render by detail value', () => { - wrapper = mount( + cy.mount( { treeDefaultExpandAll dataSource={dataSource} /> - ); - assertSelected('4', false); - assertSelected('6', true); - + ).as('Demo'); + shouldSelected('4', false); + shouldSelected('6', true); const newValue = [ { label: '外套', value: '4' }, { label: '裙子', value: '6' }, ]; - wrapper.setProps({ value: newValue }); - wrapper.update(); - assertSelected('4', true); - assertSelected('6', true); + cy.rerender('Demo', { + value: newValue, + }); + shouldSelected('4', true); + shouldSelected('6', true); }); it('should render by defaultValue when enable treeCheckable', () => { - wrapper = mount( - - ); - assertChecked('4', true); - - wrapper.setProps({ defaultValue: '6' }); - wrapper.update(); - assertChecked('6', false); + cy.mount( + + ).as('Demo'); + shouldChecked('4', true); + cy.rerender('Demo', { + defaultValue: '6', + }); + shouldChecked('6', false); }); it('should render by value when enable treeCheckable', () => { - wrapper = mount( + cy.mount( { treeDefaultExpandAll dataSource={dataSource} /> - ); - assertChecked('4', false); - assertChecked('6', true); - + ).as('Demo'); + shouldChecked('4', false); + shouldChecked('6', true); const newValue = ['4', '6']; - wrapper.setProps({ value: newValue }); - wrapper.update(); - assertChecked('4', true); - assertChecked('6', true); + cy.rerender('Demo', { + value: newValue, + }); + shouldChecked('4', true); + shouldChecked('6', true); }); - it('should trigger onChange and close dropdown when select tree node', done => { - let triggered = false; + it('should trigger onChange and close dropdown when select tree node', () => { + const onClick = cy.spy(); + const expectValue = '4'; const expectItem = _v2n[expectValue]; - const handleChange = (value, data) => { - triggered = true; - assert(value === expectValue); - assert.deepEqual(data, expectItem); + const handleChange = ( + value: DataSourceItem | DataSourceItem[], + data: ObjectItem | ObjectItem[] | null + ) => { + onClick(); + expect(value).to.equal(expectValue); + expect(data).to.deep.equal(expectItem); }; - wrapper = mount( - + cy.mount( + ); selectTreeNode(expectValue); - wrapper.update(); - assert(triggered); - - setTimeout(() => { - assert( - !document.querySelector('.next-overlay-wrapper') || - document.querySelector('.next-overlay-wrapper').style.display === 'none' - ); - done(); - }, 1000); + cy.wrap(onClick).should('be.calledOnce'); + shouldHideElement(); }); - it('should not trigger onChange but close dropdown when select selected node', done => { - let triggered = false; + it('should not trigger onChange but close dropdown when select selected node', () => { + const onClick = cy.spy(); const value = '4'; const handleChange = () => { - triggered = true; + onClick(); }; - wrapper = mount( + cy.mount( { /> ); selectTreeNode(value); - wrapper.update(); - assert(!triggered); - - setTimeout(() => { - assert( - !document.querySelector('.next-overlay-wrapper') || - document.querySelector('.next-overlay-wrapper').style.display === 'none' - ); - done(); - }, 1000); + cy.wrap(onClick).should('not.be.called'); + shouldHideElement(); }); - it('should trigger onChange but not close dropdown when select node and enable multiple', done => { - let triggered = false; + it('should trigger onChange but not close dropdown when select node and enable multiple', () => { + const onClick = cy.spy(); const initValue = '4'; const appendValue = '6'; const expectValue = [initValue, appendValue]; - const handleChange = (value, data) => { - triggered = true; - assert.deepEqual(value, expectValue); - assert.deepEqual(data, expectValue.map(v => _v2n[v])); + const handleChange = ( + value: DataSourceItem | DataSourceItem[], + data: ObjectItem | ObjectItem[] | null + ) => { + onClick(); + expect(value).to.deep.equal(expectValue); + expect(data).to.deep.equal(expectValue.map(v => _v2n[v])); }; - wrapper = mount( + cy.mount( { /> ); selectTreeNode(appendValue); - wrapper.update(); - assert(triggered); - - setTimeout(() => { - assert( - document.querySelector('.next-overlay-wrapper') && - document.querySelector('.next-overlay-wrapper').style.display !== 'none' - ); - done(); - }, 1000); + cy.wrap(onClick).should('be.calledOnce'); + shouldShowElement(); }); it('should trigger onChange when check node', () => { - let triggered = false; + const onClick = cy.spy(); const initValue = '4'; const appendValue = '6'; const expectValue = ['4', '3']; - const handleChange = (value, data) => { - triggered = true; - assert.deepEqual(value, expectValue); - assert.deepEqual(data, expectValue.map(v => _v2n[v])); + const handleChange = ( + value: DataSourceItem | DataSourceItem[], + data: ObjectItem | ObjectItem[] | null + ) => { + onClick(); + expect(value).to.deep.equal(expectValue); + expect(data).to.deep.equal(expectValue.map(v => _v2n[v])); }; - wrapper = mount( + cy.mount( { value={initValue} onChange={handleChange} /> - ); - checkTreeNode(appendValue); - wrapper.update(); - assert(triggered); + ).then(() => { + checkTreeNode(appendValue); + cy.wrap(onClick).should('be.calledOnce'); + }); }); it('should trigger onChange when check node and enable treeCheckStrictly', () => { - let triggered = false; + const onClick = cy.spy(); const appendValue = '6'; const expectValue = [appendValue]; - const handleChange = (value, data) => { - triggered = true; - assert.deepEqual(value, expectValue); - assert.deepEqual(data, expectValue.map(v => _v2n[v])); + const handleChange = ( + value: DataSourceItem | DataSourceItem[], + data: ObjectItem | ObjectItem[] | null + ) => { + onClick(); + expect(value).to.deep.equal(expectValue); + expect(data).to.deep.equal(expectValue.map(v => _v2n[v])); }; - wrapper = mount( + cy.mount( { dataSource={dataSource} onChange={handleChange} /> - ); - checkTreeNode(appendValue); - wrapper.update(); - assert(triggered); + ).then(() => { + checkTreeNode(appendValue); + cy.wrap(onClick).should('be.calledOnce'); + }); }); it('should render tag when defaultValue [{ label, value}]', () => { - wrapper = mount( + cy.mount( { /> ); - assert.deepEqual(getLabels(wrapper), ['test1']); + getLabels().then($labels => { + expect($labels.get()).to.deep.equal(['test1']); + }); }); it('should set parent node checked if all child nodes is checked even treeCheckedStrategy is "child"', () => { - wrapper = mount( + cy.mount( { /> ); - assertChecked('3', true); + shouldChecked('3', true); }); it('should render parent tag when set treeCheckedStrategy to all', () => { - wrapper = mount( + cy.mount( { treeCheckedStrategy="parent" /> ); - assert.deepEqual(getLabels(wrapper), ['女装']); + getLabels().then($labels => { + expect($labels.get()).to.deep.equal(['女装']); + }); }); it('should render child tag when set treeCheckedStrategy to all', () => { - wrapper = mount( - + cy.mount( + ); - assert.deepEqual(getLabels(wrapper), ['裙子']); + getLabels().then($labels => { + expect($labels.get()).to.deep.equal(['裙子']); + }); }); it('should render all tag when set treeCheckedStrategy to all', () => { - wrapper = mount( + cy.mount( { treeCheckedStrategy="all" /> ); - assert.deepEqual(getLabels(wrapper), ['女装', '裙子']); - - wrapper - .find('div.next-tag') - .at(0) - .find('.next-icon-close') - .simulate('click'); - wrapper.update(); - assert.deepEqual(getLabels(wrapper), []); + + getLabels().then($labels => { + expect($labels.get()).to.deep.equal(['女装', '裙子']); + }); + + cy.get('div.next-tag').eq(0).find('.next-icon-close').click(); + cy.get('span.next-tag-body').should('not.exist'); }); it('should support preview mode render', () => { @@ -576,29 +597,38 @@ describe('TreeSelect', () => { }, ]; - wrapper = mount(); - assert(wrapper.find('.next-form-preview').length > 0); - assert(wrapper.find('.next-form-preview').text() === '西安市'); - wrapper.setProps({ - renderPreview: items => { - assert(items.length === 1); - assert(items[0].label === '西安市'); + cy.mount().as('Demo'); + cy.get('.next-form-preview') + .should('exist') + .and($el => { + expect($el.text()).to.equal('西安市'); + }); + + cy.rerender('Demo', { + renderPreview: (items: ObjectItem[]) => { + expect(items.length).to.equal(1); + expect(items[0].label).to.equal('西安市'); return 'Hello World'; }, }); - assert(wrapper.find('.next-form-preview').text() === 'Hello World'); + cy.get('.next-form-preview').then($el => { + expect($el.text()).to.equal('Hello World'); + }); }); it('should trigger onChange when remove tag', () => { - let triggered = false; + const onClick = cy.spy(); const value = ['6']; - const handleChange = (value, data) => { - triggered = true; - assert.deepEqual(value, []); - assert.deepEqual(data, []); + const handleChange = ( + value: ObjectItem | ObjectItem[], + data: ObjectItem | ObjectItem[] + ) => { + onClick(); + expect(value).to.deep.equal([]); + expect(data).to.deep.equal([]); }; - wrapper = mount( + cy.mount( { onChange={handleChange} /> ); - wrapper.find('.next-icon-close').simulate('click'); - wrapper.update(); - assert(triggered); + cy.get('.next-icon-close').click(); + cy.wrap(onClick).should('be.called'); }); - it('should support multiple with hasClear', done => { - wrapper = mount( + it('should support multiple with hasClear', () => { + cy.mount( { - assert(value === null); - done(); + expect(value).to.equal(null); }} /> ); - - wrapper.find('i.next-icon-delete-filling').simulate('click'); + cy.get('i.next-icon-delete-filling').click({ force: true }); }); it('should trigger onSearch when search some keyword', () => { - let triggered = false; + const onClick = cy.spy(); const searchedValue = '外套'; - const handleSearch = value => { - triggered = true; - assert(value === searchedValue); + const handleSearch = (value: string) => { + onClick(); + expect(value).to.equal(searchedValue); }; - wrapper = mount( + cy.mount( ); - wrapper.find('.next-select-trigger-search input').simulate('change', { target: { value: '外套' } }); - wrapper.update(); - assert(triggered); + cy.get('.next-select-trigger-search input').type('外套'); + cy.wrap(onClick).should('be.calledOnce'); }); it('should hightlight matched node when search some keyword', () => { - wrapper = mount( + cy.mount( { ); - wrapper.find('.next-select-trigger-search input').simulate('change', { target: { value: 'element' } }); - wrapper.update(); - const node = document.querySelector('.react-element'); - assert(hasClass(node, 'next-filtered')); + cy.get('.next-select-trigger-search input').type('element'); + cy.get('.react-element').should('have.class', 'next-filtered'); }); it('should ignore case when search', () => { @@ -692,43 +716,51 @@ describe('TreeSelect', () => { ], }, ]; - wrapper = mount(); + cy.mount( + + ); ['INPUT', 'input'].forEach(kw => { - wrapper.find('.next-select-trigger-search input').simulate('change', { target: { value: kw } }); - wrapper.update(); + cy.get('.next-select-trigger-search input').clear(); // Need to clear first + cy.get('.next-select-trigger-search input').type(kw); - const node = document.querySelector('.next-filtered'); - assert(node && node.querySelector('.next-tree-node-label') !== 'Input'); + cy.get('.next-filtered').find('.next-tree-node-label').should('have.text', 'Input'); }); }); // https://github.com/alibaba-fusion/next/issues/2029 it('fix bug after setState onSearch', () => { function Demo() { - const [data, setData] = useState([{ label: 'element', key: '0', className: 'react-element' }]); + const [data, setData] = useState([ + { label: 'element', key: '0', className: 'react-element' }, + ]); function handleChange() { setData([{ label: 'react-element-new', key: '1', className: 'react-element-new' }]); } return ( - + ); } - wrapper = mount(); - - wrapper.find('.next-select-trigger-search input').simulate('change', { target: { value: 'element' } }); - wrapper.update(); + cy.mount(); - assert(!hasClass(document.querySelector('.react-element'), 'next-filtered')); - assert(hasClass(document.querySelector('.react-element-new'), 'next-filtered')); + cy.get('.next-select-trigger-search input').type('element'); + cy.get('.react-element').should('not.exist'); + cy.get('.react-element-new').should('have.class', 'next-filtered'); }); it('should only show matched node and its parent node when search some keyword', () => { - wrapper = mount(); - wrapper.find('.next-select-trigger-search input').simulate('change', { target: { value: '外套' } }); - wrapper.update(); + cy.mount( + + ); + cy.get('.next-select-trigger-search input').type('外套'); const expectTreeData = [ { @@ -748,24 +780,23 @@ describe('TreeSelect', () => { ], }, ]; - assertDataAndNodes(expectTreeData); + shouldDataAndNodes(expectTreeData); - ['3', '5'].forEach(v => assert(findTreeNodeByValue(v).style.display === 'none')); + ['3', '5'].forEach(v => findTreeNodeByValue(v).should('have.css', { display: 'none' })); }); it('should support search well when use virtual', () => { const data = cloneData(dataSource); - data[0].children = data[0].children.concat( - new Array(100).fill().map((__, index) => { - index = String(index); + data[0].children = (data[0].children as TreeSelectDataItem[]).concat( + new Array(100).fill(null).map((__, index) => { return { - value: index, - label: index, + value: String(index), + label: String(index), }; }) ); - wrapper = mount( + cy.mount( { }} /> ); - wrapper.find('.next-select-trigger-search input').simulate('change', { target: { value: 77 } }); - wrapper.update(); + cy.get('.next-select-trigger-search input').type('77'); const expectTreeData = [ { @@ -793,23 +823,22 @@ describe('TreeSelect', () => { ], }, ]; - assertDataAndNodes(expectTreeData); + shouldDataAndNodes(expectTreeData); }); // https://github.com/alibaba-fusion/next/issues/2271 it('fix search bug when useVirtual', () => { const data = cloneData(dataSource); - data[0].children = data[0].children.concat( - new Array(100).fill().map((__, index) => { - index = String(index); + data[0].children = (data[0].children as TreeSelectDataItem[]).concat( + new Array(100).fill(null).map((__, index) => { return { - value: index, - label: index, + value: String(index), + label: String(index), }; }) ); - wrapper = mount( + cy.mount( { }} /> ); - wrapper.find('.next-select-trigger-search input').simulate('change', { target: { value: 77 } }); + cy.get('.next-select-trigger-search input').type('77'); - wrapper.find('.next-tree-node[value="77"] input').simulate('click'); - wrapper.update(); + cy.get('.next-tree-node[value="77"] input').click(); - assert(wrapper.find('.next-tree-node[value="1"] .indeterminate').length); + cy.get('.next-tree-node[value="1"] .indeterminate').should('exist'); }); it('should render not found if dataSource is empty or there is no search result', () => { - wrapper = mount(); - assert(document.querySelector('.next-tree-select-not-found').textContent.trim() === 'Not Found'); + cy.mount().as('Demo'); + cy.get('.next-tree-select-not-found').should('have.text', 'Not Found'); + cy.rerender('Demo', { dataSource }); + cy.get('.next-tree').should('exist'); - wrapper.setProps({ dataSource }); - wrapper.update(); - assert(document.querySelector('.next-tree')); - - wrapper.find('.next-select-trigger-search input').simulate('change', { target: { value: '哈哈' } }); - wrapper.update(); - assert(document.querySelector('.next-tree-select-not-found').textContent.trim() === 'Not Found'); + cy.get('.next-select-trigger-search input').type('哈哈'); + cy.get('.next-tree-select-not-found').should('have.text', 'Not Found'); }); it('should turn off local search when filterLocal is false', () => { - wrapper = mount( + cy.mount( { /> ); - wrapper.find('.next-select-trigger-search input').simulate('change', { target: { value: '外套' } }); - wrapper.update(); + cy.get('.next-select-trigger-search input').type('外套'); - assertDataAndNodes(dataSource); + shouldDataAndNodes(dataSource); }); it('should not clear search value when autoClearSearch is false', () => { - wrapper = mount( - + cy.mount( + ); - wrapper.find('.next-select-trigger-search input').simulate('change', { target: { value: '外套' } }); - wrapper.find('.next-tree-node[value="4"]').simulate('click'); - wrapper.update(); + cy.get('.next-select-trigger-search input').type('外套'); + cy.get('.next-tree-node[value="4"]').click(); - assert(wrapper.find('.next-select-trigger-search input').prop('value') === '外套'); + cy.get('.next-select-trigger-search input').should('have.value', '外套'); }); it('fix issues use isPreview when value is empty', () => { - wrapper = mount(); - assert(wrapper.find('.next-form-preview').instance().textContent === ''); + cy.mount(); + cy.get('.next-form-preview').should('have.text', ''); }); it('should support immutable ', () => { - wrapper = mount(); - assertDataAndNodes(dataSource); + cy.mount( + + ); + shouldDataAndNodes(dataSource); }); - it('should support keyboard', done => { - wrapper = mount( + it('should support keyboard', () => { + cy.mount( { })} /> ); - wrapper.find('.next-select').simulate('click'); - - setTimeout(() => { - assert(document.querySelector('.next-tree')); - wrapper.find('.next-select-trigger-search input').simulate('keydown', { keyCode: KEYCODE.DOWN }); - assert( - document.activeElement === - document.querySelectorAll( - '.next-tree > .next-tree-node > .next-tree-node-inner > .next-tree-node-label-wrapper' - )[0] - ); - done(); - }, 2000); + cy.get('.next-select').click(); + + cy.get('.next-tree').should('exist'); + cy.get('.next-select-trigger-search input').trigger('keydown', { keyCode: KEYCODE.DOWN }); + cy.get( + '.next-tree > .next-tree-node > .next-tree-node-inner > .next-tree-node-label-wrapper' + ).then($el => { + cy.document().its('activeElement').should('eq', $el[0]); + }); }); it('should support single line display', () => { - wrapper = mount( + cy.mount( { /> ); - assert(wrapper.find('.next-select-tag-compact').length > 0); - assert( - wrapper - .find('.next-select-tag-compact') - .text() - .includes('3/6') - ); + cy.get('.next-select-tag-compact').should('exist'); + cy.get('.next-select-tag-compact').should('include.text', '3/6'); }); it('should support valueRender', () => { - wrapper = mount( + cy.mount( { }} /> ); - assert(wrapper.find('.next-select-values').length > 0); - assert( - wrapper - .find('.next-select-values') - .text() - .trim() === '服装/男装' - ); + cy.get('.next-select-values').should('exist'); + cy.contains('.next-select-values', '服装/男装'); }); describe('should support useDetailValue', () => { it('Support dataSource mode', () => { - const div = document.createElement('div'); - document.body.appendChild(div); - const handleChange = value => { - assert(typeof value === 'object'); - assert(value.value === '1'); + const handleChange = (value: ObjectItem) => { + expect(typeof value).to.equal('object'); + expect(value.value).to.equal('1'); }; - const wrapper = mount( + cy.mount( { treeDefaultExpandAll dataSource={dataSource} onChange={handleChange} - />, - { attachTo: div } + /> ); - selectTreeNode('1', div); - wrapper.unmount(); + selectTreeNode('1'); }); it('Support children mode', () => { - const div = document.createElement('div'); - document.body.appendChild(div); - const handleChange = value => { - assert(typeof value === 'object'); - assert(value.value === '1'); + const handleChange = (value: ObjectItem) => { + expect(typeof value).to.equal('object'); + expect(value.value).to.equal('1'); }; - const wrapper = mount( + cy.mount( { - , - { attachTo: div } + ); - selectTreeNode('1', div); - wrapper.unmount(); + selectTreeNode('1'); }); it('Control mode available', () => { - const div = document.createElement('div'); - document.body.appendChild(div); - function App() { - const [value, setValue] = useState({ label: 'Component', value: '1' }); + const [value, setValue] = useState({ + label: 'Component', + value: '1', + } as ObjectItem); return ( { visible treeDefaultExpandAll value={value} - onChange={v => { - assert(v && typeof v === 'object' && v.value); + onChange={(v: ObjectItem) => { + expect(v).to.exist; + expect(typeof v).to.equal('object'); + expect(v.value).to.exist; setValue(v); }} > @@ -1037,13 +1047,11 @@ describe('TreeSelect', () => { ); } - const wrapper = mount(, { attachTo: div }); - assert(wrapper.find('.next-select-values').text().trim() === 'Component'); - selectTreeNode('2', div); - assert(wrapper.find('.next-select-values').text().trim() === 'Form'); - assertSelected('2', true, div); - wrapper.unmount(); + cy.mount(); + cy.get('.next-select-values').should('include.text', 'Component'); + selectTreeNode('2'); + cy.get('.next-select-values').should('include.text', 'Form'); + shouldSelected('2', true); }); }); - }); diff --git a/components/tree-select/index.d.ts b/components/tree-select/index.d.ts deleted file mode 100644 index ebff6e311d..0000000000 --- a/components/tree-select/index.d.ts +++ /dev/null @@ -1,252 +0,0 @@ -/// - -import React from 'react'; -import { CommonProps } from '../util'; -import { PopupProps } from '../overlay'; -import { TreeProps, Node } from '../tree'; -import { item } from '../select'; - -interface HTMLAttributesWeak extends React.HTMLAttributes { - defaultValue?: any; - onChange?: any; -} - -export interface TreeSelectProps extends HTMLAttributesWeak, CommonProps { - /** - * 树节点 - */ - children?: React.ReactNode; - - /** - * 选择框大小 - */ - size?: 'small' | 'medium' | 'large'; - - /** - * 选择框占位符 - */ - placeholder?: string; - - /** - * 是否禁用 - */ - disabled?: boolean; - - /** - * 是否有下拉箭头 - */ - hasArrow?: boolean; - - /** - * 是否有边框 - */ - hasBorder?: boolean; - - /** - * 是否有清空按钮 - */ - hasClear?: boolean; - - /** - * 自定义内联 label - */ - label?: React.ReactNode; - - /** - * 是否只读,只读模式下可以展开弹层但不能选择 - */ - readOnly?: boolean; - - /** - * 下拉框是否与选择器对齐 - */ - autoWidth?: boolean; - - /** - * 数据源,该属性优先级高于 children - */ - dataSource?: Array; - - /** - * (受控)当前值 - */ - value?: string | object | Array; - - /** - * (非受控)默认值 - */ - defaultValue?: string | object | Array; - - /** - * value/defaultValue 在 dataSource 中不存在时,是否展示 - */ - preserveNonExistentValue?: boolean; - - /** - * 选中值改变时触发的回调函数 - */ - onChange?: (value: any | Array, data: any | Array) => void; - - /** - * onChange 返回的 value 使用 dataSource 的对象 - */ - useDetailValue?: boolean; - - /** - * 是否一行显示,仅在 multiple 和 treeCheckable 为 true 时生效 - */ - tagInline?: boolean; - - /** - * 隐藏多余 tag 时显示的内容,在 tagInline 生效时起作用 - * @param {Object[]} selectedValues 当前已选中的元素 - * @param {Object[]} [totalValues] 总待选元素,treeCheckedStrategy = 'parent' 时为 undefined - */ - maxTagPlaceholder?: ( - selectedValues: any[], - totalValues?: any[] - ) => React.ReactNode | HTMLElement; - - /** - * 是否自动清除 searchValue - */ - autoClearSearch?: boolean; - - /** - * 是否显示搜索框 - */ - showSearch?: boolean; - - /** - * 在搜索框中输入时触发的回调函数 - */ - onSearch?: (keyword: string) => void; - onSearchClear?: (actionType: string) => void; - /** - * 无数据时显示内容 - */ - notFoundContent?: React.ReactNode; - - /** - * 是否支持多选 - */ - multiple?: boolean; - - /** - * 下拉框中的树是否支持勾选节点的复选框 - */ - treeCheckable?: boolean; - - /** - * 下拉框中的树勾选节点复选框是否完全受控(父子节点选中状态不再关联) - */ - treeCheckStrictly?: boolean; - - /** - * 定义选中时回填的方式 - */ - treeCheckedStrategy?: 'all' | 'parent' | 'child'; - - /** - * 下拉框中的树是否默认展开所有节点 - */ - treeDefaultExpandAll?: boolean; - - /** - * 下拉框中的树默认展开节点key的数组 - */ - treeDefaultExpandedKeys?: Array; - - /** - * 下拉框中的树异步加载数据的函数,使用请参考[Tree的异步加载数据Demo](https://fusion.design/pc/component/basic/tree#%E5%BC%82%E6%AD%A5%E5%8A%A0%E8%BD%BD%E6%95%B0%E6%8D%AE) - */ - treeLoadData?: (node: React.ReactElement) => void; - - /** - * 透传到 Tree 的属性对象 - */ - treeProps?: TreeProps; - - /** - * 初始下拉框是否显示 - */ - defaultVisible?: boolean; - - /** - * 当前下拉框是否显示 - */ - visible?: boolean; - - /** - * 下拉框显示或关闭时触发事件的回调函数 - */ - onVisibleChange?: (visible: boolean, type: string) => void; - - /** - * 下拉框自定义样式对象 - */ - popupStyle?: React.CSSProperties; - - /** - * 下拉框样式自定义类名 - */ - popupClassName?: string; - - /** - * 下拉框挂载的容器节点 - */ - popupContainer?: string | HTMLElement | ((target: HTMLElement) => HTMLElement); - - /** - * 透传到 Popup 的属性对象 - */ - popupProps?: PopupProps; - /** - * 是否跟随滚动 - */ - followTrigger?: boolean; - - /** - * 是否为预览态 - */ - isPreview?: boolean; - - /** - * 预览态模式下渲染的内容 - * @param {Array} value 选择值 { label: , value:} - */ - renderPreview?: (data: string | Array, props: any | Array) => React.ReactNode; - - /** - * 是否开启虚拟滚动 - */ - useVirtual?: boolean; - - /** - * 是否关闭本地搜索 - */ - filterLocal?: boolean; - - immutable?: boolean; - - /** - * 填充到选择框里的值的 key,默认是 value - */ - fillProps?: string; - - /** - * 点击文本是否可以勾选 - */ - clickToCheck?: boolean; - - /** - * 渲染 Select 区域展现内容的方法 - * @param {Object} item 渲染项 - * @param {Object[]} itemPaths 渲染项在dataSource内的路径 - */ - valueRender?: (item: any, itemPaths: item[]) => React.ReactNode; -} - -export default class TreeSelect extends React.Component { - static Node: typeof Node; -} diff --git a/components/tree-select/index.jsx b/components/tree-select/index.tsx similarity index 52% rename from components/tree-select/index.jsx rename to components/tree-select/index.tsx index a8af758047..afa174429b 100644 --- a/components/tree-select/index.jsx +++ b/components/tree-select/index.tsx @@ -1,8 +1,15 @@ import ConfigProvider from '../config-provider'; +import TreeNode from '../tree/view/tree-node'; +import { assignSubComponent } from '../util/component'; import TreeSelect from './tree-select'; +import type { DeprecatedTreeSelectProps } from './types'; -export default ConfigProvider.config(TreeSelect, { - transform: /* istanbul ignore next */ (props, deprecated) => { +export type { TreeSelectProps } from './types'; + +const WithTreeSelectNode = assignSubComponent(TreeSelect, { Node: TreeNode }); + +export default ConfigProvider.config(WithTreeSelectNode, { + transform: (props, deprecated) => { if ('shape' in props) { deprecated('shape', 'hasBorder', 'TreeSelect'); const { shape, ...others } = props; @@ -11,7 +18,7 @@ export default ConfigProvider.config(TreeSelect, { if ('container' in props) { deprecated('container', 'popupContainer', 'TreeSelect'); - const { container, ...others } = props; + const { container, ...others } = props as DeprecatedTreeSelectProps; props = { popupContainer: container, ...others }; } diff --git a/components/tree-select/mobile/index.jsx b/components/tree-select/mobile/index.tsx similarity index 77% rename from components/tree-select/mobile/index.jsx rename to components/tree-select/mobile/index.tsx index 51930c8eda..57e9560865 100644 --- a/components/tree-select/mobile/index.jsx +++ b/components/tree-select/mobile/index.tsx @@ -1,3 +1,4 @@ +// @ts-expect-error meet-react does not export TreeSelect import { TreeSelect as MeetTreeSelect } from '@alifd/meet-react'; import NextTreeSelect from '../index'; diff --git a/components/tree-select/style.js b/components/tree-select/style.ts similarity index 100% rename from components/tree-select/style.js rename to components/tree-select/style.ts diff --git a/components/tree-select/tree-select.jsx b/components/tree-select/tree-select.tsx similarity index 72% rename from components/tree-select/tree-select.jsx rename to components/tree-select/tree-select.tsx index 4be98bcfdc..b7152acb4f 100644 --- a/components/tree-select/tree-select.jsx +++ b/components/tree-select/tree-select.tsx @@ -1,9 +1,9 @@ -import React, { Component, Children, isValidElement, cloneElement } from 'react'; +import React, { type ReactNode, Component, Children, isValidElement, cloneElement } from 'react'; import { polyfill } from 'react-lifecycles-compat'; import PropTypes from 'prop-types'; import classNames from 'classnames'; -import Select from '../select'; -import Tree from '../tree'; +import Select, { type DataSourceItem, type ObjectItem } from '../select'; +import Tree, { type NodeInstance, type TreeProps } from '../tree'; import { normalizeToArray, getAllCheckedKeys, @@ -14,38 +14,48 @@ import { import { func, obj, KEYCODE, str } from '../util'; import zhCN from '../locale/zh-cn'; import { getValueDataSource, valueToSelectKey } from '../select/util'; +import type { + Key, + TreeSelectProps, + TreeSelectState, + KeyEntities, + TreeSelectDataItem, + DataNode, +} from './types'; +import type { NodeElement } from '../tree/types'; const noop = () => {}; const { Node: TreeNode } = Tree; const { bindCtx } = func; const POS_REGEXP = /^\d+(-\d+){1,}$/; -const flatDataSource = props => { - const _k2n = {}; - const _p2n = {}; - const _v2n = {}; +const flatDataSource = (props: TreeSelectProps) => { + const _k2n: KeyEntities = {}; + const _p2n: KeyEntities = {}; + const _v2n: KeyEntities = {}; if ('dataSource' in props) { - const loop = (data, prefix = '0') => - data.map((item, index) => { + const loop = (data: TreeSelectDataItem[], prefix = '0') => + data.map((item: TreeSelectDataItem, index) => { const { value, children } = item; const pos = `${prefix}-${index}`; - const key = item.key || pos; + const key = (item.key as Key) || pos; - const newItem = { ...item, key, pos }; + const newItem = { ...item, key, pos } as DataNode; if (children && children.length) { newItem.children = loop(children, pos); } - _k2n[key] = _p2n[pos] = _v2n[value] = newItem; + // When null and undefined are used as keys of an object, they will be converted to the string type + _k2n[key] = _p2n[pos] = _v2n[value as string] = newItem; return newItem; }); - loop(props.dataSource); + loop(props.dataSource!); } else if ('children' in props) { - const loop = (children, prefix = '0') => + const loop = (children: ReactNode, prefix = '0') => Children.map(children, (node, index) => { - if (!React.isValidElement(node)) { + if (!isValidElement(node)) { return; } @@ -66,12 +76,12 @@ const flatDataSource = props => { return { _k2n, _p2n, _v2n }; }; -const isSearched = (label, searchedValue) => { +const isSearched = (label: ReactNode, searchedValue: string) => { let labelString = ''; searchedValue = String(searchedValue); - const loop = arg => { + const loop = (arg: ReactNode) => { if (isValidElement(arg) && arg.props.children) { Children.forEach(arg.props.children, loop); } else { @@ -90,19 +100,19 @@ const isSearched = (label, searchedValue) => { return false; }; -const getSearchKeys = (searchedValue, _k2n, _p2n) => { - const searchedKeys = []; - const retainedKeys = []; +const getSearchKeys = (searchedValue: string, _k2n: KeyEntities, _p2n: KeyEntities) => { + const searchedKeys: Key[] = []; + const retainedKeys: Key[] = []; Object.keys(_k2n).forEach(k => { const { label, pos } = _k2n[k]; if (isSearched(label, searchedValue)) { searchedKeys.push(k); - const posArr = pos.split('-'); - posArr.forEach((n, i) => { + const posArr = pos!.split('-'); + posArr.forEach((n: string, i: number) => { if (i > 0) { const p = posArr.slice(0, i + 1).join('-'); - const kk = _p2n[p].key; + const kk = _p2n[p].key as Key; if (retainedKeys.indexOf(kk) === -1) { retainedKeys.push(kk); } @@ -117,208 +127,64 @@ const getSearchKeys = (searchedValue, _k2n, _p2n) => { /** * TreeSelect */ -class TreeSelect extends Component { +class TreeSelect extends Component { static propTypes = { prefix: PropTypes.string, pure: PropTypes.bool, locale: PropTypes.object, className: PropTypes.string, - /** - * 树节点 - */ children: PropTypes.node, - /** - * 选择框大小 - */ size: PropTypes.oneOf(['small', 'medium', 'large']), - /** - * 选择框占位符 - */ placeholder: PropTypes.string, - /** - * 是否禁用 - */ disabled: PropTypes.bool, - /** - * 是否有下拉箭头 - */ hasArrow: PropTypes.bool, - /** - * 是否有边框 - */ hasBorder: PropTypes.bool, - /** - * 是否有清空按钮 - */ hasClear: PropTypes.bool, - /** - * 自定义内联 label - */ label: PropTypes.node, - /** - * 是否只读,只读模式下可以展开弹层但不能选择 - */ readOnly: PropTypes.bool, - /** - * 下拉框是否与选择器对齐 - */ autoWidth: PropTypes.bool, - /** - * 数据源,该属性优先级高于 children - */ dataSource: PropTypes.arrayOf(PropTypes.object), - /** - * value/defaultValue 在 dataSource 中不存在时,是否展示 - * @version 1.25 - */ preserveNonExistentValue: PropTypes.bool, - /** - * (受控)当前值 - */ - value: PropTypes.oneOfType([PropTypes.string, PropTypes.object, PropTypes.arrayOf(PropTypes.any)]), - /** - * (非受控)默认值 - */ - defaultValue: PropTypes.oneOfType([PropTypes.string, PropTypes.object, PropTypes.arrayOf(PropTypes.any)]), - /** - * 选中值改变时触发的回调函数 - * @param {String|Array} value 选中的值,单选时返回单个值,多选时返回数组 - * @param {Object|Array} data 选中的数据,包括 value, label, pos, key属性,单选时返回单个值,多选时返回数组,父子节点选中关联时,同时选中,只返回父节点 - */ + value: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.object, + PropTypes.arrayOf(PropTypes.any), + ]), + defaultValue: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.object, + PropTypes.arrayOf(PropTypes.any), + ]), onChange: PropTypes.func, - /** - * 是否一行显示,仅在 multiple 和 treeCheckable 为 true 时生效 - * @version 1.25 - */ tagInline: PropTypes.bool, - /** - * 隐藏多余 tag 时显示的内容,在 tagInline 生效时起作用 - * @param {Object[]} selectedValues 当前已选中的元素 - * @param {Object[]} totalValues 总待选元素 - * @returns {reactNode} - * @version 1.25 - */ maxTagPlaceholder: PropTypes.func, - /** - * 选择时是否自动清空 searchValue - * @version 1.26 - */ autoClearSearch: PropTypes.bool, - /** - * 是否显示搜索框 - */ showSearch: PropTypes.bool, - /** - * 是否使用本地过滤,在数据源为远程的时候需要关闭此项 - */ filterLocal: PropTypes.bool, - /** - * 在搜索框中输入时触发的回调函数 - * @param {String} keyword 输入的关键字 - */ onSearch: PropTypes.func, onSearchClear: PropTypes.func, - /** - * 无数据时显示内容 - */ notFoundContent: PropTypes.node, - /** - * 是否支持多选 - */ multiple: PropTypes.bool, - /** - * 下拉框中的树是否支持勾选节点的复选框 - */ treeCheckable: PropTypes.bool, - /** - * 下拉框中的树勾选节点复选框是否完全受控(父子节点选中状态不再关联) - */ treeCheckStrictly: PropTypes.bool, - /** - * 定义选中时回填的方式 - * @enumdesc 返回所有选中的节点, 父子节点都选中时只返回父节点, 父子节点都选中时只返回子节点 - */ treeCheckedStrategy: PropTypes.oneOf(['all', 'parent', 'child']), - /** - * 下拉框中的树是否默认展开所有节点 - */ treeDefaultExpandAll: PropTypes.bool, - /** - * 下拉框中的树默认展开节点key的数组 - */ treeDefaultExpandedKeys: PropTypes.arrayOf(PropTypes.string), - /** - * 下拉框中的树异步加载数据的函数,使用请参考[Tree的异步加载数据Demo](https://fusion.design/pc/component/tree?themeid=2#dynamic-container) - * @param {ReactElement} node 被点击展开的节点 - */ treeLoadData: PropTypes.func, - /** - * 透传到 Tree 的属性对象 - */ treeProps: PropTypes.object, - /** - * 初始下拉框是否显示 - */ defaultVisible: PropTypes.bool, - /** - * 当前下拉框是否显示 - */ visible: PropTypes.bool, - /** - * 下拉框显示或关闭时触发事件的回调函数 - * @param {Boolean} visible 是否显示 - * @param {String} type 触发显示关闭的操作类型 - */ onVisibleChange: PropTypes.func, - /** - * 下拉框自定义样式对象 - */ popupStyle: PropTypes.object, - /** - * 下拉框样式自定义类名 - */ popupClassName: PropTypes.string, - /** - * 下拉框挂载的容器节点 - */ popupContainer: PropTypes.any, - /** - * 透传到 Popup 的属性对象 - */ popupProps: PropTypes.object, - /** - * 是否跟随滚动 - */ followTrigger: PropTypes.bool, - /** - * 是否为预览态 - */ isPreview: PropTypes.bool, - /** - * 预览态模式下渲染的内容 - * @param {Array} value 选择值 { label: , value:} - */ renderPreview: PropTypes.func, - /** - * 是否开启虚拟滚动 - */ useVirtual: PropTypes.bool, - /** - * 是否是不可变数据 - * @version 1.23 - */ immutable: PropTypes.bool, - /** - * 点击文本是否可以勾选 - */ clickToCheck: PropTypes.bool, - /** - * 渲染 Select 展现内容的方法 - * @param {Object} item 渲染节点的item - * @param {Object[]} itemPaths item的全路径数组 - * @return {ReactNode} 展现内容 - * @default item => `item.label || item.value` - */ valueRender: PropTypes.func, useDetailValue: PropTypes.bool, }; @@ -360,14 +226,19 @@ class TreeSelect extends Component { clickToCheck: false, }; - constructor(props, context) { + tree: InstanceType; + select: InstanceType; + + constructor(props: TreeSelectProps, context?: unknown) { super(props, context); const { defaultVisible, visible, defaultValue, value } = props; this.state = { visible: typeof visible === 'undefined' ? defaultVisible : visible, - value: normalizeToArray(typeof value === 'undefined' ? defaultValue : value), + value: normalizeToArray( + typeof value === 'undefined' ? defaultValue : value + ) as TreeSelectState['value'], searchedValue: '', expandedKeys: [], searchedKeys: [], @@ -380,7 +251,12 @@ class TreeSelect extends Component { // init value/mapValueDS when defaultValue is not undefined if (this.state.value !== undefined) { - this.state.mapValueDS = getValueDataSource(this.state.value, this.state.mapValueDS).mapValueDS; + // @ts-expect-error Since `this.state` cannot be reassigned, use `@ts-expect-error` temporarily + this.state.mapValueDS = getValueDataSource( + this.state.value, + this.state.mapValueDS + ).mapValueDS; + // @ts-expect-error Since `this.state` cannot be reassigned, use `@ts-expect-error` temporarily this.state.value = this.state.value.map(v => { return valueToSelectKey(v); }); @@ -402,8 +278,8 @@ class TreeSelect extends Component { ]); } - static getDerivedStateFromProps(props, state) { - const st = {}; + static getDerivedStateFromProps(props: TreeSelectProps, state: TreeSelectState) { + const st = {} as Partial; if ('value' in props) { const valueArray = normalizeToArray(props.value); @@ -434,21 +310,21 @@ class TreeSelect extends Component { }; } - getKeysByValue(value) { + getKeysByValue(value: TreeSelectState['value']) { return value.reduce((ret, v) => { - const k = this.state._v2n[v] && this.state._v2n[v].key; + const k = this.state._v2n[v as string] && this.state._v2n[v as string].key; if (k) { ret.push(k); } return ret; - }, []); + }, [] as string[]); } - getValueByKeys(keys) { + getValueByKeys(keys: Key[]) { return keys.map(k => this.state._k2n[k].value); } - getFullItemPath(item) { + getFullItemPath(item: TreeSelectState['_k2n'][Key]) { if (!item) { return []; } @@ -463,7 +339,7 @@ class TreeSelect extends Component { return [item]; } - getValueForSelect(value) { + getValueForSelect(value: TreeSelectState['value']) { const { treeCheckedStrategy } = this.props; const nonExistentValueKeys = this.getNonExistentValueKeys(); @@ -487,20 +363,21 @@ class TreeSelect extends Component { return [...values, ...nonExistentValueKeys]; } - getData(value, forSelect) { + getData(value: TreeSelectState['value'], forSelect?: boolean) { const { preserveNonExistentValue } = this.props; const { mapValueDS } = this.state; - const ret = value.reduce((ret, v) => { + const ret = (value as DataSourceItem[]).reduce((ret: ObjectItem[], v: Key) => { const k = this.state._v2n[v] && this.state._v2n[v].key; if (k) { - const { label, pos, disabled, checkboxDisabled, children, ...rests } = this.state._k2n[k]; + const { label, pos, disabled, checkboxDisabled, children, ...rests } = + this.state._k2n[k]; const d = { ...rests, value: v, label, pos, - }; + } as ObjectItem; if (forSelect) { d.disabled = disabled || checkboxDisabled; } else { @@ -527,48 +404,52 @@ class TreeSelect extends Component { if (!preserveNonExistentValue) { return []; } - const nonExistentValues = value.filter(v => !this.state._v2n[v]); + const nonExistentValues = value.filter((v: Key) => !this.state._v2n[v]); return nonExistentValues; } getNonExistentValueKeys() { const nonExistentValues = this.getNonExistentValues(); return nonExistentValues.map(v => { - if (typeof v === 'object' && v.hasOwnProperty('value')) { - return v.value; + if (typeof v === 'object' && v!.hasOwnProperty('value')) { + // @ts-expect-error v must not be object + return v!.value; } return v; }); } - saveTreeRef(ref) { + saveTreeRef(ref: InstanceType) { this.tree = ref; } - saveSelectRef(ref) { + saveSelectRef(ref: InstanceType) { this.select = ref; } - handleVisibleChange(visible, type) { + handleVisibleChange(visible: boolean, type?: string) { if (!('visible' in this.props)) { this.setState({ visible, }); } - if (['fromTree', 'keyboard'].indexOf(type) !== -1 && !visible) { + if (['fromTree', 'keyboard'].indexOf(type!) !== -1 && !visible) { this.select.focusInput(); } - this.props.onVisibleChange(visible, type); + this.props.onVisibleChange!(visible, type!); } - triggerOnChange(value, data) { + triggerOnChange( + value: ObjectItem[] | DataSourceItem[] | ObjectItem['value'] | null, + data: ObjectItem[] | ObjectItem | null + ) { const { useDetailValue, onChange } = this.props; - onChange(useDetailValue ? data : value, data); + onChange!(useDetailValue ? data : value, data); } - handleSelect(selectedKeys, extra) { + handleSelect(selectedKeys: Key[], extra: { selected: boolean }) { const { multiple, autoClearSearch } = this.props; const { selected } = extra; @@ -589,7 +470,9 @@ class TreeSelect extends Component { const data = this.getData(value); const selectedData = this.getData(selectedValue); // 单选情况下,不返回 nonExistentValue,直接返回当前选择值,避免无法改选的问题 - multiple ? this.triggerOnChange(value, data) : this.triggerOnChange(selectedValue[0], selectedData[0]); + multiple + ? this.triggerOnChange(value, data) + : this.triggerOnChange(selectedValue[0], selectedData[0]); // clear search value manually if (autoClearSearch) { @@ -600,7 +483,7 @@ class TreeSelect extends Component { } } - handleCheck(checkedKeys) { + handleCheck(checkedKeys: Key[]) { const { autoClearSearch } = this.props; let value = this.getValueByKeys(checkedKeys); @@ -621,7 +504,7 @@ class TreeSelect extends Component { } } - handleRemove(removedItem) { + handleRemove(removedItem: ObjectItem) { const { value: removedValue } = removedItem; const { treeCheckable, treeCheckStrictly, treeCheckedStrategy } = this.props; @@ -630,17 +513,17 @@ class TreeSelect extends Component { // there's linkage relationship among nodes treeCheckable && !treeCheckStrictly && - ['parent', 'all'].indexOf(treeCheckedStrategy) !== -1 && + ['parent', 'all'].indexOf(treeCheckedStrategy!) !== -1 && // value exits in datasource - this.state._v2n[removedValue] + this.state._v2n[removedValue as string] ) { - const removedPos = this.state._v2n[removedValue].pos; - value = this.state.value.filter(v => { + const removedPos = this.state._v2n[removedValue as string].pos; + value = this.state.value.filter((v: string) => { const p = this.state._v2n[v].pos; - return !isDescendantOrSelf(removedPos, p); + return !isDescendantOrSelf(removedPos!, p!); }); - const nums = removedPos.split('-'); + const nums = removedPos!.split('-'); for (let i = nums.length; i > 2; i--) { const parentPos = nums.slice(0, i - 1).join('-'); const parentValue = this.state._p2n[parentPos].value; @@ -665,7 +548,7 @@ class TreeSelect extends Component { this.triggerOnChange(value, data); } - handleSearch(searchedValue) { + handleSearch(searchedValue: string) { const { _k2n, _p2n } = this.state; const { searchedKeys, retainedKeys } = getSearchKeys(searchedValue, _k2n, _p2n); @@ -677,25 +560,25 @@ class TreeSelect extends Component { autoExpandParent: true, }); - this.props.onSearch(searchedValue); + this.props.onSearch!(searchedValue); } - handleSearchClear(triggerType) { + handleSearchClear(triggerType: string) { this.setState({ searchedValue: '', expandedKeys: [], }); - this.props.onSearchClear(triggerType); + this.props.onSearchClear!(triggerType); } - handleExpand(expandedKeys) { + handleExpand(expandedKeys: Key[]) { this.setState({ expandedKeys, autoExpandParent: false, }); } - handleKeyDown(e) { + handleKeyDown(e: React.KeyboardEvent) { const { onKeyDown } = this.props; const { visible } = this.state; @@ -718,7 +601,7 @@ class TreeSelect extends Component { } } - handleChange(value, triggerType) { + handleChange(value: DataSourceItem[] | DataSourceItem, triggerType: string) { if (this.props.hasClear && triggerType === 'clear') { if (!('value' in this.props)) { this.setState({ @@ -730,15 +613,15 @@ class TreeSelect extends Component { } } - searchNodes(children) { + searchNodes(children: ReactNode) { const { searchedKeys, retainedKeys } = this.state; - const loop = children => { - const retainedNodes = []; - Children.forEach(children, child => { - if (searchedKeys.indexOf(child.key) > -1) { + const loop = (children: ReactNode) => { + const retainedNodes: NodeElement[] = []; + Children.forEach(children, (child: NodeElement) => { + if (searchedKeys.indexOf(child.key!) > -1) { retainedNodes.push(child); - } else if (retainedKeys.indexOf(child.key) > -1) { + } else if (retainedKeys.indexOf(child.key!) > -1) { const retainedNode = child.props.children ? cloneElement(child, {}, loop(child.props.children)) : child; @@ -756,32 +639,38 @@ class TreeSelect extends Component { return loop(children); } - createNodesByData(data, searching) { + createNodesByData(data: TreeSelectProps['dataSource'], searching?: boolean) { const { searchedKeys, retainedKeys } = this.state; - const loop = (data, isParentMatched, prefix = '0') => { - const retainedNodes = []; + const loop = ( + data: TreeSelectProps['dataSource'], + isParentMatched?: boolean, + prefix = '0' + ) => { + const retainedNodes: NodeElement[] = []; - data.forEach((item, index) => { - const { children, ...others } = item; + data!.forEach((item, index) => { + const { children, ...others } = item as TreeSelectDataItem; const pos = `${prefix}-${index}`; const key = this.state._p2n[pos].key; - const addNode = (isParentMatched, hide) => { + const addNode = (isParentMatched?: boolean, hide?: boolean) => { if (hide) { others.style = { display: 'none' }; } retainedNodes.push( - {children && children.length ? loop(children, isParentMatched, pos) : null} + {children && children.length + ? loop(children, isParentMatched, pos) + : null} ); }; if (searching) { - if (searchedKeys.indexOf(key) > -1 || isParentMatched) { + if (searchedKeys.indexOf(key!) > -1 || isParentMatched) { addNode(true); - } else if (retainedKeys.indexOf(key) > -1) { + } else if (retainedKeys.indexOf(key!) > -1) { addNode(false); } else { addNode(false, true); @@ -836,7 +725,7 @@ class TreeSelect extends Component { useVirtual, isNodeBlock: true, clickToCheck, - }; + } as TreeProps; // 使用虚拟滚动 设置默认高度 if (useVirtual) { @@ -861,7 +750,7 @@ class TreeSelect extends Component { } else { treeProps.selectedKeys = keys; if (!readOnly) { - treeProps.onSelect = this.handleSelect; + treeProps.onSelect = this.handleSelect as TreeProps['onSelect']; } } @@ -871,12 +760,14 @@ class TreeSelect extends Component { treeProps.expandedKeys = expandedKeys; treeProps.autoExpandParent = autoExpandParent; treeProps.onExpand = this.handleExpand; - treeProps.filterTreeNode = node => { - return searchedKeys.indexOf(node.props.eventKey) > -1; + treeProps.filterTreeNode = (node: NodeInstance) => { + return searchedKeys.indexOf(node.props.eventKey!) > -1; }; if (searchedKeys.length) { - newChildren = dataSource ? this.createNodesByData(dataSource, true) : this.searchNodes(children); + newChildren = dataSource + ? this.createNodesByData(dataSource, true) + : this.searchNodes(children); } else { notFound = true; } @@ -902,7 +793,9 @@ class TreeSelect extends Component { return (
{notFound ? ( -
{notFoundContent}
+
+ {notFoundContent} +
) : ( {newChildren} @@ -912,7 +805,10 @@ class TreeSelect extends Component { ); } - renderPreview(data, others) { + renderPreview( + data: ObjectItem[] | ObjectItem, + others: Omit + ) { const { prefix, className, renderPreview } = this.props; const previewCls = classNames(className, `${prefix}form-preview`); @@ -944,12 +840,12 @@ class TreeSelect extends Component { * treeCheckedStrategy = 'parent': totalValue 无意义,不返回 * treeCheckedStrategy = 'child': totalValue 为所有 leaf 节点 */ - renderMaxTagPlaceholder(value, totalValue) { + renderMaxTagPlaceholder(value: ObjectItem[], totalValue: ObjectItem[]) { // 这里的 totalValue 为所有 leaf 节点 const { treeCheckStrictly, maxTagPlaceholder, treeCheckedStrategy, locale } = this.props; const { _v2n } = this.state; - let treeSelectTotalValue = totalValue; // all the leaf nodes + let treeSelectTotalValue: ObjectItem[] | undefined = totalValue; // all the leaf nodes // calculate total value if (treeCheckStrictly) { @@ -971,17 +867,16 @@ class TreeSelect extends Component { // default render function if (treeCheckedStrategy === 'parent') { // don't show totalValue when treeCheckedStrategy = 'parent' - return `${str.template(locale.shortMaxTagPlaceholder, { + return `${str.template(locale!.shortMaxTagPlaceholder, { selected: value.length, })}`; } - return `${str.template(locale.maxTagPlaceholder, { + return `${str.template(locale!.maxTagPlaceholder, { selected: value.length, - total: treeSelectTotalValue.length, + total: treeSelectTotalValue!.length, })}`; } - /*eslint-enable*/ render() { const { prefix, @@ -1015,16 +910,17 @@ class TreeSelect extends Component { const valueRenderProps = typeof valueRender === 'function' ? { - valueRender: item => { + valueRender: (item: TreeSelectState['_k2n'][Key]) => { return valueRender(item, this.getFullItemPath(item)); }, } : undefined; // if (non-leaf 节点可选 & 父子节点选中状态需要联动),需要额外计算父子节点间的联动关系 - const valueForSelect = treeCheckable && !treeCheckStrictly ? this.getValueForSelect(value) : value; + const valueForSelect = + treeCheckable && !treeCheckStrictly ? this.getValueForSelect(value) : value; - let data = this.getData(valueForSelect, true); + let data: ObjectItem[] | ObjectItem = this.getData(valueForSelect, true); if (!multiple && !treeCheckable) { data = data[0]; } @@ -1073,6 +969,4 @@ class TreeSelect extends Component { } } -TreeSelect.Node = TreeNode; - export default polyfill(TreeSelect); diff --git a/components/tree-select/types.ts b/components/tree-select/types.ts new file mode 100644 index 0000000000..c2d71bd79a --- /dev/null +++ b/components/tree-select/types.ts @@ -0,0 +1,375 @@ +import type React from 'react'; +import type { CommonProps } from '../util'; +import type { PopupProps } from '../overlay'; +import type { TreeProps } from '../tree'; +import type { ObjectItem } from '../select'; +import type { DataSourceItem, BaseProps as SelectProps } from '../select/types'; +import type { BasicDataNode } from '../tree/types'; + +export type Key = string; +export interface DataNode extends ObjectItem, BasicDataNode { + key: Key; + pos: string; + children?: DataNode[]; +} +export interface TreeSelectDataItem extends ObjectItem, BasicDataNode { + children?: TreeSelectDataItem[]; +} + +export type KeyEntities = Record; + +type HTMLAttributesWeak = Omit, 'defaultValue' | 'onChange'>; + +/** + * @api TreeSelect + */ +export interface TreeSelectProps extends HTMLAttributesWeak, CommonProps { + /** + * 树节点 + * @en Tree node + */ + children?: React.ReactNode; + + /** + * 选择框大小 + * @en Select size + * @defaultValue 'medium' + */ + size?: 'small' | 'medium' | 'large'; + + /** + * 选择框占位符 + * @en Select placeholder + */ + placeholder?: string; + + /** + * 是否禁用 + * @en Whether to be disabled + * @defaultValue false + */ + disabled?: boolean; + + /** + * 是否有下拉箭头 + * @en Whether to show the arrow + * @defaultValue true + */ + hasArrow?: boolean; + + /** + * 是否有边框 + * @en Whether to show the border + * @defaultValue true + */ + hasBorder?: boolean; + + /** + * 是否有清空按钮 + * @en Whether to show the clear button + * @defaultValue true + */ + hasClear?: boolean; + + /** + * 自定义内联 label + * @en Custom inline label + */ + label?: React.ReactNode; + + /** + * 是否只读,只读模式下可以展开弹层但不能选择 + * @en Whether to be read-only (read-only mode can expand the popup but cannot select) + */ + readOnly?: boolean; + + /** + * 下拉框是否与选择器对齐 + * @en Whether the drop-down box is aligned with the selector + * @defaultValue true + */ + autoWidth?: boolean; + + /** + * 数据源,该属性优先级高于 children + * @en Data source (higher priority than children) + */ + dataSource?: TreeSelectDataItem[]; + + /** + * (受控)当前值 + * @en Current value (Controlled) + */ + value?: DataSourceItem[] | DataSourceItem; + + /** + * (非受控)默认值 + * @en Default value (Uncontrolled) + * @defaultValue null + */ + defaultValue?: SelectProps['defaultValue']; + + /** + * value/defaultValue 在 dataSource 中不存在时,是否展示 + * @en Whether to display when value/defaultValue does not exist in dataSource + * @defaultValue false + * @version 1.25 + */ + preserveNonExistentValue?: boolean; + + /** + * 选中值改变时触发的回调函数 + * @en Callback when the selected value changes + * @defaultValue () =\> \{\} + */ + onChange?: ( + value: DataSourceItem[] | DataSourceItem, + data: ObjectItem[] | ObjectItem | null + ) => void; + + /** + * onChange 返回的 value 使用 dataSource 的对象 + * @en onChange returns the value using the object in dataSource + * @skip + */ + useDetailValue?: boolean; + + /** + * 是否一行显示,仅在 multiple 和 treeCheckable 为 true 时生效 + * @en Whether to display on one line (only effective when multiple and treeCheckable are true) + * @defaultValue false + * @version 1.25 + */ + tagInline?: boolean; + + /** + * 隐藏多余 tag 时显示的内容,在 tagInline 生效时起作用 + * @en Content to display when hiding excess tags (effective when tagInline is true) + * @param selectedValues - 当前已选中的元素 - Selected element + * @param totalValues - 总待选元素,treeCheckedStrategy = 'parent' 时为 undefined - Total pending element, treeCheckedStrategy = 'parent' is undefined + * @returns ReactNode | HTMLElement - ReactNode or HTMLElement + * @version 1.25 + */ + maxTagPlaceholder?: ( + selectedValues: ObjectItem[], + totalValues?: ObjectItem[] + ) => React.ReactNode | HTMLElement; + + /** + * 是否自动清除 searchValue + * @en Whether to automatically clear searchValue + * @defaultValue true + * @version 1.26 + */ + autoClearSearch?: boolean; + + /** + * 是否显示搜索框 + * @en Whether to show the search box + * @defaultValue false + */ + showSearch?: boolean; + + /** + * 在搜索框中输入时触发的回调函数 + * @en Callback when input in search box changes + * @defaultValue () =\> \{\} + */ + onSearch?: (keyword: string) => void; + /** + * onSearchClear + * @skip + */ + onSearchClear?: (actionType: string) => void; + + /** + * 无数据时显示内容 + * @en Content to display when there is no data + * @defaultValue 'Not Found' + */ + notFoundContent?: React.ReactNode; + + /** + * 是否支持多选 + * @en Whether to support multiple selection + * @defaultValue false + */ + multiple?: boolean; + + /** + * 下拉框中的树是否支持勾选节点的复选框 + * @en Whether the check box of the tree in the drop-down box supports the selection of the child node checkbox + * @defaultValue false + */ + treeCheckable?: boolean; + + /** + * 下拉框中的树勾选节点复选框是否完全受控(父子节点选中状态不再关联) + * @en Whether the check box of the tree in the drop-down box is completely controlled (the parent-child node selected status is no longer associated) + * @defaultValue false + */ + treeCheckStrictly?: boolean; + + /** + * 定义选中时回填的方式 + * @en Definition of how to fill in when selected + * @defaultValue 'parent' + */ + treeCheckedStrategy?: 'all' | 'parent' | 'child'; + + /** + * 下拉框中的树是否默认展开所有节点 + * @en Whether the tree in the drop-down box is expanded by default all nodes + * @defaultValue false + */ + treeDefaultExpandAll?: boolean; + + /** + * 下拉框中的树默认展开节点key的数组 + * @en The array of keys of the nodes expanded by default in the tree in the drop-down box + * @defaultValue [] + */ + treeDefaultExpandedKeys?: Array; + + /** + * 下拉框中的树异步加载数据的函数,使用请参考[Tree的异步加载数据Demo](https://fusion.design/pc/component/basic/tree#%E5%BC%82%E6%AD%A5%E5%8A%A0%E8%BD%BD%E6%95%B0%E6%8D%AE) + * @en The function of asynchronous loading data in the tree in the drop-down box, please refer to [Tree asynchronous loading data Demo](https://fusion.design/pc/component/basic/tree#%E5%BC%82%E6%AD%A5%E5%8A%A0%E8%BD%BD%E6%95%B0%E6%8D%AE) + */ + treeLoadData?: TreeProps['loadData']; + + /** + * 透传到 Tree 的属性对象 + * @en Pass-through to the property object of Tree + * @defaultValue \{\} + */ + treeProps?: TreeProps; + + /** + * 初始下拉框是否显示 + * @en Initial display state of the drop-down box + * @defaultValue false + */ + defaultVisible?: boolean; + + /** + * 当前下拉框是否显示 + * @en Current display state of the drop-down box (Controlled) + */ + visible?: boolean; + + /** + * 下拉框显示或关闭时触发事件的回调函数 + * @en Callback when the drop-down box is displayed or closed + * @defaultValue () =\> \{\} + */ + onVisibleChange?: (visible: boolean, type: string) => void; + + /** + * 下拉框自定义样式对象 + * @en Custom style object for the drop-down box + */ + popupStyle?: React.CSSProperties; + + /** + * 下拉框样式自定义类名 + * @en Custom class name for the drop-down box + */ + popupClassName?: string; + + /** + * 下拉框挂载的容器节点 + * @en Mounting container node for the drop-down box + */ + popupContainer?: string | HTMLElement | ((target: HTMLElement) => HTMLElement); + + /** + * 透传到 Popup 的属性对象 + * @en Pass-through to the property object of Popup + */ + popupProps?: PopupProps; + + /** + * 是否跟随滚动 + * @en Whether to follow scrolling + */ + followTrigger?: boolean; + + /** + * 是否为预览态 + * @en Whether it is in preview mode + */ + isPreview?: boolean; + + /** + * 预览态模式下渲染的内容 + * @en Content rendered in preview mode + */ + renderPreview?: (data: ObjectItem[], props: TreeSelectProps) => React.ReactNode; + + /** + * 是否开启虚拟滚动 + * @en Whether to open virtual scrolling + * @defaultValue false + */ + useVirtual?: boolean; + + /** + * 是否关闭本地搜索 + * @en Whether to close local search + * @defaultValue true + */ + filterLocal?: boolean; + + /** + * 是否是不可变数据 + * @en Whether it is immutable data + * @version 1.23 + */ + immutable?: boolean; + + /** + * 填充到选择框里的值的 key,默认是 value + * @en The key of the value filled in to the Select box, the default is value + * @skip + */ + fillProps?: string; + + /** + * 点击文本是否可以勾选 + * @en Whether clicking on the text can be selected + * @defaultValue false + */ + clickToCheck?: boolean; + + /** + * 渲染 Select 区域展现内容的方法 + * @en Method for rendering Select area display content + * @defaultValue (item) =\> item.label || item.value + * @param item - 渲染项 - Extra item + * @param itemPaths - 渲染项在dataSource内的路径 - Extra item path + * @returns ReactNode - 展现内容 - Display content + */ + valueRender?: (item: TreeSelectState['_k2n'][Key], itemPaths: ObjectItem[]) => React.ReactNode; +} + +export interface TreeSelectState { + visible: TreeSelectProps['visible']; + value: ObjectItem['value'][]; + searchedValue: string; + expandedKeys: Key[]; + searchedKeys: Key[]; + retainedKeys: Key[]; + autoExpandParent: boolean; + mapValueDS: Record; + _k2n: KeyEntities; + _p2n: KeyEntities; + _v2n: KeyEntities; +} + +export interface DeprecatedTreeSelectProps extends TreeSelectProps { + /** + * Use popupContainer instead + * @deprecated Use popupContainer instead + */ + container?: TreeSelectProps['popupContainer']; +} diff --git a/components/tree/__docs__/index.en-us.md b/components/tree/__docs__/index.en-us.md index 9136ce8dd7..472cef72b1 100644 --- a/components/tree/__docs__/index.en-us.md +++ b/components/tree/__docs__/index.en-us.md @@ -17,48 +17,48 @@ Folders, organizational structures, taxonomy, countries, regions, etc. Most of t ### Tree -| Param | Description | Type | Default Value | Required | Supported Version | -| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -------- | ----------------- | -| children | Chidren of the tree component | React.ReactNode | - | | - | -| dataSource | Data source of the tree component | TreeDataType[] | - | | - | -| showLine | Show line or not | boolean | false | yes | - | -| selectable | Selectable nodes or not | boolean | false | yes | - | -| selectedKeys | Selected keys array (controlled) | string[] | - | | - | -| defaultSelectedKeys | Default selected keys array | string[] | [] | | - | -| onSelect | Callback function when selected or unselected a node

**signature**:
**params**:
_selectedKeys_: Array of selected keys
_extra_: Extra parameters
**return**:
Void | (
selectedKeys: string[],
extra: {
selectedNodes: Array;
node: NodeInstance;
selected: boolean;
event: React.KeyboardEvent;
}
) => void | () =\> \{\} | | - | -| multiple | Multiple selectable | boolean | false | | - | -| checkable | Checkable nodes or not | boolean | false | | - | -| checkedKeys | Checked keys array or `{checked: Array, indeterminate: Array}` object (controlled) | \| {
checked: string[];
indeterminate: string[];
}
\| string[] | - | | - | -| defaultCheckedKeys | Default checked keys array (uncontrolled) | \| {
checked: string[];
indeterminate: string[];
}
\| string[] | [] | yes | - | -| checkStrictly | Whether the checkbox is fully controlled (parent | boolean | false | yes | - | -| checkedStrategy | Define how to fill the checked state when selected | 'all' \| 'parent' \| 'child' | 'all' | yes | - | -| onCheck | Callback function when checked or unchecked a checkbox

**signature**:
**params**:
_checkedKeys_: Array of checked keys
_extra_: Extra parameters
**return**:
Void | (
checkedKeys: string[],
extra: {
checkedNodes: Array;
checkedNodesPositions: Array;
indeterminateKeys: string[];
node: NodeInstance;
checked: boolean;
key: Key;
}
) => void | () =\> \{\} | yes | - | -| expandedKeys | Expanded keys array (controlled) | string[] | - | | - | -| defaultExpandedKeys | Default expanded keys array (uncontrolled) | string[] | [] | yes | - | -| defaultExpandAll | Whether to expand all nodes by default | boolean | false | yes | - | -| autoExpandParent | Whether to automatically expand parent nodes | boolean | true | yes | - | -| onExpand | Callback function when expanded or collapsed a node

**signature**:
**params**:
_expandedKeys_: Array of expanded keys
_extra_: Extra parameters
**return**:
Void | (
expandedKeys: string[],
extra: {
node: NodeInstance;
expanded: boolean;
}
) => void | () =\> \{\} | yes | - | -| editable | Editable nodes or not | boolean | false | yes | - | -| onEditFinish | Callback function when edit finish

**signature**:
**params**:
_key_: Node key
_label_: Node label
_node_: Node instance
**return**:
Void | (key: Key, label: string, node: NodeInstance) => void | () =\> \{\} | yes | - | -| draggable | Draggable nodes or not | boolean | false | yes | - | -| onDragStart | Callback function when drag start

**signature**:
**params**:
_info_: Extra parameters
**return**:
Void | (info: {
event: React.MouseEvent;
node: NodeInstance;
expandedKeys: string[];
}) => void | () =\> \{\} | yes | - | -| onDragEnter | Callback function when drag enter

**signature**:
**params**:
_info_: Extra parameters
**return**:
Void | (info: {
event: React.MouseEvent;
node: NodeInstance;
expandedKeys: Array;
}) => void | () =\> \{\} | yes | - | -| onDragOver | Callback function when drag over

**signature**:
**params**:
_info_: Extra parameters
**return**:
Void | (info: { event: React.MouseEvent; node: NodeInstance }) => void | () =\> \{\} | yes | - | -| onDragLeave | Callback function when drag leave

**signature**:
**params**:
_info_: Extra parameters
**return**:
Void | (info: { event: React.MouseEvent; node: NodeInstance }) => void | () =\> \{\} | yes | - | -| onDragEnd | Callback function when drag end

**signature**:
**params**:
_info_: Extra parameters
**return**:
Void | (info: { event: React.MouseEvent; node: NodeInstance }) => void | () =\> \{\} | yes | - | -| onDrop | Callback function when drop

**signature**:
**params**:
_info_: Extra parameters
**return**:
Void | (info: {
event: React.MouseEvent;
node: NodeInstance;
dragNode: NodeInstance;
dragNodesKeys: Array;
dropPosition: number;
}) => void | () =\> \{\} | yes | - | -| canDrop | Whether the node can be a target node for drag

**signature**:
**params**:
_info_: Extra parameters
**return**:
Whether can be a target node | (info: {
event?: React.MouseEvent;
node: NodeInstance;
dragNode: NodeInstance;
dragNodesKeys: Array;
dropPosition: number;
}) => boolean | () =\> \{\} | yes | - | -| loadData | Asynchronous load data function

**signature**:
**params**:
_node_: Node instance | (node: NodeInstance) => Promise | - | | - | -| filterTreeNode | Highlight the node

**signature**:
**params**:
_node_: Node instance
**return**:
Whether is filtered | (node: NodeInstance) => boolean | - | | - | -| onRightClick | Callback function when right click a node

**signature**:
**params**:
_info_: Extra parameters
**return**:
Void | (info: { event: React.MouseEvent; node: NodeInstance }) => void | () =\> \{\} | yes | - | -| isLabelBlock | Whether the node occupies the remaining space | boolean | false | | - | -| isNodeBlock | Whether the node occupies a line | boolean \| Record | false | | - | -| animation | Whether to open the animation | boolean | true | | - | -| focusedKey | Current focused submenu or menu item key | Key | - | | - | -| renderChildNodes | Render child nodes

**signature**:
**params**:
_nodes_: All child nodes
**return**:
Child nodes | (nodes: NodeElement[]) => React.ReactNode | - | | - | -| labelRender | Render a single child node

**signature**:
**params**:
_node_: Node data
**return**:
Return node | (node: Record) => React.ReactNode | - | | 1.25 | -| immutable | Whether it is immutable data | boolean | false | | 1.23 | -| useVirtual | Whether to open virtual scrolling | boolean | false | | - | +| Param | Description | Type | Default Value | Required | Supported Version | +| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -------- | ----------------- | +| children | Chidren of the tree component | React.ReactNode | - | | - | +| dataSource | Data source of the tree component | TreeDataType[] | - | | - | +| showLine | Show line or not | boolean | false | | - | +| selectable | Selectable nodes or not | boolean | false | | - | +| selectedKeys | Selected keys array (controlled) | string[] | - | | - | +| defaultSelectedKeys | Default selected keys array | string[] | [] | | - | +| onSelect | Callback function when selected or unselected a node

**signature**:
**params**:
_selectedKeys_: Array of selected keys
_extra_: Extra parameters
**return**:
Void | (
selectedKeys: string[],
extra: {
selectedNodes: Array\;
node: NodeInstance;
selected: boolean;
event: React.KeyboardEvent \| React.MouseEvent;
}
) => void | () =\> \{\} | | - | +| multiple | Multiple selectable | boolean | false | | - | +| checkable | Checkable nodes or not | boolean | false | | - | +| checkedKeys | Checked keys array or `{checked: Array, indeterminate: Array}` object (controlled) | \| {
checked: string[];
indeterminate: string[];
}
\| string[] | - | | - | +| defaultCheckedKeys | Default checked keys array (uncontrolled) | \| {
checked: string[];
indeterminate: string[];
}
\| string[] | [] | | - | +| checkStrictly | Whether the checkbox is fully controlled (parent | boolean | false | | - | +| checkedStrategy | Define how to fill the checked state when selected | 'all' \| 'parent' \| 'child' | 'all' | | - | +| onCheck | Callback function when checked or unchecked a checkbox

**signature**:
**params**:
_checkedKeys_: Array of checked keys
_extra_: Extra parameters
**return**:
Void | (
checkedKeys: string[],
extra: {
checkedNodes: Array\;
checkedNodesPositions: Array\;
indeterminateKeys: string[];
node: NodeInstance;
checked: boolean;
key: Key;
}
) => void | () =\> \{\} | | - | +| expandedKeys | Expanded keys array (controlled) | string[] | - | | - | +| defaultExpandedKeys | Default expanded keys array (uncontrolled) | string[] | [] | | - | +| defaultExpandAll | Whether to expand all nodes by default | boolean | false | | - | +| autoExpandParent | Whether to automatically expand parent nodes | boolean | true | | - | +| onExpand | Callback function when expanded or collapsed a node

**signature**:
**params**:
_expandedKeys_: Array of expanded keys
_extra_: Extra parameters
**return**:
Void | (
expandedKeys: string[],
extra: {
node: NodeInstance;
expanded: boolean;
}
) => void | () =\> \{\} | | - | +| editable | Editable nodes or not | boolean | false | | - | +| onEditFinish | Callback function when edit finish

**signature**:
**params**:
_key_: Node key
_label_: Node label
_node_: Node instance
**return**:
Void | (key: Key, label: string, node: NodeInstance) => void | () =\> \{\} | | - | +| draggable | Draggable nodes or not | boolean | false | | - | +| onDragStart | Callback function when drag start

**signature**:
**params**:
_info_: Extra parameters
**return**:
Void | (info: {
event: React.MouseEvent;
node: NodeInstance;
expandedKeys: string[];
}) => void | () =\> \{\} | | - | +| onDragEnter | Callback function when drag enter

**signature**:
**params**:
_info_: Extra parameters
**return**:
Void | (info: {
event: React.MouseEvent;
node: NodeInstance;
expandedKeys: Array\;
}) => void | () =\> \{\} | | - | +| onDragOver | Callback function when drag over

**signature**:
**params**:
_info_: Extra parameters
**return**:
Void | (info: { event: React.MouseEvent; node: NodeInstance }) => void | () =\> \{\} | | - | +| onDragLeave | Callback function when drag leave

**signature**:
**params**:
_info_: Extra parameters
**return**:
Void | (info: { event: React.MouseEvent; node: NodeInstance }) => void | () =\> \{\} | | - | +| onDragEnd | Callback function when drag end

**signature**:
**params**:
_info_: Extra parameters
**return**:
Void | (info: { event: React.MouseEvent; node: NodeInstance }) => void | () =\> \{\} | | - | +| onDrop | Callback function when drop

**signature**:
**params**:
_info_: Extra parameters
**return**:
Void | (info: {
event: React.MouseEvent;
node: NodeInstance;
dragNode: NodeInstance;
dragNodesKeys: Array\;
dropPosition: number;
}) => void | () =\> \{\} | | - | +| canDrop | Whether the node can be a target node for drag

**signature**:
**params**:
_info_: Extra parameters
**return**:
Whether can be a target node | (info: {
event?: React.MouseEvent;
node: NodeInstance;
dragNode: NodeInstance;
dragNodesKeys: Array\;
dropPosition: number;
}) => boolean | () =\> \{\} | | - | +| loadData | Asynchronous load data function

**signature**:
**params**:
_node_: Node instance | (node: NodeInstance) => Promise\ | - | | - | +| filterTreeNode | Highlight the node

**signature**:
**params**:
_node_: Node instance
**return**:
Whether is filtered | (node: NodeInstance) => boolean | - | | - | +| onRightClick | Callback function when right click a node

**signature**:
**params**:
_info_: Extra parameters
**return**:
Void | (info: { event: React.MouseEvent; node: NodeInstance }) => void | () =\> \{\} | | - | +| isLabelBlock | Whether the node occupies the remaining space | boolean | false | | - | +| isNodeBlock | Whether the node occupies a line | boolean \| Record\ | false | | - | +| animation | Whether to open the animation | boolean | true | | - | +| focusedKey | Current focused submenu or menu item key | Key | - | | - | +| renderChildNodes | Render child nodes

**signature**:
**params**:
_nodes_: All child nodes
**return**:
Child nodes | (nodes: NodeElement[]) => React.ReactNode | - | | - | +| labelRender | Render a single child node

**signature**:
**params**:
_node_: Node data
**return**:
Return node | (node: Record\) => React.ReactNode | - | | 1.25 | +| immutable | Whether it is immutable data | boolean | false | | 1.23 | +| useVirtual | Whether to open virtual scrolling | boolean | false | | - | ### Tree.Node diff --git a/components/tree/__docs__/index.md b/components/tree/__docs__/index.md index c894046b5b..107bd72c8d 100644 --- a/components/tree/__docs__/index.md +++ b/components/tree/__docs__/index.md @@ -17,48 +17,48 @@ ### Tree -| 参数 | 说明 | 类型 | 默认值 | 是否必填 | 支持版本 | -| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | -------- | -------- | -| children | 树节点 | React.ReactNode | - | | - | -| dataSource | 数据源,该属性优先级高于 children | TreeDataType[] | - | | - | -| showLine | 是否显示树的线 | boolean | false | 是 | - | -| selectable | 是否支持选中节点 | boolean | false | 是 | - | -| selectedKeys | (用于受控)当前选中节点 key 的数组 | string[] | - | | - | -| defaultSelectedKeys | (用于非受控)默认选中节点 key 的数组 | string[] | [] | | - | -| onSelect | 选中或取消选中节点时触发的回调函数

**签名**:
**参数**:
_selectedKeys_: 选中节点key的数组
_extra_: 额外参数
**返回值**:
空 | (
selectedKeys: string[],
extra: {
selectedNodes: Array;
node: NodeInstance;
selected: boolean;
event: React.KeyboardEvent;
}
) => void | () =\> \{\} | | - | -| multiple | 是否支持多选 | boolean | false | | - | -| checkable | 是否支持勾选节点的复选框 | boolean | false | | - | -| checkedKeys | (用于受控)当前勾选复选框节点 key 的数组或 `{checked: Array, indeterminate: Array}` 的对象 | \| {
checked: string[];
indeterminate: string[];
}
\| string[] | - | | - | -| defaultCheckedKeys | (用于非受控)默认勾选复选框节点 key 的数组 | \| {
checked: string[];
indeterminate: string[];
}
\| string[] | [] | 是 | - | -| checkStrictly | 勾选节点复选框是否完全受控(父子节点选中状态不再关联) | boolean | false | 是 | - | -| checkedStrategy | 定义选中时回填的方式 | 'all' \| 'parent' \| 'child' | 'all' | 是 | - | -| onCheck | 勾选或取消勾选复选框时触发的回调函数

**签名**:
**参数**:
_checkedKeys_: 勾选复选框节点key的数组
_extra_: 额外参数
**返回值**:
空 | (
checkedKeys: string[],
extra: {
checkedNodes: Array;
checkedNodesPositions: Array;
indeterminateKeys: string[];
node: NodeInstance;
checked: boolean;
key: Key;
}
) => void | () =\> \{\} | 是 | - | -| expandedKeys | (用于受控)当前展开的节点 key 的数组 | string[] | - | | - | -| defaultExpandedKeys | (用于非受控)默认展开的节点 key 的数组 | string[] | [] | 是 | - | -| defaultExpandAll | 是否默认展开所有节点 | boolean | false | 是 | - | -| autoExpandParent | 是否自动展开父节点 | boolean | true | 是 | - | -| onExpand | 展开或收起节点时触发的回调函数

**签名**:
**参数**:
_expandedKeys_: 展开节点key的数组
_extra_: 额外参数
**返回值**:
空 | (
expandedKeys: string[],
extra: {
node: NodeInstance;
expanded: boolean;
}
) => void | () =\> \{\} | 是 | - | -| editable | 是否支持编辑节点内容 | boolean | false | 是 | - | -| onEditFinish | 编辑节点内容完成时触发的回调函数

**签名**:
**参数**:
_key_: 编辑节点 key
_label_: 编辑节点完成时节点的文本
_node_: 当前编辑的节点
**返回值**:
空 | (key: Key, label: string, node: NodeInstance) => void | () =\> \{\} | 是 | - | -| draggable | 是否支持拖拽节点 | boolean | false | 是 | - | -| onDragStart | 开始拖拽节点时触发的回调函数

**签名**:
**参数**:
_info_: 额外参数
**返回值**:
空 | (info: {
event: React.MouseEvent;
node: NodeInstance;
expandedKeys: string[];
}) => void | () =\> \{\} | 是 | - | -| onDragEnter | 拖拽节点进入目标节点时触发的回调函数

**签名**:
**参数**:
_info_: 额外参数
**返回值**:
空 | (info: {
event: React.MouseEvent;
node: NodeInstance;
expandedKeys: Array;
}) => void | () =\> \{\} | 是 | - | -| onDragOver | 拖拽节点在目标节点上移动的时候触发的回调函数

**签名**:
**参数**:
_info_: 额外参数
**返回值**:
空 | (info: { event: React.MouseEvent; node: NodeInstance }) => void | () =\> \{\} | 是 | - | -| onDragLeave | 拖拽节点离开目标节点时触发的回调函数

**签名**:
**参数**:
_info_: 额外参数
**返回值**:
空 | (info: { event: React.MouseEvent; node: NodeInstance }) => void | () =\> \{\} | 是 | - | -| onDragEnd | 拖拽节点拖拽结束时触发的回调函数

**签名**:
**参数**:
_info_: 额外参数
**返回值**:
空 | (info: { event: React.MouseEvent; node: NodeInstance }) => void | () =\> \{\} | 是 | - | -| onDrop | 拖拽节点放入目标节点内或前后触发的回调函数

**签名**:
**参数**:
_info_: 额外参数
**返回值**:
空 | (info: {
event: React.MouseEvent;
node: NodeInstance;
dragNode: NodeInstance;
dragNodesKeys: Array;
dropPosition: number;
}) => void | () =\> \{\} | 是 | - | -| canDrop | 节点是否可被作为拖拽的目标节点

**签名**:
**参数**:
_info_: 额外参数
**返回值**:
是否可以被当作目标节点 | (info: {
event?: React.MouseEvent;
node: NodeInstance;
dragNode: NodeInstance;
dragNodesKeys: Array;
dropPosition: number;
}) => boolean | () =\> \{\} | 是 | - | -| loadData | 异步加载数据的函数

**签名**:
**参数**:
_node_: 被点击展开的节点 | (node: NodeInstance) => Promise | - | | - | -| filterTreeNode | 按需筛选高亮节点

**签名**:
**参数**:
_node_: 待筛选的节点
**返回值**:
是否被筛选中 | (node: NodeInstance) => boolean | - | | - | -| onRightClick | 右键点击节点时触发的回调函数

**签名**:
**参数**:
_info_: 额外参数
**返回值**:
空 | (info: { event: React.MouseEvent; node: NodeInstance }) => void | () =\> \{\} | 是 | - | -| isLabelBlock | 设置节点是否占满剩余空间,一般用于统一在各节点右侧添加元素(借助 flex 实现,暂时只支持 ie10+) | boolean | false | | - | -| isNodeBlock | 设置节点是否占满一行 | boolean \| Record | false | | - | -| animation | 是否开启展开收起动画 | boolean | true | | - | -| focusedKey | 当前获得焦点的子菜单或菜单项 key 值 | Key | - | | - | -| renderChildNodes | 渲染子节点

**签名**:
**参数**:
_nodes_: 所有的子节点
**返回值**:
子节点 | (nodes: NodeElement[]) => React.ReactNode | - | | - | -| labelRender | 渲染单个子节点

**签名**:
**参数**:
_node_: 节点数据
**返回值**:
返回节点 | (node: Record) => React.ReactNode | - | | 1.25 | -| immutable | 是否是不可变数据 | boolean | false | | 1.23 | -| useVirtual | 是否开启虚拟滚动 | boolean | false | | - | +| 参数 | 说明 | 类型 | 默认值 | 是否必填 | 支持版本 | +| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | -------- | -------- | +| children | 树节点 | React.ReactNode | - | | - | +| dataSource | 数据源,该属性优先级高于 children | TreeDataType[] | - | | - | +| showLine | 是否显示树的线 | boolean | false | | - | +| selectable | 是否支持选中节点 | boolean | false | | - | +| selectedKeys | (用于受控)当前选中节点 key 的数组 | string[] | - | | - | +| defaultSelectedKeys | (用于非受控)默认选中节点 key 的数组 | string[] | [] | | - | +| onSelect | 选中或取消选中节点时触发的回调函数

**签名**:
**参数**:
_selectedKeys_: 选中节点key的数组
_extra_: 额外参数
**返回值**:
空 | (
selectedKeys: string[],
extra: {
selectedNodes: Array\;
node: NodeInstance;
selected: boolean;
event: React.KeyboardEvent \| React.MouseEvent;
}
) => void | () =\> \{\} | | - | +| multiple | 是否支持多选 | boolean | false | | - | +| checkable | 是否支持勾选节点的复选框 | boolean | false | | - | +| checkedKeys | (用于受控)当前勾选复选框节点 key 的数组或 `{checked: Array, indeterminate: Array}` 的对象 | \| {
checked: string[];
indeterminate: string[];
}
\| string[] | - | | - | +| defaultCheckedKeys | (用于非受控)默认勾选复选框节点 key 的数组 | \| {
checked: string[];
indeterminate: string[];
}
\| string[] | [] | | - | +| checkStrictly | 勾选节点复选框是否完全受控(父子节点选中状态不再关联) | boolean | false | | - | +| checkedStrategy | 定义选中时回填的方式 | 'all' \| 'parent' \| 'child' | 'all' | | - | +| onCheck | 勾选或取消勾选复选框时触发的回调函数

**签名**:
**参数**:
_checkedKeys_: 勾选复选框节点key的数组
_extra_: 额外参数
**返回值**:
空 | (
checkedKeys: string[],
extra: {
checkedNodes: Array\;
checkedNodesPositions: Array\;
indeterminateKeys: string[];
node: NodeInstance;
checked: boolean;
key: Key;
}
) => void | () =\> \{\} | | - | +| expandedKeys | (用于受控)当前展开的节点 key 的数组 | string[] | - | | - | +| defaultExpandedKeys | (用于非受控)默认展开的节点 key 的数组 | string[] | [] | | - | +| defaultExpandAll | 是否默认展开所有节点 | boolean | false | | - | +| autoExpandParent | 是否自动展开父节点 | boolean | true | | - | +| onExpand | 展开或收起节点时触发的回调函数

**签名**:
**参数**:
_expandedKeys_: 展开节点key的数组
_extra_: 额外参数
**返回值**:
空 | (
expandedKeys: string[],
extra: {
node: NodeInstance;
expanded: boolean;
}
) => void | () =\> \{\} | | - | +| editable | 是否支持编辑节点内容 | boolean | false | | - | +| onEditFinish | 编辑节点内容完成时触发的回调函数

**签名**:
**参数**:
_key_: 编辑节点 key
_label_: 编辑节点完成时节点的文本
_node_: 当前编辑的节点
**返回值**:
空 | (key: Key, label: string, node: NodeInstance) => void | () =\> \{\} | | - | +| draggable | 是否支持拖拽节点 | boolean | false | | - | +| onDragStart | 开始拖拽节点时触发的回调函数

**签名**:
**参数**:
_info_: 额外参数
**返回值**:
空 | (info: {
event: React.MouseEvent;
node: NodeInstance;
expandedKeys: string[];
}) => void | () =\> \{\} | | - | +| onDragEnter | 拖拽节点进入目标节点时触发的回调函数

**签名**:
**参数**:
_info_: 额外参数
**返回值**:
空 | (info: {
event: React.MouseEvent;
node: NodeInstance;
expandedKeys: Array\;
}) => void | () =\> \{\} | | - | +| onDragOver | 拖拽节点在目标节点上移动的时候触发的回调函数

**签名**:
**参数**:
_info_: 额外参数
**返回值**:
空 | (info: { event: React.MouseEvent; node: NodeInstance }) => void | () =\> \{\} | | - | +| onDragLeave | 拖拽节点离开目标节点时触发的回调函数

**签名**:
**参数**:
_info_: 额外参数
**返回值**:
空 | (info: { event: React.MouseEvent; node: NodeInstance }) => void | () =\> \{\} | | - | +| onDragEnd | 拖拽节点拖拽结束时触发的回调函数

**签名**:
**参数**:
_info_: 额外参数
**返回值**:
空 | (info: { event: React.MouseEvent; node: NodeInstance }) => void | () =\> \{\} | | - | +| onDrop | 拖拽节点放入目标节点内或前后触发的回调函数

**签名**:
**参数**:
_info_: 额外参数
**返回值**:
空 | (info: {
event: React.MouseEvent;
node: NodeInstance;
dragNode: NodeInstance;
dragNodesKeys: Array\;
dropPosition: number;
}) => void | () =\> \{\} | | - | +| canDrop | 节点是否可被作为拖拽的目标节点

**签名**:
**参数**:
_info_: 额外参数
**返回值**:
是否可以被当作目标节点 | (info: {
event?: React.MouseEvent;
node: NodeInstance;
dragNode: NodeInstance;
dragNodesKeys: Array\;
dropPosition: number;
}) => boolean | () =\> \{\} | | - | +| loadData | 异步加载数据的函数

**签名**:
**参数**:
_node_: 被点击展开的节点 | (node: NodeInstance) => Promise\ | - | | - | +| filterTreeNode | 按需筛选高亮节点

**签名**:
**参数**:
_node_: 待筛选的节点
**返回值**:
是否被筛选中 | (node: NodeInstance) => boolean | - | | - | +| onRightClick | 右键点击节点时触发的回调函数

**签名**:
**参数**:
_info_: 额外参数
**返回值**:
空 | (info: { event: React.MouseEvent; node: NodeInstance }) => void | () =\> \{\} | | - | +| isLabelBlock | 设置节点是否占满剩余空间,一般用于统一在各节点右侧添加元素(借助 flex 实现,暂时只支持 ie10+) | boolean | false | | - | +| isNodeBlock | 设置节点是否占满一行 | boolean \| Record\ | false | | - | +| animation | 是否开启展开收起动画 | boolean | true | | - | +| focusedKey | 当前获得焦点的子菜单或菜单项 key 值 | Key | - | | - | +| renderChildNodes | 渲染子节点

**签名**:
**参数**:
_nodes_: 所有的子节点
**返回值**:
子节点 | (nodes: NodeElement[]) => React.ReactNode | - | | - | +| labelRender | 渲染单个子节点

**签名**:
**参数**:
_node_: 节点数据
**返回值**:
返回节点 | (node: Record\) => React.ReactNode | - | | 1.25 | +| immutable | 是否是不可变数据 | boolean | false | | 1.23 | +| useVirtual | 是否开启虚拟滚动 | boolean | false | | - | ### Tree.Node diff --git a/components/tree/types.ts b/components/tree/types.ts index f0820354b7..b6b55e7b4f 100644 --- a/components/tree/types.ts +++ b/components/tree/types.ts @@ -3,6 +3,7 @@ import type { CommonProps } from '../util'; import type { VirtualListProps } from '../virtual-list'; import type { Tree } from './view/tree'; import type { TreeNode } from './view/tree-node'; +import type { DataSourceItem } from '../select'; export type Key = string; export type KeyEntities = Record; @@ -252,6 +253,13 @@ export interface NodeProps extends CommonProps { * @skip */ style?: React.CSSProperties; + + /** + * value + * @en value + * @skip + */ + value?: DataSourceItem; } export type NodeElement = React.ReactElement; diff --git a/components/tree/view/util.ts b/components/tree/view/util.ts index b8e9caf3bf..ed83aef501 100644 --- a/components/tree/view/util.ts +++ b/components/tree/view/util.ts @@ -1,4 +1,5 @@ import type { DataNode, FieldDataNode, Key, KeyEntities, NodeElement } from '../types'; +import type { KeyEntities as TreeSelectKeyEntities } from '../../tree-select/types'; import TreeNode from './tree-node'; // export function normalizeToArray(keys: undefined | null): [];