perf: update select

pull/1845/head
tangjinzhou 2020-02-23 16:16:19 +08:00
parent 147f1530ed
commit 3e8e90da5e
13 changed files with 232 additions and 72 deletions

View File

@ -1,5 +1,5 @@
module.exports = {
dev: {
componentName: 'steps', // dev components
componentName: 'select', // dev components
},
};

View File

@ -63,7 +63,7 @@ exports[`renders ./components/select/demo/custom-dropdown-menu.md correctly 1`]
<div tabindex="0" class="ant-select ant-select-enabled" style="width: 120px;">
<div role="combobox" aria-autocomplete="list" aria-haspopup="true" aria-controls="test-uuid" class="ant-select-selection ant-select-selection--single">
<div class="ant-select-selection__rendered">
<div title="Lucy" class="ant-select-selection-selected-value" style="display: block; opacity: 1;">Lucy</div>
<div title="lucy" class="ant-select-selection-selected-value" style="display: block; opacity: 1;">lucy</div>
</div><span unselectable="on" class="ant-select-arrow" style="user-select: none;"><i aria-label="icon: down" 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" focusable="false" 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>
</div>
</div>
@ -125,6 +125,24 @@ exports[`renders ./components/select/demo/optgroup.md correctly 1`] = `
</div>
`;
exports[`renders ./components/select/demo/option-label-prop.md correctly 1`] = `
<div tabindex="0" class="ant-select ant-select-enabled" style="width: 100%;">
<div role="combobox" aria-autocomplete="list" aria-haspopup="true" aria-controls="test-uuid" class="ant-select-selection ant-select-selection--multiple">
<div class="ant-select-selection__rendered">
<div unselectable="on" class="ant-select-selection__placeholder" style="display: none; user-select: none;">select one country</div>
<div>
<li unselectable="on" role="presentation" title="China" class="ant-select-selection__choice" style="user-select: none;">
<div class="ant-select-selection__choice__content">China</div><span class="ant-select-selection__choice__remove"><i aria-label="icon: close" class="ant-select-remove-icon anticon anticon-close"><svg viewBox="64 64 896 896" data-icon="close" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" class=""><path d="M563.8 512l262.5-312.9c4.4-5.2.7-13.1-6.1-13.1h-79.8c-4.7 0-9.2 2.1-12.3 5.7L511.6 449.8 295.1 191.7c-3-3.6-7.5-5.7-12.3-5.7H203c-6.8 0-10.5 7.9-6.1 13.1L459.4 512 196.9 824.9A7.95 7.95 0 0 0 203 838h79.8c4.7 0 9.2-2.1 12.3-5.7l216.5-258.1 216.5 258.1c3 3.6 7.5 5.7 12.3 5.7h79.8c6.8 0 10.5-7.9 6.1-13.1L563.8 512z"></path></svg></i></span>
</li>
<li class="ant-select-search ant-select-search--inline">
<div class="ant-select-search__field__wrap"><input autocomplete="off" value="" class="ant-select-search__field"><span class="ant-select-search__field__mirror">&nbsp;</span></div>
</li>
</div>
</div>
</div>
</div>
`;
exports[`renders ./components/select/demo/options.md correctly 1`] = `
<div tabindex="0" class="ant-select ant-select-enabled" style="width: 120px;">
<div role="combobox" aria-autocomplete="list" aria-haspopup="true" aria-controls="test-uuid" class="ant-select-selection ant-select-selection--single">

View File

@ -3,9 +3,11 @@ import { asyncExpect } from '@/tests/utils';
import Select from '..';
import Icon from '../../icon';
import focusTest from '../../../tests/shared/focusTest';
import mountTest from '../../../tests/shared/mountTest';
describe('Select', () => {
focusTest(Select);
mountTest(Select);
it('should have default notFoundContent', async () => {
const wrapper = mount(Select, {

View File

@ -14,21 +14,27 @@ Customize the dropdown menu via `dropdownRender`.
<div slot="dropdownRender" slot-scope="menu">
<v-nodes :vnodes="menu" />
<a-divider style="margin: 4px 0;" />
<div style="padding: 8px; cursor: pointer;"><a-icon type="plus" /> Add item</div>
<div style="padding: 4px 8px; cursor: pointer;" @mousedown="e => e.preventDefault()" @click="addItem"><a-icon type="plus" /> Add item</div>
</div>
<a-select-option value="jack">Jack</a-select-option>
<a-select-option value="lucy">Lucy</a-select-option>
<a-select-option v-for="item in items" :value="item" :key="item">{{item}}</a-select-option>
</a-select>
</template>
<script>
let index = 0;
export default {
data: () => ({ console: console }),
data: () => ({ items: ['jack', 'lucy'] }),
components: {
VNodes: {
functional: true,
render: (h, ctx) => ctx.props.vnodes,
},
},
methods: {
addItem() {
console.log('addItem');
this.items.push(`New item ${index++}`)
}
}
};
</script>
```

