feat(cascader): support onSearch callback (#28011)

* feat(cascader): support onSearch callback

* fix(cascader): move onSearch into showSearch

* fix(cascader): rename onSearch to onChange

* fix(cascader): rename onSearch to onChange

* Update index.zh-CN.md

* Update index.en-US.md

Co-authored-by: rgbhuang <rgbhuang@tencent.com>
Co-authored-by: 偏右 <afc163@gmail.com>
This commit is contained in:
GSBL 2020-11-30 13:21:15 +08:00 committed by GitHub
parent 33b96d3461
commit cff73d1f08
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 321 additions and 3 deletions

View File

@ -190,6 +190,134 @@ exports[`renders ./components/cascader/demo/custom-render.md correctly 1`] = `
</span>
`;
exports[`renders ./components/cascader/demo/custom-search.md correctly 1`] = `
<div
class="ant-row"
style="margin-left:-6px;margin-right:-6px;margin-top:-9px;margin-bottom:9px"
>
<div
class="ant-col ant-col-24"
style="padding-left:6px;padding-right:6px;padding-top:9px;padding-bottom:9px"
>
<span
class="ant-cascader-picker ant-cascader-picker-show-search"
tabindex="0"
>
<span
class="ant-cascader-picker-label"
/>
<input
autocomplete="off"
class="ant-input ant-cascader-input "
placeholder="Please select"
tabindex="-1"
type="text"
value=""
/>
<span
aria-label="down"
class="anticon anticon-down ant-cascader-picker-arrow"
role="img"
>
<svg
aria-hidden="true"
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
</span>
</div>
<div
class="ant-col ant-col-24"
style="padding-left:6px;padding-right:6px;padding-top:9px;padding-bottom:9px"
>
<span
class="ant-cascader-picker ant-cascader-picker-show-search"
tabindex="0"
>
<span
class="ant-cascader-picker-label"
/>
<input
autocomplete="off"
class="ant-input ant-cascader-input "
placeholder="Please select"
tabindex="-1"
type="text"
value=""
/>
<span
aria-label="down"
class="anticon anticon-down ant-cascader-picker-arrow"
role="img"
>
<svg
aria-hidden="true"
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
</span>
</div>
<div
class="ant-col ant-col-24"
style="padding-left:6px;padding-right:6px;padding-top:9px;padding-bottom:9px"
>
<span
class="ant-cascader-picker ant-cascader-picker-show-search"
tabindex="0"
>
<span
class="ant-cascader-picker-label"
/>
<input
autocomplete="off"
class="ant-input ant-cascader-input "
placeholder="Please select"
tabindex="-1"
type="text"
value=""
/>
<span
aria-label="down"
class="anticon anticon-down ant-cascader-picker-arrow"
role="img"
>
<svg
aria-hidden="true"
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
</span>
</div>
</div>
`;
exports[`renders ./components/cascader/demo/custom-trigger.md correctly 1`] = `
<span>
Unselect 

View File

@ -542,4 +542,11 @@ describe('Cascader', () => {
.simulate('click');
expect(onChange).toHaveBeenCalledWith(['Zhejiang', 'Hangzhou', 'West Lake'], expect.anything());
});
it('showSearch.onChange should work correctly', () => {
const onChange = jest.fn();
const wrapper = mount(<Cascader options={options} showSearch={{ onChange }} />);
wrapper.find('input').simulate('change', { target: { value: '123' } });
expect(onChange).toHaveBeenCalledWith('123');
});
});

View File

@ -0,0 +1,166 @@
---
order: 12
title:
zh-CN: 自定义搜索
en-US: Custom Search
---
## zh-CN
可以直接监听`showSearch.onChange`事件,自己实现搜索逻辑
## en-US
You can directly listen `showSearch.onChange` events and implement search logic yourself
```jsx
import { Cascader, Row, Col } from 'antd';
import { useState } from 'react';
const CustomSearchCascaders = () => {
const originOptions = [
{
value: 'zhejiang',
label: 'Zhejiang',
children: [
{
value: 'hangzhou',
label: 'Hangzhou',
children: [
{
value: 'xihu',
label: 'West Lake',
},
{
value: 'xiasha',
label: 'Xia Sha',
disabled: true,
},
],
},
],
},
{
value: 'jiangsu',
label: 'Jiangsu',
children: [
{
value: 'nanjing',
label: 'Nanjing',
children: [
{
value: 'zhonghuamen',
label: 'Zhong Hua men',
},
],
},
],
},
];
const [options1, setOptions1] = useState(originOptions);
const [options2, setOptions2] = useState(originOptions);
const [options3, setOptions3] = useState(originOptions);
function flattenTree(opts, props, ancestor = []) {
let flattenOptions = [];
opts.forEach(option => {
const path = ancestor.concat(option);
if (props.changeOnSelect || !option.children || !option.children.length) {
flattenOptions.push(path);
}
if (option.children) {
flattenOptions = flattenOptions.concat(flattenTree(option.children, props, path));
}
});
return flattenOptions;
}
function onChange(value, selectedOptions) {
console.log(value, selectedOptions);
}
function filter(inputValue, path) {
return path.some(option => option.label.toLowerCase().indexOf(inputValue.toLowerCase()) > -1);
}
function highlightKeyword(str, keyword) {
return str.split(keyword).map((node, index) =>
index === 0
? node
: [
<span style={{ color: 'blue' }} key="seperator">
{keyword}
</span>,
node,
],
);
}
const flattenOptions = flattenTree(originOptions, { changeOnSelect: false });
function search(val) {
const filtered = [];
flattenOptions.forEach(path => {
const match = filter(val, path);
if (match) {
filtered.push(path);
}
});
const result = filtered.map(path => ({
__IS_FILTERED_OPTION: true,
path,
value: path.map(o => o.value),
label: path.map((option, index) => {
const { label } = option;
const node = label.indexOf(val) > -1 ? highlightKeyword(label, val) : label;
return index === 0 ? node : [' / ', node];
}),
disabled: path.some(o => !!o.disabled),
isEmptyNode: true,
}));
return result;
}
function onChange1(val) {
const result = search(val);
setOptions1(result);
}
function onChange2(val) {
const result = search(val);
setOptions2(result);
}
function onChange3(val) {
const result = search(val);
setOptions3(result);
}
return (
<Row gutter={[12, 18]}>
<Col span={24}>
<Cascader
options={options1}
onChange={onChange}
placeholder="Please select"
showSearch={{
onChange: onChange1,
}}
/>
</Col>
<Col span={24}>
<Cascader
options={options2}
onChange={onChange}
placeholder="Please select"
showSearch={{
onChange: onChange2,
}}
/>
</Col>
<Col span={24}>
<Cascader
options={options3}
onChange={onChange}
placeholder="Please select"
showSearch={{
onChange: onChange3,
}}
/>
</Col>
</Row>
);
};
ReactDOM.render(<CustomSearchCascaders />, mountNode);
```

View File

@ -58,6 +58,7 @@ Cascade selection box.
| matchInputWidth | Whether the width of list matches input, ([how it looks](https://github.com/ant-design/ant-design/issues/25779)) | boolean | true | |
| render | Used to render filtered options | function(inputValue, path): ReactNode | - | |
| sort | Used to sort filtered options | function(a, b, inputValue) | - | |
| onChange | A callback function, can be executed when the search input changes | function(value: string) | - | 4.9.0 |
### Option

View File

@ -64,6 +64,7 @@ export interface ShowSearchType {
) => number;
matchInputWidth?: boolean;
limit?: number | false;
onChange?: (val: string) => void;
}
export interface CascaderProps {
@ -254,7 +255,11 @@ class Cascader extends React.Component<CascaderProps, CascaderState> {
if ('popupVisible' in nextProps) {
newState.popupVisible = nextProps.popupVisible;
}
if (nextProps.showSearch && prevProps.options !== nextProps.options) {
if (
nextProps.showSearch &&
(nextProps.showSearch as ShowSearchType).onChange === undefined &&
prevProps.options !== nextProps.options
) {
newState.flattenOptions = flattenTree(nextProps.options, nextProps);
}
@ -278,7 +283,10 @@ class Cascader extends React.Component<CascaderProps, CascaderState> {
inputValue: '',
inputFocused: false,
popupVisible: props.popupVisible,
flattenOptions: props.showSearch ? flattenTree(props.options, props) : undefined,
flattenOptions:
props.showSearch && (props.showSearch as ShowSearchType).onChange === undefined
? flattenTree(props.options, props)
: undefined,
prevProps: props,
};
}
@ -370,9 +378,16 @@ class Cascader extends React.Component<CascaderProps, CascaderState> {
handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { popupVisible } = this.state;
const inputValue = e.target.value;
const { showSearch } = this.props;
if (!popupVisible) {
this.handlePopupVisibleChange(true);
}
if (showSearch) {
const { onChange } = showSearch as ShowSearchType;
if (onChange) {
onChange(inputValue);
}
}
this.setState({ inputValue });
};
@ -564,7 +579,7 @@ class Cascader extends React.Component<CascaderProps, CascaderState> {
let { options } = props;
const names: FilledFieldNamesType = getFilledFieldNames(this.props);
if (options && options.length > 0) {
if (state.inputValue) {
if (state.inputValue && (showSearch as ShowSearchType).onChange === undefined) {
options = this.generateFilteredOptions(prefixCls, renderEmpty);
}
} else {

View File

@ -61,6 +61,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/UdS8y8xyZ/Cascader.svg
| matchInputWidth | 搜索结果列表是否与输入框同宽([效果](https://github.com/ant-design/ant-design/issues/25779) | boolean | true | |
| render | 用于渲染 filter 后的选项 | function(inputValue, path): ReactNode | - | |
| sort | 用于排序 filter 后的选项 | function(a, b, inputValue) | - | |
| onChange | 文本框值变化时的回调 | function(value: string) | - | 4.9.0 |
### Option