Skip to content

Commit 3a49503

Browse files
committed
feat: cascader support limit
1 parent 843b074 commit 3a49503

File tree

4 files changed

+139
-29
lines changed

4 files changed

+139
-29
lines changed

components/cascader/__tests__/index.test.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import KeyCode from '../../_util/KeyCode'
44
import Cascader from '..'
55
import focusTest from '../../../tests/shared/focusTest'
66

7+
function $$ (className) {
8+
return document.body.querySelectorAll(className)
9+
}
710
const options = [{
811
value: 'zhejiang',
912
label: 'Zhejiang',
@@ -28,6 +31,10 @@ const options = [{
2831
}],
2932
}]
3033

34+
function filter (inputValue, path) {
35+
return path.some(option => option.label.toLowerCase().indexOf(inputValue.toLowerCase()) > -1)
36+
}
37+
3138
describe('Cascader', () => {
3239
focusTest(Cascader)
3340

@@ -187,4 +194,60 @@ describe('Cascader', () => {
187194
expect(wrapper.vm.inputValue).toBe('123')
188195
})
189196
})
197+
198+
describe('limit filtered item count', () => {
199+
beforeEach(() => {
200+
document.body.outerHTML = ''
201+
})
202+
203+
afterEach(() => {
204+
document.body.outerHTML = ''
205+
})
206+
207+
it('limit with positive number', async () => {
208+
const wrapper = mount(Cascader, {
209+
propsData: { options, showSearch: { filter, limit: 1 }},
210+
sync: false,
211+
attachToDocument: true,
212+
})
213+
wrapper.find('input').trigger('click')
214+
wrapper.find('input').element.value = 'a'
215+
wrapper.find('input').trigger('input')
216+
await asyncExpect(() => {
217+
expect($$('.ant-cascader-menu-item').length).toBe(1)
218+
}, 0)
219+
})
220+
221+
it('not limit', async () => {
222+
const wrapper = mount(Cascader, {
223+
propsData: { options, showSearch: { filter, limit: false }},
224+
sync: false,
225+
attachToDocument: true,
226+
})
227+
wrapper.find('input').trigger('click')
228+
wrapper.find('input').element.value = 'a'
229+
wrapper.find('input').trigger('input')
230+
await asyncExpect(() => {
231+
expect($$('.ant-cascader-menu-item').length).toBe(2)
232+
}, 0)
233+
})
234+
235+
it('negative limit', async () => {
236+
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
237+
const wrapper = mount(Cascader, {
238+
propsData: { options, showSearch: { filter, limit: -1 }},
239+
sync: false,
240+
attachToDocument: true,
241+
})
242+
wrapper.find('input').trigger('click')
243+
wrapper.find('input').element.value = 'a'
244+
wrapper.find('input').trigger('input')
245+
await asyncExpect(() => {
246+
expect($$('.ant-cascader-menu-item').length).toBe(2)
247+
}, 0)
248+
expect(errorSpy).toBeCalledWith(
249+
"Warning: 'limit' of showSearch in Cascader should be positive number or false.",
250+
)
251+
})
252+
})
190253
})

components/cascader/index.en-US.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ Fields in `showSearch`:
3434
| Property | Description | Type | Default |
3535
| -------- | ----------- | ---- | ------- |
3636
| filter | The function will receive two arguments, inputValue and option, if the function returns true, the option will be included in the filtered set; Otherwise, it will be excluded. | `function(inputValue, path): boolean` | |
37+
| limit | Set the count of filtered items | number \| false | 50 |
3738
| matchInputWidth | Whether the width of result list equals to input's | boolean | |
3839
| render | Used to render filtered options, you can use slot="showSearchRender" and slot-scope="{inputValue, path}" | `function({inputValue, path}): vNode` | |
3940
| sort | Used to sort filtered options. | `function(a, b, inputValue)` | |

components/cascader/index.jsx

