Skip to content

Commit c094790

Browse files
committed
feat: select add dropdownRender removeIcon clearIcon menuItemSelectedIcon
1 parent b955d50 commit c094790

File tree

12 files changed

+300
-73
lines changed

12 files changed

+300
-73
lines changed

components/select/__tests__/__snapshots__/demo.test.js.snap

Lines changed: 86 additions & 44 deletions
Large diffs are not rendered by default.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`Select Select Custom Icons should support customized icons 1`] = `
4+
<div class="ant-select ant-select-enabled">
5+
<div role="combobox" aria-autocomplete="list" aria-haspopup="true" aria-controls="test-uuid" tabindex="0" class="ant-select-selection ant-select-selection--single">
6+
<div class="ant-select-selection__rendered"></div><span unselectable="on" class="ant-select-arrow"><i class="ant-select-arrow-icon anticon anticon-down"><svg viewBox="64 64 896 896" data-icon="down" width="1em" height="1em" fill="currentColor" aria-hidden="true" class=""><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"></path></svg></i></span>
7+
</div>
8+
</div>
9+
`;

components/select/__tests__/index.test.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { mount } from '@vue/test-utils'
22
import { asyncExpect } from '@/tests/utils'
33
import Select from '..'
4+
import Icon from '../../icon'
45
import focusTest from '../../../tests/shared/focusTest'
56

67
describe('Select', () => {
@@ -144,4 +145,23 @@ describe('Select', () => {
144145
expect(triggerComponent.componentInstance.visible).toBe(false)
145146
})
146147
})
148+
149+
describe('Select Custom Icons', () => {
150+
it('should support customized icons', () => {
151+
const wrapper = mount({
152+
render () {
153+
return (
154+
<Select
155+
removeIcon={<Icon type='close' />}
156+
clearIcon={<Icon type='close' />}
157+
menuItemSelectedIcon={<Icon type='close' />}
158+
>
159+
<Option value='1'>1</Option>
160+
</Select>
161+
)
162+
},
163+
})
164+
expect(wrapper.html()).toMatchSnapshot()
165+
})
166+
})
147167
})

components/select/demo/basic.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ Basic Usage
2121
<a-select defaultValue="lucy" style='width: 120px' disabled>
2222
<a-select-option value="lucy">Lucy</a-select-option>
2323
</a-select>
24+
<a-select defaultValue="lucy" style='width: 120px' loading>
25+
<a-select-option value="lucy">Lucy</a-select-option>
26+
</a-select>
2427
</div>
2528
</template>
2629
<script>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
2+
<cn>
3+
#### 扩展菜单
4+
使用 `dropdownRender` 对下拉菜单进行自由扩展。
5+
</cn>
6+
7+
<us>
8+
#### Custom dropdown
9+
Customize the dropdown menu via `dropdownRender`.
10+
</us>
11+
12+
```html
13+
<template>
14+
<a-select defaultValue="lucy" style="width: 120px">
15+
<div slot="dropdownRender" slot-scope="menu">
16+
<v-nodes :vnodes="menu"/>
17+
<a-divider style="margin: 4px 0;" />
18+
<div style="padding: 8px; cursor: pointer;">
19+
<a-icon type="plus" /> Add item
20+
</div>
21+
</div>
22+
<a-select-option value="jack">Jack</a-select-option>
23+
<a-select-option value="lucy">Lucy</a-select-option>
24+
</a-select>
25+
</template>
26+
<script>
27+
export default {
28+
data: ()=>({console: console}),
29+
components: {
30+
VNodes: {
31+
functional: true,
32+
render: (h, ctx) => ctx.props.vnodes
33+
}
34+
},
35+
}
36+
</script>
37+
```
38+
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
2+
<cn>
3+
#### 隐藏已选择选项
4+
隐藏下拉列表中已选择的选项。
5+
</cn>
6+
7+
<us>
8+
#### Hide Already Selected
9+
Hide already selected options in the dropdown.
10+
</us>
11+
12+
```html
13+
<template>
14+
<a-select
15+
mode="multiple"
16+
placeholder="Inserted are removed"
17+
:value="selectedItems"
18+
@change="handleChange"
19+
style="width: 100%"
20+
>
21+
<a-select-option v-for="item in filteredOptions" :key="item" :value="item">
22+
{{item}}
23+
</a-select-option>
24+
</a-select>
25+
</template>
26+
<script>
27+
const OPTIONS = ['Apples', 'Nails', 'Bananas', 'Helicopters'];
28+
export default {
29+
data() {
30+
return {
31+
selectedItems: [],
32+
}
33+
},
34+
computed: {
35+
filteredOptions() {
36+
return OPTIONS.filter(o => !this.selectedItems.includes(o));
37+
}
38+
},
39+
methods: {
40+
handleChange(selectedItems) {
41+
this.selectedItems = selectedItems
42+
}
43+
}
44+
}
45+
</script>
46+
```
47+

