diff --git a/src/cascader/Cascader.tsx b/src/cascader/Cascader.tsx index 6230004e..329f42e3 100644 --- a/src/cascader/Cascader.tsx +++ b/src/cascader/Cascader.tsx @@ -8,7 +8,7 @@ import { Popup } from 'tdesign-mobile-react/popup'; import { Radio, RadioGroup } from 'tdesign-mobile-react/radio'; import Tabs from 'tdesign-mobile-react/tabs'; import TabContext from 'tdesign-mobile-react/tabs/context'; -import { StyledProps, TNode, TreeKeysType, TreeOptionData } from '../common'; +import { StyledProps, TNode, TreeOptionData } from '../common'; import { usePrefixClass } from '../hooks/useClass'; import useDefaultProps from '../hooks/useDefaultProps'; import { cascaderDefaultProps } from './defaultProps'; @@ -41,6 +41,8 @@ const Cascader = forwardRef((props) => { subTitles, options: inputOptions, keys, + checkStrictly, + closeBtn, onChange, onClose, onPick, @@ -53,8 +55,7 @@ const Cascader = forwardRef((props) => { // 根据 inputOptions 和 key 重新构建 options const options = useMemo(() => { - // TODO: keys 的类型 不对 - const { label = 'label', value = 'value', children = 'children' } = (keys || {}) as TreeKeysType; + const { label = 'label', value = 'value', children = 'children' } = keys || {}; const convert = (options: TreeOptionData[]) => options.map((item) => ({ @@ -115,9 +116,16 @@ const Cascader = forwardRef((props) => { }, [optionsList, internalSelectedValues, placeholder]); const selectedValuesByInterValue = useMemo(() => { - // 最后一级的value为匹配时,返回整个链路上的value + /** + * checkStrictly true 从外到内 匹配上就挺 返回整个链路上的value + * checkStrictly false 最后一级的 value 匹配时,返回整个链路上的value + */ const findValues = (options: TreeOptionData[]): CascaderProps['value'][] => { for (const item of options) { + if (checkStrictly && item.value === internalValue) { + return [item.value]; + } + const isLast = !(Array.isArray(item.children) && item.children.length); if (isLast) { if (item.value === internalValue) { @@ -134,7 +142,7 @@ const Cascader = forwardRef((props) => { }; return findValues(options); - }, [options, internalValue]); + }, [options, internalValue, checkStrictly]); // 当 selectedValuesByInterValue 深度变化 的时候再控制 selectedValues useDeepCompareEffect(() => { @@ -149,6 +157,23 @@ const Cascader = forwardRef((props) => { } }, [optionsList, stepIndex]); + // 结束了 + const onFinish = useCallback( + (selectedValues: CascaderProps['value'][]) => { + const selectedOptions = [...optionsList].slice(0, selectedValues.length).map((options, index) => { + const target = options.find((item) => item.value === selectedValues[index]); + const { label = 'label', value = 'value' } = keys || {}; + return { + [label]: target?.label || '', + [value]: target?.value || '', + }; + }); + setInternalValue(last(selectedValues), selectedOptions as any); + onClose?.('finish'); + }, + [onClose, optionsList, setInternalValue, keys], + ); + return ( ((props) => {
{ + if (checkStrictly) { + onFinish(internalSelectedValues); + return; + } + setInternalVisible(false); onClose?.('close-btn'); }} > - + {closeBtn === true ? : closeBtn}
{labelList.length ? ( @@ -248,23 +278,7 @@ const Cascader = forwardRef((props) => { return; } - // 结束了 - const selectedOptions = optionsList.map((options, index) => { - const target = options.find((item) => item.value === selectedValues[index]); - const { - label = 'label', - value = 'value', - children = 'children', - } = (keys || {}) as TreeKeysType; - return { - [label]: target?.label || '', - [value]: target?.value || '', - [children]: target?.children, - }; - }); - // TODO onChange 的 selectedOptions 类型不对 - setInternalValue(value, selectedOptions as any); - onClose?.('finish'); + onFinish(selectedValues); }} > {curOptions.map((item) => ( diff --git a/src/cascader/_example/check-strictly.tsx b/src/cascader/_example/check-strictly.tsx new file mode 100644 index 00000000..745c402d --- /dev/null +++ b/src/cascader/_example/check-strictly.tsx @@ -0,0 +1,101 @@ +import React, { useState } from 'react'; + +import { Cascader, Cell } from 'tdesign-mobile-react'; +import './style/index.less'; + +const data = { + areaList: [ + { + label: '北京市', + value: '110000', + children: [ + { + value: '110100', + label: '北京市', + children: [ + { value: '110101', label: '东城区' }, + { value: '110102', label: '西城区' }, + { value: '110105', label: '朝阳区' }, + { value: '110106', label: '丰台区' }, + { value: '110107', label: '石景山区' }, + { value: '110108', label: '海淀区' }, + { value: '110109', label: '门头沟区' }, + { value: '110111', label: '房山区' }, + { value: '110112', label: '通州区' }, + { value: '110113', label: '顺义区' }, + { value: '110114', label: '昌平区' }, + { value: '110115', label: '大兴区' }, + { value: '110116', label: '怀柔区' }, + { value: '110117', label: '平谷区' }, + { value: '110118', label: '密云区' }, + { value: '110119', label: '延庆区' }, + ], + }, + ], + }, + { + label: '天津市', + value: '120000', + children: [ + { + value: '120100', + label: '天津市', + children: [ + { value: '120101', label: '和平区' }, + { value: '120102', label: '河东区' }, + { value: '120103', label: '河西区' }, + { value: '120104', label: '南开区' }, + { value: '120105', label: '河北区' }, + { value: '120106', label: '红桥区' }, + { value: '120110', label: '东丽区' }, + { value: '120111', label: '西青区' }, + { value: '120112', label: '津南区' }, + { value: '120113', label: '北辰区' }, + { value: '120114', label: '武清区' }, + { value: '120115', label: '宝坻区' }, + { value: '120116', label: '滨海新区' }, + { value: '120117', label: '宁河区' }, + { value: '120118', label: '静海区' }, + { value: '120119', label: '蓟州区' }, + ], + }, + ], + }, + ], +}; + +export default function CheckStrictlyDemo() { + const [visible, setVisible] = useState(false); + + const [note, setNote] = useState('请选择地址'); + + const [value, setValue] = useState(); + + return ( + <> + { + setVisible(true); + }} + /> + 确定} + options={data.areaList} + onChange={(value, selectedOptions) => { + setNote((selectedOptions as any).map((item) => item.label).join('/') || ''); + setValue(value); + }} + onClose={() => { + setVisible(false); + }} + /> + + ); +} diff --git a/src/cascader/_example/index.tsx b/src/cascader/_example/index.tsx index 69b8034a..fb6f3391 100644 --- a/src/cascader/_example/index.tsx +++ b/src/cascader/_example/index.tsx @@ -2,8 +2,8 @@ import React from 'react'; import TDemoBlock from '../../../site/mobile/components/DemoBlock'; import TDemoHeader from '../../../site/mobile/components/DemoHeader'; import BaseDemo from './base'; +import CheckStrictlyDemo from './check-strictly'; import KeysDemo from './keys'; -import LazyDemo from './lazy'; import ThemeTabDemo from './theme-tab'; import WithTitleDemo from './with-title'; import WithValueDemo from './with-value'; @@ -29,8 +29,8 @@ export default function CascaderDemo() { - - + +
); diff --git a/src/cascader/_example/lazy.tsx b/src/cascader/_example/lazy.tsx deleted file mode 100644 index 3860be40..00000000 --- a/src/cascader/_example/lazy.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import cloneDeep from 'lodash/cloneDeep'; -import React, { useState } from 'react'; -import { Icon } from 'tdesign-icons-react'; -import { Cascader, Cell, Toast } from 'tdesign-mobile-react'; -import './style/index.less'; - -export default function LazyDemo() { - const [visible, setVisible] = useState(false); - - const [data, setData] = useState([ - { - label: '深圳市', - value: '440300', - children: [], - }, - ]); - - const [note, setNote] = useState('请选择地址'); - - const [value, setValue] = useState(); - - return ( - <> - { - setVisible(true); - }} - /> - { - setNote((selectedOptions as any).map((item) => item.label).join('/') || ''); - setValue(value); - }} - onClose={() => { - setVisible(false); - }} - onPick={(value) => { - if (value !== '440300') { - return; - } - const newData = cloneDeep(data); - const target = newData.find((item) => item.value === value); - if (!target || target.children.length) { - return; - } - const toast = Toast({ - icon: , - message: '加载中...', - direction: 'column', - placement: 'middle', - duration: 10000, - preventScrollThrough: true, - }); - - setTimeout(() => { - target.children = [ - { value: '440304', label: '福田区' }, - { value: '440303', label: '罗湖区' }, - { value: '440305', label: '南山区' }, - { value: '440306', label: '宝安区' }, - { value: '440307', label: '龙岗区' }, - { value: '440308', label: '盐田区' }, - { value: '440309', label: '龙华区' }, - { value: '440310', label: '坪山区' }, - { value: '440311', label: '光明区' }, - ]; - setData(newData); - toast.destroy(); - }, 1000); - }} - /> - - ); -} diff --git a/src/cascader/cascader.en-US.md b/src/cascader/cascader.en-US.md index 360a3fad..6e2d4135 100644 --- a/src/cascader/cascader.en-US.md +++ b/src/cascader/cascader.en-US.md @@ -2,15 +2,15 @@ ## API - ### Cascader Props name | type | default | description | required -- | -- | -- | -- | -- className | String | - | className of component | N style | Object | - | CSS(Cascading Style Sheets),Typescript:`React.CSSProperties` | N +checkStrictly | Boolean | false | \- | N closeBtn | TNode | true | Typescript:`boolean \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N -keys | Object | - | Typescript:`KeysType`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +keys | Object | - | Typescript:`CascaderKeysType` `type CascaderKeysType = TreeKeysType`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts)。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/cascader/type.ts) | N lazy | Boolean | false | \- | N loadCompleted | Boolean | false | \- | N options | Array | [] | Typescript:`Array` | N @@ -21,6 +21,6 @@ title | TNode | - | Typescript:`string \| TNode`。[see more ts definition](ht value | String / Number | - | \- | N defaultValue | String / Number | - | uncontrolled property | N visible | Boolean | false | \- | N -onChange | Function | | Typescript:`(value: string \| number, selectedOptions: string[]) => void`
| N +onChange | Function | | Typescript:`(value: string \| number, selectedOptions: CascaderOption[]) => void`
| N onClose | Function | | Typescript:`(trigger: TriggerSource) => void`
[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/cascader/type.ts)。
`type TriggerSource = 'overlay' \| 'close-btn' \| 'finish'`
| N onPick | Function | | Typescript:`(value: string \| number, index: number) => void`
| N diff --git a/src/cascader/cascader.md b/src/cascader/cascader.md index e66c7df2..66210594 100644 --- a/src/cascader/cascader.md +++ b/src/cascader/cascader.md @@ -36,22 +36,22 @@ toc: false ::: demo _example/with-title ::: -### 异步加载 +### 选择任意一项 -::: demo _example/lazy +::: demo _example/check-strictly ::: ## API - ### Cascader Props 名称 | 类型 | 默认值 | 描述 | 必传 -- | -- | -- | -- | -- className | String | - | 类名 | N style | Object | - | 样式,TS 类型:`React.CSSProperties` | N +checkStrictly | Boolean | false | 父子节点选中状态不再关联,可各自选中或取消 | N closeBtn | TNode | true | 关闭按钮。TS 类型:`boolean \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N -keys | Object | - | 用来定义 value / label 在 `options` 中对应的字段别名。TS 类型:`KeysType`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N +keys | Object | - | 用来定义 value / label 在 `options` 中对应的字段别名。TS 类型:`CascaderKeysType` `type CascaderKeysType = TreeKeysType`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts)。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/cascader/type.ts) | N lazy | Boolean | false | 是否异步加载 | N loadCompleted | Boolean | false | 是否完成异步加载 | N options | Array | [] | 可选项数据源。TS 类型:`Array` | N @@ -62,6 +62,6 @@ title | TNode | - | 标题。TS 类型:`string \| TNode`。[通用类型定义 value | String / Number | - | 选项值 | N defaultValue | String / Number | - | 选项值。非受控属性 | N visible | Boolean | false | 是否展示 | N -onChange | Function | | TS 类型:`(value: string \| number, selectedOptions: string[]) => void`
值发生变更时触发 | N +onChange | Function | | TS 类型:`(value: string \| number, selectedOptions: CascaderOption[]) => void`
值发生变更时触发 | N onClose | Function | | TS 类型:`(trigger: TriggerSource) => void`
关闭时触发。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/cascader/type.ts)。
`type TriggerSource = 'overlay' \| 'close-btn' \| 'finish'`
| N onPick | Function | | TS 类型:`(value: string \| number, index: number) => void`
选择后触发 | N diff --git a/src/cascader/type.ts b/src/cascader/type.ts index 2773f144..8c7d12b8 100644 --- a/src/cascader/type.ts +++ b/src/cascader/type.ts @@ -4,9 +4,14 @@ * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC * */ -import { TNode, TreeOptionData, KeysType } from '../common'; +import { TNode, TreeKeysType, TreeOptionData } from '../common'; export interface TdCascaderProps { + /** + * 父子节点选中状态不再关联,可各自选中或取消 + * @default false + */ + checkStrictly?: boolean; /** * 关闭按钮 * @default true @@ -15,7 +20,7 @@ export interface TdCascaderProps void; + onChange?: (value: string | number, selectedOptions: CascaderOption[]) => void; /** * 关闭时触发 */ @@ -77,4 +82,6 @@ export interface TdCascaderProps void; } +export type CascaderKeysType = TreeKeysType; + export type TriggerSource = 'overlay' | 'close-btn' | 'finish';