Lines changed: 74 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Icon from '../icon'
1010
import { hasProp, filterEmpty, getOptionProps, getStyle, getClass, getAttrs, getComponentFromProp, isValidElement } from '../_util/props-util'
1111
import BaseMixin from '../_util/BaseMixin'
1212
import { cloneElement } from '../_util/vnode'
13+
import warning from '../_util/warning'
1314

1415
const CascaderOptionType = PropTypes.shape({
1516
value: PropTypes.string,
@@ -32,6 +33,7 @@ const ShowSearchType = PropTypes.shape({
3233
render: PropTypes.func,
3334
sort: PropTypes.func,
3435
matchInputWidth: PropTypes.bool,
36+
limit: PropTypes.oneOfType([Boolean, Number]),
3537
}).loose
3638
function noop () {}
3739

@@ -78,6 +80,9 @@ const CascaderProps = {
7880
suffixIcon: PropTypes.any,
7981
}
8082

83+
// We limit the filtered item count by default
84+
const defaultLimit = 50
85+
8186
function defaultFilterOption (inputValue, path, names) {
8287
return path.some(option => option[names.label].indexOf(inputValue) > -1)
8388
}
@@ -99,6 +104,26 @@ function getFilledFieldNames ({ fieldNames = {}}) {
99104
return names
100105
}
101106

107+
function flattenTree (
108+
options = [],
109+
props,
110+
ancestor = [],
111+
) {
112+
const names = getFilledFieldNames(props)
113+
let flattenOptions = []
114+
const childrenName = names.children
115+
options.forEach(option => {
116+
const path = ancestor.concat(option)
117+
if (props.changeOnSelect || !option[childrenName] || !option[childrenName].length) {
118+
flattenOptions.push(path)
119+
}
120+
if (option[childrenName]) {
121+
flattenOptions = flattenOptions.concat(flattenTree(option[childrenName], props, path))
122+
}
123+
})
124+
return flattenOptions
125+
}
126+
102127
const defaultDisplayRender = ({ labels }) => labels.join(' / ')
103128

104129
const Cascader = {
@@ -110,9 +135,13 @@ const Cascader = {
110135
prop: 'value',
111136
event: 'change',
112137
},
138+
inject: {
139+
configProvider: { default: {}},
140+
localeData: { default: {}},
141+
},
113142
data () {
114143
this.cachedOptions = []
115-
const { value, defaultValue, popupVisible, showSearch, options, flattenTree } = this
144+
const { value, defaultValue, popupVisible, showSearch, options } = this
116145
return {
117146
sValue: value || defaultValue || [],
118147
inputValue: '',
@@ -137,7 +166,7 @@ const Cascader = {
137166
},
138167
options (val) {
139168
if (this.showSearch) {
140-
this.setState({ flattenOptions: this.flattenTree(this.options, this.$props) })
169+
this.setState({ flattenOptions: flattenTree(val, this.$props) })
141170
}
142171
},
143172
},
@@ -171,11 +200,11 @@ const Cascader = {
171200

172201
handlePopupVisibleChange (popupVisible) {
173202
if (!hasProp(this, 'popupVisible')) {
174-
this.setState({
203+
this.setState(state => ({
175204
sPopupVisible: popupVisible,
176205
inputFocused: popupVisible,
177-
inputValue: popupVisible ? this.inputValue : '',
178-
})
206+
inputValue: popupVisible ? state.inputValue : '',
207+
}))
179208
}
180209
this.$emit('popupVisibleChange', popupVisible)
181210
},
@@ -244,36 +273,42 @@ const Cascader = {
244273
}
245274
},
246275

247-
flattenTree (options, props, ancestor = []) {
248-
const names = getFilledFieldNames(props)
249-
let flattenOptions = []
250-
const childrenName = names.children
251-
options.forEach((option) => {
252-
const path = ancestor.concat(option)
253-
if (props.changeOnSelect || !option[childrenName] || !option[childrenName].length) {
254-
flattenOptions.push(path)
255-
}
256-
if (option[childrenName]) {
257-
flattenOptions = flattenOptions.concat(
258-
this.flattenTree(option[childrenName], props, path)
259-
)
260-
}
261-
})
262-
return flattenOptions
263-
},
264-
265276
generateFilteredOptions (prefixCls) {
266277
const { showSearch, notFoundContent, $scopedSlots } = this
267278
const names = getFilledFieldNames(this.$props)
268279
const {
269280
filter = defaultFilterOption,
270281
// render = this.defaultRenderFilteredOption,
271282
sort = defaultSortFilteredOption,
283+
limit = defaultLimit,
272284
} = showSearch
273-
const { flattenOptions = [], inputValue } = this.$data
274285
const render = showSearch.render || $scopedSlots.showSearchRender || this.defaultRenderFilteredOption
275-
const filtered = flattenOptions.filter((path) => filter(inputValue, path, names))
276-
.sort((a, b) => sort(a, b, inputValue, names))
286+
const { flattenOptions = [], inputValue } = this.$data
287+
288+
// Limit the filter if needed
289+
let filtered
290+
if (limit > 0) {
291+
filtered = []
292+
let matchCount = 0
293+
294+
// Perf optimization to filter items only below the limit
295+
flattenOptions.some(path => {
296+
const match = filter(inputValue, path, names)
297+
if (match) {
298+
filtered.push(path)
299+
matchCount += 1
300+
}
301+
return matchCount >= limit
302+
})
303+
} else {
304+
warning(
305+
typeof limit !== 'number',
306+
"'limit' of showSearch in Cascader should be positive number or false.",
307+
)
308+
filtered = flattenOptions.filter(path => filter(inputValue, path, names))
309+
}
310+
311+
filtered.sort((a, b) => sort(a, b, inputValue, names))
277312

278313
if (filtered.length > 0) {
279314
return filtered.map((path) => {
@@ -307,14 +342,22 @@ const Cascader = {
307342
},
308343

309344
render () {
310-
const { $slots, sPopupVisible, inputValue, $listeners } = this
345+
const { $slots, sPopupVisible, inputValue, $listeners, configProvider, localeData } = this
311346
const { sValue: value, inputFocused } = this.$data
312347
const props = getOptionProps(this)
313348
let suffixIcon = getComponentFromProp(this, 'suffixIcon')
314349
suffixIcon = Array.isArray(suffixIcon) ? suffixIcon[0] : suffixIcon
350+
const { getPopupContainer: getContextPopupContainer } = configProvider
315351
const {
316-
prefixCls, inputPrefixCls, placeholder, size, disabled,
317-
allowClear, showSearch = false, ...otherProps } = props
352+
prefixCls,
353+
inputPrefixCls,
354+
placeholder = localeData.placeholder,
355+
size,
356+
disabled,
357+
allowClear,
358+
showSearch = false,
359+
...otherProps
360+
} = props
318361

319362
const sizeCls = classNames({
320363
[`${inputPrefixCls}-lg`]: size === 'large',
@@ -448,9 +491,11 @@ const Cascader = {
448491
<Icon type='redo' spin />
449492
</span>
450493
)
494+
const getPopupContainer = props.getPopupContainer || getContextPopupContainer
451495
const cascaderProps = {
452496
props: {
453497
...props,
498+
getPopupContainer,
454499
options: options,
455500
value: value,
456501
popupVisible: sPopupVisible,

components/cascader/index.zh-CN.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
| 参数 | 说明 | 类型 | 默认值 |
3535
| --- | --- | --- | --- |
3636
| filter | 接收 `inputValue` `path` 两个参数,当 `path` 符合筛选条件时,应返回 true,反之则返回 false。 | `function(inputValue, path): boolean` | |
37+
| limit | 搜索结果展示数量 | number \| false | 50 |
3738
| matchInputWidth | 搜索结果列表是否与输入框同宽 | boolean | |
3839
| render | 用于渲染 filter 后的选项,可使用slot="showSearchRender" 和 slot-scope="{inputValue, path}" | `function({inputValue, path}): vNode` | |
3940
| sort | 用于排序 filter 后的选项 | `function(a, b, inputValue)` | |

0 commit comments

Comments
 (0)