components/select/demo/index.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import SearchBox from './search-box'
1111
import Search from './search'
1212
import SelectUsers from './select-users'
1313
import Suffix from './suffix'
14+
import HideSelected from './hide-selected'
15+
import CustomDropdownMenu from './custom-dropdown-menu'
1416
1517
import CN from '../index.zh-CN.md'
1618
import US from '../index.en-US.md'
@@ -50,6 +52,8 @@ export default {
5052
<Search/>
5153
<SelectUsers/>
5254
<Suffix/>
55+
<HideSelected />
56+
<CustomDropdownMenu />
5357
<api>
5458
<CN slot='cn' />
5559
<US/>

components/select/index.en-US.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
| disabled | Whether disabled select | boolean | false |
2020
| dropdownClassName | className of dropdown menu | string | - |
2121
| dropdownMatchSelectWidth | Whether dropdown's width is same with select. | boolean | true |
22+
| dropdownRender | Customize dropdown content | (menuNode: VNode, props) => VNode | - |
2223
| dropdownStyle | style of dropdown menu | object | - |
2324
| filterOption | If true, filter options by input, if function, filter options against it. 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. | boolean or function(inputValue, option) | true |
2425
| firstActiveValue | Value of action option by default | string\|string\[] | - |
@@ -34,12 +35,16 @@
3435
| showSearch | Whether show search input in single mode. | boolean | false |
3536
| showArrow | Whether to show the drop-down arrow | boolean | true |
3637
| size | Size of Select input. `default` `large` `small` | string | default |
37-
| suffixIcon | The custom suffix icon | string \| VNode \| slot | - |
38+
| suffixIcon | The custom suffix icon | VNode \| slot | - |
39+
| removeIcon | The custom remove icon | VNode \| slot | - |
40+
| clearIcon | The custom clear icon | VNode \| slot | - |
41+
| menuItemSelectedIcon | The custom menuItemSelected icon | VNode \| slot | - |
3842
| tokenSeparators | Separator used to tokenize on tag/multiple mode | string\[] | |
3943
| value(v-model) | Current selected option. | string\|number\|string\[]\|number\[] | - |
4044
| options | Data of the selectOption, manual construction work is no longer needed if this property has been set | array&lt;{value, label, [disabled, key, title]}> | \[] |
4145
| defaultOpen | Initial open state of dropdown | boolean | - |
4246
| open | Controlled open state of dropdown | boolean | - |
47+
| loading | indicate loading state | Boolean | false |
4348

4449
### events
4550
| Events Name | Description | Arguments |

components/select/index.jsx

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import { cloneElement } from '../_util/vnode'
1212
const AbstractSelectProps = () => ({
1313
prefixCls: PropTypes.string,
1414
size: PropTypes.oneOf(['small', 'large', 'default']),
15+
showAction: PropTypes.oneOfType([
16+
PropTypes.string,
17+
PropTypes.arrayOf(String),
18+
]),
1519
notFoundContent: PropTypes.any,
1620
transitionName: PropTypes.string,
1721
choiceTransitionName: PropTypes.string,
@@ -37,6 +41,8 @@ const AbstractSelectProps = () => ({
3741
open: PropTypes.bool,
3842
defaultOpen: PropTypes.bool,
3943
autoClearSearchValue: PropTypes.bool,
44+
dropdownRender: PropTypes.func,
45+
loading: PropTypes.bool,
4046
})
4147
const Value = PropTypes.shape({
4248
key: PropTypes.string,
@@ -71,6 +77,9 @@ const SelectProps = {
7177
getInputElement: PropTypes.func,
7278
options: PropTypes.array,
7379
suffixIcon: PropTypes.any,
80+
removeIcon: PropTypes.any,
81+
clearIcon: PropTypes.any,
82+
menuItemSelectedIcon: PropTypes.any,
7483
}
7584

7685
const SelectPropTypes = {
@@ -107,6 +116,9 @@ const Select = {
107116
prop: 'value',
108117
event: 'change',
109118
},
119+
inject: {
120+
configProvider: { default: {}},
121+
},
110122
created () {
111123
warning(
112124
this.$props.mode !== 'combobox',
@@ -134,17 +146,39 @@ const Select = {
134146
const { mode } = this
135147
return mode === 'combobox' || mode === SECRET_COMBOBOX_MODE_DO_NOT_USE
136148
},
149+
150+
renderSuffixIcon () {
151+
const { prefixCls, loading } = this.$props
152+
let suffixIcon = getComponentFromProp(this, 'suffixIcon')
153+
suffixIcon = Array.isArray(suffixIcon) ? suffixIcon[0] : suffixIcon
154+
if (suffixIcon) {
155+
return isValidElement(suffixIcon)
156+
? cloneElement(suffixIcon, { class: `${prefixCls}-arrow-icon` })
157+
: suffixIcon
158+
}
159+
if (loading) {
160+
return <Icon type='loading' />
161+
}
162+
return <Icon type='down' class={`${prefixCls}-arrow-icon`} />
163+
},
164+
137165
renderSelect (locale) {
138166
const {
139167
prefixCls,
140168
size,
141169
mode,
142170
options,
171+
getPopupContainer,
143172
...restProps
144173
} = getOptionProps(this)
145-
let suffixIcon = getComponentFromProp(this, 'suffixIcon')
146-
suffixIcon = Array.isArray(suffixIcon) ? suffixIcon[0] : suffixIcon
147-
const rest = omit(restProps, ['inputIcon', 'removeIcon', 'clearIcon', 'suffixIcon'])
174+
const { getPopupContainer: getContextPopupContainer } = this.configProvider
175+
let removeIcon = getComponentFromProp(this, 'removeIcon')
176+
removeIcon = Array.isArray(removeIcon) ? removeIcon[0] : removeIcon
177+
let clearIcon = getComponentFromProp(this, 'clearIcon')
178+
clearIcon = Array.isArray(clearIcon) ? clearIcon[0] : clearIcon
179+
let menuItemSelectedIcon = getComponentFromProp(this, 'menuItemSelectedIcon')
180+
menuItemSelectedIcon = Array.isArray(menuItemSelectedIcon) ? menuItemSelectedIcon[0] : menuItemSelectedIcon
181+
const rest = omit(restProps, ['inputIcon', 'removeIcon', 'clearIcon', 'suffixIcon', 'menuItemSelectedIcon'])
148182

149183
const cls = {
150184
[`${prefixCls}-lg`]: size === 'large',
@@ -162,31 +196,27 @@ const Select = {
162196
tags: mode === 'tags',
163197
combobox: this.isCombobox(),
164198
}
199+
const finalRemoveIcon = (removeIcon &&
200+
(isValidElement(removeIcon)
201+
? cloneElement(removeIcon, { class: `${prefixCls}-remove-icon` })
202+
: removeIcon)) || <Icon type='close' class={`${prefixCls}-remove-icon`} />
165203

166-
const inputIcon = suffixIcon && (
167-
isValidElement(suffixIcon)
168-
? cloneElement(suffixIcon) : suffixIcon) || (
169-
<Icon type='down' class={`${prefixCls}-arrow-icon`} />
170-
)
204+
const finalClearIcon = (clearIcon &&
205+
(isValidElement(clearIcon)
206+
? cloneElement(clearIcon, { class: `${prefixCls}-clear-icon` })
207+
: clearIcon)) || (<Icon type='close-circle' theme='filled' class={`${prefixCls}-clear-icon`} />)
171208

172-
const removeIcon = (
173-
<Icon type='close' class={`${prefixCls}-remove-icon`} />
174-
)
175-
176-
const clearIcon = (
177-
<Icon type='close-circle' theme='filled' class={`${prefixCls}-clear-icon`} />
178-
)
179-
180-
const menuItemSelectedIcon = (
181-
<Icon type='check' class={`${prefixCls}-selected-icon`} />
182-
)
209+
const finalMenuItemSelectedIcon = (menuItemSelectedIcon &&
210+
(isValidElement(menuItemSelectedIcon)
211+
? cloneElement(menuItemSelectedIcon, { class: `${prefixCls}-selected-icon` })
212+
: menuItemSelectedIcon)) || <Icon type='check' class={`${prefixCls}-selected-icon`} />
183213

184214
const selectProps = {
185215
props: {
186-
inputIcon,
187-
removeIcon,
188-
clearIcon,
189-
menuItemSelectedIcon,
216+
inputIcon: this.renderSuffixIcon(),
217+
removeIcon: finalRemoveIcon,
218+
clearIcon: finalClearIcon,
219+
menuItemSelectedIcon: finalMenuItemSelectedIcon,
190220
...rest,
191221
...modeConfig,
192222
prefixCls,
@@ -201,6 +231,8 @@ const Select = {
201231
})
202232
: filterEmpty(this.$slots.default),
203233
__propsSymbol__: Symbol(),
234+
dropdownRender: getComponentFromProp(this, 'dropdownRender', {}, false),
235+
getPopupContainer: getPopupContainer || getContextPopupContainer,
204236
},
205237
on: this.$listeners,
206238
class: cls,

components/select/index.zh-CN.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
| disabled | 是否禁用 | boolean | false |
1919
| dropdownClassName | 下拉菜单的 className 属性 | string | - |
2020
| dropdownMatchSelectWidth | 下拉菜单和选择器同宽 | boolean | true |
21+
| dropdownRender | 自定义下拉框内容 | (menuNode: VNode, props) => VNode | - |
2122
| dropdownStyle | 下拉菜单的 style 属性 | object | - |
2223
| filterOption | 是否根据输入项进行筛选。当其为一个函数时,会接收 `inputValue` `option` 两个参数,当 `option` 符合筛选条件时,应返回 `true`,反之则返回 `false`| boolean or function(inputValue, option) | true |
2324
| firstActiveValue | 默认高亮的选项 | string\|string\[] | - |
@@ -33,7 +34,10 @@
3334
| showSearch | 使单选模式可搜索 | boolean | false |
3435
| showArrow | 是否显示下拉小箭头 | boolean | true |
3536
| size | 选择框大小,可选 `large` `small` | string | default |
36-
| suffixIcon | 自定义的选择框后缀图标 | string \| VNode \| slot | - |
37+
| suffixIcon | 自定义的选择框后缀图标 | VNode \| slot | - |
38+
| removeIcon | 自定义的多选框清除图标 | VNode \| slot | - |
39+
| clearIcon | 自定义的多选框清空图标 | VNode \| slot | - |
40+
| menuItemSelectedIcon | 自定义当前选中的条目图标 | VNode \| slot | - |
3741
| tokenSeparators | 在 tags 和 multiple 模式下自动分词的分隔符 | string\[] | |
3842
| value(v-model) | 指定当前选中的条目 | string\|string\[]\|number\|number\[] | - |
3943
| options | options 数据,如果设置则不需要手动构造 selectOption 节点 | array&lt;{value, label, [disabled, key, title]}> | \[] |

0 commit comments

Comments
 (0)