Skip to content

Commit ce01687

Browse files
authoredMar 25, 2022
feat(Cascader): 添加搜索功能 (#715)
1 parent 1feba3a commit ce01687

File tree

2 files changed

+179
-29
lines changed

2 files changed

+179
-29
lines changed
 

‎packages/react-cascader/README.md

+67-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,71 @@ const Demo = () => {
5757

5858
return (
5959
<div style={{ width: 200 }}>
60-
<Cascader allowClear={true} placeholder="请选择" value={[1, 4, 7]} option={options} onChange={(value, seleteds) => console.log(value, seleteds)} />
60+
< Cascader
61+
allowClear={true}
62+
placeholder="请选择"
63+
value={[1, 4, 7]}
64+
option={options}
65+
onChange={(value, seleteds) => console.log(value, seleteds)}
66+
/>
67+
</div>
68+
)
69+
};
70+
ReactDOM.render(<Demo />, _mount_);
71+
```
72+
73+
## 搜索选项
74+
75+
<!--rehype:bgWhite=true&codeSandbox=true&codePen=true-->
76+
```jsx
77+
import ReactDOM from 'react-dom';
78+
import { Cascader } from 'uiw';
79+
80+
const Demo = () => {
81+
82+
const options = [
83+
{
84+
label: '上海市',
85+
value: 1,
86+
children: [
87+
{
88+
label: '徐汇区',
89+
value: 4,
90+
children: [
91+
{ label: '半淞园路街道', value: 6 },
92+
{ label: '南京东路街道', value: 7 },
93+
{ label: '外滩街道', value: 8 },
94+
]
95+
},
96+
]
97+
},
98+
{
99+
label: '北京市',
100+
value: 9,
101+
children: [
102+
{
103+
label: '崇文区',
104+
value: 12,
105+
children: [
106+
{ label: '东花市街道', value: 13 },
107+
{ label: '体育馆路街道', value: 14 },
108+
{ label: '前门街道', value: 15 },
109+
]
110+
},
111+
]
112+
},
113+
];
114+
115+
return (
116+
<div style={{ width: 200 }}>
117+
<Cascader
118+
allowClear={true}
119+
placeholder="请选择"
120+
value={[1, 4, 7]}
121+
option={options}
122+
onChange={(value, seleteds) => console.log(value, seleteds)}
123+
onSearch={(text)=> console.log('text', text)}
124+
/>
61125
</div>
62126
)
63127
};
@@ -138,6 +202,7 @@ const options = [
138202
children: (
139203
<Cascader
140204
allowClear={true}
205+
onSearch={true}
141206
placeholder="请选择"
142207
option={options}
143208
onChange={(value, seleteds) => console.log(value, seleteds)}
@@ -183,3 +248,4 @@ ReactDOM.render(<Demo />, _mount_);
183248
| option | 选项菜单 | { value: String \| Number, label: React.ReactNode, children: Array<String \| Number>} | - | - |
184249
| value | 指定当前选中的条目,多选时为一个数组 | String[] \| Number[] | - | - |
185250
| onChange | 选中选项调用此函数 | function( isSeleted, value, selectedOptions) | - | - |
251+
| onSearch | 开启搜索选项 | functionon(searchText) \| Boolean | - | v4.16.1 |

‎packages/react-cascader/src/index.tsx

+112-28
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,22 @@ import './style/index.less';
88

99
type ValueType = Array<string | number>;
1010
type OptionType = { value: string | number; label: React.ReactNode; children?: Array<OptionType> };
11+
type SearchOptionType = { label: string; options: Array<OptionType> };
1112

1213
export interface CascaderProps extends IProps, DropdownProps {
1314
option?: Array<OptionType>;
1415
value?: ValueType;
1516
onChange?: (isSeleted: boolean, value: ValueType, selectedOptions: Array<OptionType>) => void;
17+
onSearch?: boolean | ((searchText: string) => void);
1618
allowClear?: boolean;
1719
placeholder?: string;
18-
isOpen?: boolean;
1920
}
2021

2122
function Cascader(props: CascaderProps) {
2223
const {
2324
value,
2425
onChange,
26+
onSearch,
2527

2628
allowClear,
2729
placeholder,
@@ -36,16 +38,48 @@ function Cascader(props: CascaderProps) {
3638
const [innerIsOpen, setInnerIsOpen] = useState(false);
3739
const [selectedValue, setSelectedValue] = useState<Array<OptionType>>([]);
3840
const [selectIconType, setSelectIconType] = useState('');
41+
const [searchText, setSearchText] = useState<string>('');
42+
const [searchOn, setSearchOn] = useState(false);
43+
const [inputValue, setInputValue] = useState('');
44+
const [searchOption, setSearchOption] = useState<Array<SearchOptionType>>();
3945

4046
useEffect(() => {
41-
const valueTemp: Array<OptionType> = [];
42-
let optChildren = option;
43-
value?.map((item) => {
44-
const findOpt = optChildren.find((opt) => opt.value === item);
45-
optChildren = findOpt?.children || [];
46-
valueTemp.push({ label: item, value: item, ...findOpt });
47+
if (onSearch) {
48+
const tempOptions: Array<SearchOptionType> = [];
49+
iteratorOption(option, (opt) => {
50+
const label = opt.map((m) => m.label).join(' / ');
51+
tempOptions.push({ label, options: opt });
52+
});
53+
setSearchOption(tempOptions);
54+
}
55+
}, [onSearch]);
56+
57+
const iteratorOption = (
58+
options: Array<OptionType>,
59+
cb: (opt: Array<OptionType>) => void,
60+
opts: Array<OptionType> = [],
61+
) => {
62+
options.map((opt) => {
63+
const optsTemp = [...opts, opt];
64+
if (opt.children) {
65+
iteratorOption(opt.children, cb, optsTemp);
66+
} else {
67+
cb?.(optsTemp);
68+
}
4769
});
48-
setSelectedValue(valueTemp);
70+
};
71+
72+
useEffect(() => {
73+
if (value) {
74+
const valueTemp: Array<OptionType> = [];
75+
let optChildren = option;
76+
value?.map((item) => {
77+
const findOpt = optChildren.find((opt) => opt.value === item);
78+
optChildren = findOpt?.children || [];
79+
valueTemp.push({ label: item, value: item, ...findOpt });
80+
});
81+
setSelectedValue(valueTemp);
82+
}
4983
}, [value]);
5084

5185
function onVisibleChange(isOpen: boolean) {
@@ -62,6 +96,12 @@ function Cascader(props: CascaderProps) {
6296
setSelectIconType(selectIconType);
6397
}
6498

99+
const searchItemClick = (options: Array<OptionType>) => {
100+
setSearchText('');
101+
setInnerIsOpen(false);
102+
handelChange(false, options);
103+
};
104+
65105
const handleItemClick = (optionsItem: OptionType, level: number) => {
66106
selectedValue.splice(level, selectedValue.length - level, optionsItem);
67107

@@ -76,10 +116,21 @@ function Cascader(props: CascaderProps) {
76116

77117
const onClear = (e: React.MouseEvent<any, MouseEvent>) => {
78118
e.stopPropagation();
79-
console.log(123);
80119
handelChange(false, []);
81120
};
82121

122+
const handelSearch = (searchText: string) => {
123+
setSearchText(searchText);
124+
};
125+
126+
const inputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
127+
if (!innerIsOpen) {
128+
setInnerIsOpen(!innerIsOpen);
129+
}
130+
const value = e.target.value;
131+
onSearch && handelSearch(value);
132+
};
133+
83134
const widths = (style?.width as number) * 0.5 || undefined;
84135

85136
const OptionIter = (option: Array<OptionType>, level: number = 0) => {
@@ -116,9 +167,10 @@ function Cascader(props: CascaderProps) {
116167
) as JSX.Element;
117168
};
118169

119-
const inputValue = useMemo(() => {
120-
return selectedValue.map((opt) => opt.label).join(' / ');
121-
}, [selectedValue.length]);
170+
useEffect(() => {
171+
const inputValue = selectedValue.map((opt) => opt.label).join(' / ');
172+
setInputValue(inputValue);
173+
}, [selectedValue]);
122174

123175
return (
124176
<Dropdown
@@ -130,28 +182,60 @@ function Cascader(props: CascaderProps) {
130182
onVisibleChange={onVisibleChange}
131183
isOpen={innerIsOpen}
132184
menu={
133-
<div style={{ display: 'flex' }}>
134-
{new Array(selectedValue.length + 1)
135-
.fill(0)
136-
.map((_, index) => {
137-
const options = index ? selectedValue[index - 1]?.children! : option;
138-
return OptionIter(options, index);
139-
})
140-
.filter((m) => !!m)}
141-
</div>
185+
!searchText ? (
186+
<div style={{ display: 'flex' }}>
187+
{new Array(selectedValue.length + 1)
188+
.fill(0)
189+
.map((_, index) => {
190+
const options = index ? selectedValue[index - 1]?.children! : option;
191+
return OptionIter(options, index);
192+
})
193+
.filter((m) => !!m)}
194+
</div>
195+
) : (
196+
<Menu
197+
bordered
198+
style={{
199+
minHeight: 25,
200+
minWidth: style?.width,
201+
overflowY: 'scroll',
202+
width: style?.width,
203+
}}
204+
>
205+
{!searchOption || searchOption.length === 0 ? (
206+
<div style={{ color: '#c7c7c7', fontSize: 12 }}>{'没有数据'}</div>
207+
) : (
208+
searchOption
209+
.filter((opt) => opt.label.includes(searchText.trim()))
210+
.map((opt, index) => {
211+
return (
212+
<Menu.Item
213+
key={index}
214+
text={opt.label}
215+
onClick={() => searchItemClick(opt.options)} //
216+
/>
217+
);
218+
})
219+
)}
220+
</Menu>
221+
)
142222
}
143223
>
144224
<span onMouseLeave={() => renderSelectIcon('leave')} onMouseOver={() => renderSelectIcon('enter')}>
145225
<Input
146-
value={inputValue}
147-
onChange={() => {}}
148-
placeholder={placeholder}
226+
value={searchOn ? searchText : inputValue}
227+
onChange={inputChange}
228+
placeholder={searchOn ? inputValue : placeholder}
149229
style={{ width: style?.width }}
150-
readOnly
230+
onFocus={() => onSearch && setSearchOn(true)}
231+
onBlur={() => onSearch && setSearchOn(false)}
232+
readOnly={!onSearch}
151233
addonAfter={
152-
selectIconType === 'close' && (
153-
<Icon type={`${selectIconType}`} onClick={onClear} className={`${prefixCls}-close`} />
154-
)
234+
<span style={{ width: 'auto' }}>
235+
{selectIconType === 'close' && (
236+
<Icon type={selectIconType} onClick={onClear} className={`${prefixCls}-close`} />
237+
)}
238+
</span>
155239
}
156240
/>
157241
</span>

0 commit comments

Comments
 (0)
Please sign in to comment.