View File

@ -13,6 +13,7 @@ import SelectUsers from './select-users';
import Suffix from './suffix';
import HideSelected from './hide-selected';
import CustomDropdownMenu from './custom-dropdown-menu';
import OptionLabelProp from './option-label-prop';
import CN from '../index.zh-CN.md';
import US from '../index.en-US.md';
@ -54,6 +55,7 @@ export default {
<Suffix />
<HideSelected />
<CustomDropdownMenu />
<OptionLabelProp />
<api>
<CN slot="cn" />
<US />

View File

@ -0,0 +1,60 @@
<cn>
#### 定制回填内容
使用 `optionLabelProp` 指定回填到选择框的 `Option` 属性。
</cn>
<us>
#### Custom selection render
Spacified the prop name of Option which will be rendered in select box.
</us>
```tpl
<template>
<a-select
mode="multiple"
style="width: 100%"
placeholder="select one country"
v-model="value"
optionLabelProp="label"
>
<a-select-option value="china" label="China">
<span role="img" aria-label="China">
🇨🇳
</span>
China (中国)
</a-select-option>
<a-select-option value="usa" label="USA">
<span role="img" aria-label="USA">
🇺🇸
</span>
USA (美国)
</a-select-option>
<a-select-option value="japan" label="Japan">
<span role="img" aria-label="Japan">
🇯🇵
</span>
Japan (日本)
</a-select-option>
<a-select-option value="korea" label="Korea">
<span role="img" aria-label="Korea">
🇰🇷
</span>
Korea (韩国)
</a-select-option>
</a-select>
</template>
<script>
export default {
data(){
return {
value: ['china']
}
},
watch: {
value(val) {
console.log(`selected:`, val);
},
},
};
</script>
```

View File

@ -20,6 +20,7 @@
| dropdownMatchSelectWidth | Whether dropdown's width is same with select. | boolean | true |
| dropdownRender | Customize dropdown content | (menuNode: VNode, props) => VNode | - |
| dropdownStyle | style of dropdown menu | object | - |
| dropdownMenuStyle | additional style applied to dropdown menu | object | - |
| 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 |
| firstActiveValue | Value of action option by default | string\|string\[] | - |
| getPopupContainer | Parent Node which the selector should be rendered to. Default to `body`. When position issues happen, try to modify it into scrollable content and position it relative. | function(triggerNode) | () => document.body |
@ -85,3 +86,9 @@
| -------- | ----------- | ------------ | ------- |
| key | | string | - |
| label | Group label | string\|slot | - |
## FAQ
### The dropdown is closed when click `dropdownRender` area?
See the [dropdownRender example](/components/select/#components-select-demo-custom-dropdown).

View File

@ -118,21 +118,13 @@ const Select = {
created() {
warning(
this.$props.mode !== 'combobox',
'Select',
'The combobox mode of Select is deprecated,' +
'it will be removed in next major version,' +
'please use AutoComplete instead',
);
},
methods: {
savePopupRef(ref) {
this.popupRef = ref;
},
focus() {
this.$refs.vcSelect.focus();
},
blur() {
this.$refs.vcSelect.blur();
},
getNotFoundContent(renderEmpty) {
const h = this.$createElement;
const notFoundContent = getComponentFromProp(this, 'notFoundContent');
@ -144,6 +136,16 @@ const Select = {
}
return renderEmpty(h, 'Select');
},
savePopupRef(ref) {
this.popupRef = ref;
},
focus() {
this.$refs.vcSelect.focus();
},
blur() {
this.$refs.vcSelect.blur();
},
isCombobox() {
const { mode } = this;
return mode === 'combobox' || mode === SECRET_COMBOBOX_MODE_DO_NOT_USE;
@ -171,6 +173,7 @@ const Select = {
mode,
options,
getPopupContainer,
showArrow,
...restProps
} = getOptionProps(this);
@ -198,6 +201,7 @@ const Select = {
const cls = {
[`${prefixCls}-lg`]: size === 'large',
[`${prefixCls}-sm`]: size === 'small',
[`${prefixCls}-show-arrow`]: showArrow,
};
let { optionLabelProp } = this.$props;
@ -234,6 +238,7 @@ const Select = {
removeIcon: finalRemoveIcon,
clearIcon: finalClearIcon,
menuItemSelectedIcon: finalMenuItemSelectedIcon,
showArrow,
...rest,
...modeConfig,
prefixCls,

View File

@ -20,6 +20,7 @@
| dropdownMatchSelectWidth | 下拉菜单和选择器同宽 | boolean | true |
| dropdownRender | 自定义下拉框内容 | (menuNode: VNode, props) => VNode | - |
| dropdownStyle | 下拉菜单的 style 属性 | object | - |
| dropdownMenuStyle | dropdown 菜单自定义样式 | object | - |
| filterOption | 是否根据输入项进行筛选。当其为一个函数时,会接收 `inputValue` `option` 两个参数,当 `option` 符合筛选条件时,应返回 `true`,反之则返回 `false`。 | boolean or function(inputValue, option) | true |
| firstActiveValue | 默认高亮的选项 | string\|string\[] | - |
| getPopupContainer | 菜单渲染父节点。默认渲染到 body 上,如果你遇到菜单滚动定位问题,试试修改为滚动的区域,并相对其定位。 | Function(triggerNode) | () => document.body |
@ -86,3 +87,9 @@
| ----- | ---- | --------------------------- | ------ |
| key | | string | - |
| label | 组名 | string\|\|function(h)\|slot | 无 |
## FAQ
### 点击 `dropdownRender` 里的内容浮层关闭怎么办?
看下 [dropdownRender 例子](/components/select-cn/#components-select-demo-custom-dropdown) 里的说明。

View File

@ -37,7 +37,7 @@ export default {
},
created() {
this.rafInstance = { cancel: () => null };
this.rafInstance = null;
this.lastInputValue = this.$props.inputValue;
this.lastVisible = false;
},
@ -60,8 +60,8 @@ export default {
this.prevVisible = this.visible;
},
beforeDestroy() {
if (this.rafInstance && this.rafInstance.cancel) {
this.rafInstance.cancel();
if (this.rafInstance) {
raf.cancel(this.rafInstance);
}
},
methods: {

View File

@ -59,6 +59,11 @@ const SELECT_EMPTY_VALUE_KEY = 'RC_SELECT_EMPTY_VALUE_KEY';
const noop = () => null;
// Where el is the DOM element you'd like to test for visibility
function isHidden(node) {
return !node || node.offsetParent === null;
}
function chaining(...fns) {
return function(...args) {
// eslint-disable-line
@ -130,6 +135,13 @@ const Select = {
this.__propsSymbol__,
'Replace slots.default with props.children and pass props.__propsSymbol__',
);
if (props.tags && typeof props.filterOption !== 'function') {
const isDisabledExist = Object.keys(optionsInfo).some(key => optionsInfo[key].disabled);
warning(
!isDisabledExist,
'Please avoid setting option to disabled in tags mode since user can always type text as tag.',
);
}
const state = {
_value: this.getValueFromProps(props, true), // true: use default value
_inputValue: props.combobox
@ -191,6 +203,7 @@ const Select = {
beforeDestroy() {
this.clearFocusTime();
this.clearBlurTime();
this.clearComboboxTime();
if (this.dropdownContainer) {
document.body.removeChild(this.dropdownContainer);
this.dropdownContainer = null;
@ -326,7 +339,7 @@ const Select = {
if (nextValue !== undefined) {
this.fireChange(nextValue);
}
this.setOpenState(false, true);
this.setOpenState(false, { needFocus: true });
this.setInputValue('', false);
return;
}
@ -378,14 +391,14 @@ const Select = {
},
onInputKeydown(event) {
const props = this.$props;
if (props.disabled) {
const { disabled, combobox, defaultActiveFirstOption } = this.$props;
if (disabled) {
return;
}
const state = this.$data;
const isRealOpen = this.getRealOpenState(state);
const keyCode = event.keyCode;
if (isMultipleOrTags(props) && !event.target.value && keyCode === KeyCode.BACKSPACE) {
if (isMultipleOrTags(this.$props) && !event.target.value && keyCode === KeyCode.BACKSPACE) {
event.preventDefault();
const { _value: value } = state;
if (value.length) {
@ -407,6 +420,12 @@ const Select = {
if (isRealOpen || !props.combobox) {
event.preventDefault();
}
// Hard close popup to avoid lock of non option in combobox mode
if (isRealOpen && combobox && defaultActiveFirstOption === false) {
this.comboboxTimer = setTimeout(() => {
this.setOpenState(false);
});
}
} else if (keyCode === KeyCode.ESC) {
if (state._open) {
this.setOpenState(false);
@ -433,12 +452,14 @@ const Select = {
const props = this.$props;
const selectedValue = getValuePropValue(item);
const lastValue = value[value.length - 1];
this.fireSelect(selectedValue);
let skipTrigger = false;
if (isMultipleOrTags(props)) {
if (findIndexInValueBySingleValue(value, selectedValue) !== -1) {
return;
skipTrigger = true;
} else {
value = value.concat([selectedValue]);
}
value = value.concat([selectedValue]);
} else {
if (
!isCombobox(props) &&
@ -446,30 +467,40 @@ const Select = {
lastValue === selectedValue &&
selectedValue !== this.$data._backfillValue
) {
this.setOpenState(false, true);
return;
this.setOpenState(false, { needFocus: true, fireSearch: false });
skipTrigger = true;
} else {
value = [selectedValue];
this.setOpenState(false, { needFocus: true, fireSearch: false });
}
value = [selectedValue];
this.setOpenState(false, true);
}
this.fireChange(value);
const inputValue = isCombobox(props) ? getPropValue(item, props.optionLabelProp) : '';
if (!skipTrigger) {
this.fireChange(value);
}
if (!skipTrigger) {
this.fireSelect(selectedValue);
const inputValue = isCombobox(props) ? getPropValue(item, props.optionLabelProp) : '';
if (props.autoClearSearchValue) {
this.setInputValue(inputValue, false);
if (props.autoClearSearchValue) {
this.setInputValue(inputValue, false);
}
}
},
onMenuDeselect({ item, domEvent }) {
if (domEvent.type === 'keydown' && domEvent.keyCode === KeyCode.ENTER) {
this.removeSelected(getValuePropValue(item));
const menuItemDomNode = item.$el;
// https://github.com/ant-design/ant-design/issues/20465#issuecomment-569033796
if (!isHidden(menuItemDomNode)) {
this.removeSelected(getValuePropValue(item));
}
return;
}
if (domEvent.type === 'click') {
this.removeSelected(getValuePropValue(item));
}
if (this.autoClearSearchValue) {
this.setInputValue('', false);
this.setInputValue('');
}
},
@ -478,7 +509,7 @@ const Select = {
e.preventDefault();
this.clearBlurTime();
if (!this.disabled) {
this.setOpenState(!this.$data._open, !this.$data._open);
this.setOpenState(!this.$data._open, { needFocus: !this.$data._open });
}
},
@ -505,7 +536,7 @@ const Select = {
if (value.length) {
this.fireChange([]);
}
this.setOpenState(false, true);
this.setOpenState(false, { needFocus: true });
if (inputValue) {
this.setInputValue('');
}
@ -527,9 +558,12 @@ const Select = {
}
let defaultLabel = value;
if (this.$props.labelInValue) {
const label = getLabelFromPropsValue(this.$props.value, value);
if (label !== undefined) {
defaultLabel = label;
const valueLabel = getLabelFromPropsValue(this.$props.value, value);
const defaultValueLabel = getLabelFromPropsValue(this.$props.defaultValue, value);
if (valueLabel !== undefined) {
defaultLabel = valueLabel;
} else if (defaultValueLabel !== undefined) {
defaultLabel = defaultValueLabel;
}
}
const defaultInfo = {
@ -734,7 +768,18 @@ const Select = {
return;
}
this.clearBlurTime();
if (!isMultipleOrTagsOrCombobox(this.$props) && e.target === this.getInputDOMNode()) {
// In IE11, onOuterFocus will be trigger twice when focus input
// First one: e.target is div
// Second one: e.target is input
// other browser only trigger second one
// https://github.com/ant-design/ant-design/issues/15942
// Here we ignore the first one when e.target is div
const inputNode = this.getInputDOMNode();
if (inputNode && e.target === this.rootRef) {
return;
}
if (!isMultipleOrTagsOrCombobox(this.$props) && e.target === inputNode) {
return;
}
if (this._focused) {
@ -837,8 +882,9 @@ const Select = {
}
},
setOpenState(open, needFocus) {
setOpenState(open, config = {}) {
const { $props: props, $data: state } = this;
const { needFocus, fireSearch } = config;
if (state._open === open) {
this.maybeFocus(open, !!needFocus);
return;
@ -850,7 +896,7 @@ const Select = {
};
// clear search input value when open is false in singleMode.
if (!open && isSingleMode(props) && props.showSearch) {
this.setInputValue('', false);
this.setInputValue('', fireSearch);
}
if (!open) {
this.maybeFocus(open, !!needFocus);
@ -1002,6 +1048,13 @@ const Select = {
}
},
clearComboboxTime() {
if (this.comboboxTimer) {
clearTimeout(this.comboboxTimer);
this.comboboxTimer = null;
}
},
updateFocusClassName() {
const { rootRef, prefixCls } = this;
// avoid setState and its side effect
@ -1130,30 +1183,18 @@ const Select = {
options.push(menuItem);
menuItems.push(menuItem);
});
if (inputValue) {
const notFindInputItem = menuItems.every(option => {
// this.filterOption return true has two meaning,
// 1, some one exists after filtering
// 2, filterOption is set to false
// condition 2 does not mean the option has same value with inputValue
const filterFn = () => getValuePropValue(option) === inputValue;
if (filterOption !== false) {
return !this._filterOption(inputValue, option, filterFn);
}
return !filterFn();
});
if (notFindInputItem) {
const p = {
attrs: UNSELECTABLE_ATTRIBUTE,
key: inputValue,
props: {
value: inputValue,
role: 'option',
},
style: UNSELECTABLE_STYLE,
};
options.unshift(<MenuItem {...p}>{inputValue}</MenuItem>);
}
// ref: https://github.com/ant-design/ant-design/issues/14090
if (inputValue && menuItems.every(option => getValuePropValue(option) !== inputValue)) {
const p = {
attrs: UNSELECTABLE_ATTRIBUTE,
key: inputValue,
props: {
value: inputValue,
role: 'option',
},
style: UNSELECTABLE_STYLE,
};
options.unshift(<MenuItem {...p}>{inputValue}</MenuItem>);
}
}

View File

@ -1,4 +1,5 @@
import classnames from 'classnames';
import raf from 'raf';
import Trigger from '../vc-trigger';
import PropTypes from '../_util/vue-types';
import DropdownMenu from './DropdownMenu';
@ -65,6 +66,7 @@ export default {
};
},
created() {
this.rafInstance = null;
this.saveDropdownMenuRef = saveRef(this, 'dropdownMenuRef');
this.saveTriggerRef = saveRef(this, 'triggerRef');
},
@ -80,14 +82,24 @@ export default {
this.setDropdownWidth();
});
},
beforeDestroy() {
this.cancelRafInstance();
},
methods: {
setDropdownWidth() {
const width = this.$el.offsetWidth;
if (width !== this.dropdownWidth) {
this.setState({ dropdownWidth: width });
this.cancelRafInstance();
this.rafInstance = raf(() => {
const width = this.$el.offsetWidth;
if (width !== this.dropdownWidth) {
this.setState({ dropdownWidth: width });
}
});
},
cancelRafInstance() {
if (this.rafInstance) {
raf.cancel(this.rafInstance);
}
},
getInnerMenu() {
return this.dropdownMenuRef && this.dropdownMenuRef.$refs.menuRef;
},

View File

@ -1,4 +1,4 @@
// based on vc-select 8.9.0
// based on vc-select 9.2.2
import ProxySelect, { Select } from './Select';
import Option from './Option';
import { SelectPropTypes } from './PropTypes';