refactor: select
parent
1c508d61fb
commit
d027f286e3
|
@ -31,11 +31,6 @@ The label of the selected item will be packed as an object for passing to the on
|
|||
import type { SelectProps } from 'ant-design-vue';
|
||||
import { defineComponent, ref } from 'vue';
|
||||
|
||||
interface Value {
|
||||
value?: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const options = ref<SelectProps['options']>([
|
||||
|
@ -48,11 +43,11 @@ export default defineComponent({
|
|||
label: 'Lucy (101)',
|
||||
},
|
||||
]);
|
||||
const handleChange = (value: Value) => {
|
||||
const handleChange: SelectProps['onChange'] = value => {
|
||||
console.log(value); // { key: "lucy", label: "Lucy (101)" }
|
||||
};
|
||||
return {
|
||||
value: ref<Value>({ value: 'lucy' }),
|
||||
value: ref({ value: 'lucy' }),
|
||||
options,
|
||||
handleChange,
|
||||
};
|
||||
|
|
|
@ -10,10 +10,13 @@ title:
|
|||
|
||||
使用 `optionLabelProp` 指定回填到选择框的 `Option` 属性。
|
||||
|
||||
或者使用 `tagRender` 插槽自定义渲染节点
|
||||
|
||||
## en-US
|
||||
|
||||
Spacified the prop name of Option which will be rendered in select box.
|
||||
|
||||
or use `tagRender` slot for custom rendering of tags.
|
||||
</docs>
|
||||
|
||||
<template>
|
||||
|
@ -23,7 +26,7 @@ Spacified the prop name of Option which will be rendered in select box.
|
|||
mode="multiple"
|
||||
style="width: 100%"
|
||||
placeholder="select one country"
|
||||
option-label-prop="label"
|
||||
option-label-prop="children"
|
||||
>
|
||||
<a-select-option value="china" label="China">
|
||||
<span role="img" aria-label="China">🇨🇳</span>
|
||||
|
@ -58,6 +61,29 @@ Spacified the prop name of Option which will be rendered in select box.
|
|||
</a-select>
|
||||
<span>Note: v-slot:option support from v2.2.5</span>
|
||||
</a-space>
|
||||
<br />
|
||||
<br />
|
||||
<a-space direction="vertical" style="width: 100%">
|
||||
<a-select
|
||||
v-model:value="value"
|
||||
mode="multiple"
|
||||
style="width: 100%"
|
||||
placeholder="select one country"
|
||||
:options="options"
|
||||
>
|
||||
<template #option="{ value: val, label, icon }">
|
||||
<span role="img" :aria-label="val">{{ icon }}</span>
|
||||
{{ label }}
|
||||
</template>
|
||||
<template #tagRender="{ value: val, label, closable, onClose, option }">
|
||||
<a-tag :closable="closable" style="margin-right: 3px" @close="onClose">
|
||||
{{ label }}
|
||||
<span role="img" :aria-label="val">{{ option.icon }}</span>
|
||||
</a-tag>
|
||||
</template>
|
||||
</a-select>
|
||||
<span>Note: v-slot:tagRender support from v3.0</span>
|
||||
</a-space>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, watch } from 'vue';
|
||||
|
@ -91,7 +117,6 @@ export default defineComponent({
|
|||
watch(value, val => {
|
||||
console.log(`selected:`, val);
|
||||
});
|
||||
|
||||
return {
|
||||
options,
|
||||
value,
|
||||
|
|
|
@ -19,7 +19,7 @@ The height of the input field for the select defaults to 32px. If size is set to
|
|||
<template>
|
||||
<a-radio-group v-model:value="size">
|
||||
<a-radio-button value="large">Large</a-radio-button>
|
||||
<a-radio-button value="default">Default</a-radio-button>
|
||||
<a-radio-button value="middle">Middle</a-radio-button>
|
||||
<a-radio-button value="small">Small</a-radio-button>
|
||||
</a-radio-group>
|
||||
<br />
|
||||
|
@ -51,6 +51,7 @@ The height of the input field for the select defaults to 32px. If size is set to
|
|||
</a-space>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import type { SizeType } from 'ant-design-vue/es/config-provider';
|
||||
import { defineComponent, ref } from 'vue';
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
|
@ -60,7 +61,7 @@ export default defineComponent({
|
|||
|
||||
return {
|
||||
popupScroll,
|
||||
size: ref('default'),
|
||||
size: ref<SizeType>('middle'),
|
||||
value1: ref('a1'),
|
||||
value2: ref(['a1', 'b2']),
|
||||
value3: ref(['a1', 'b2']),
|
||||
|
|
|
@ -55,6 +55,7 @@ Select component to select value from options.
|
|||
| clearIcon | The custom clear icon | VNode \| slot | - | |
|
||||
| menuItemSelectedIcon | The custom menuItemSelected icon | VNode \| slot | - | |
|
||||
| tokenSeparators | Separator used to tokenize on tag/multiple mode | string\[] | | |
|
||||
| tagRender | Customize tag render, only applies when `mode` is set to `multiple` or `tags` | slot \| (props) => any | - | |
|
||||
| value(v-model) | Current selected option. | string\|number\|string\[]\|number\[] | - | |
|
||||
| options | Data of the selectOption, manual construction work is no longer needed if this property has been set | array<{value, label, [disabled, key, title]}> | \[] | |
|
||||
| option | custom render option by slot | v-slot:option="{value, label, [disabled, key, title]}" | - | 2.2.5 |
|
||||
|
|
|
@ -1,26 +1,28 @@
|
|||
import type { App, PropType, Plugin, ExtractPropTypes } from 'vue';
|
||||
import { computed, defineComponent, ref } from 'vue';
|
||||
import classNames from '../_util/classNames';
|
||||
import RcSelect, { selectProps as vcSelectProps, Option, OptGroup } from '../vc-select';
|
||||
import type { OptionProps as OptionPropsType } from '../vc-select/Option';
|
||||
import type { BaseSelectRef } from '../vc-select2';
|
||||
import RcSelect, { selectProps as vcSelectProps, Option, OptGroup } from '../vc-select2';
|
||||
import type { BaseOptionType, DefaultOptionType } from '../vc-select2/Select';
|
||||
import type { OptionProps } from '../vc-select2/Option';
|
||||
import getIcons from './utils/iconUtil';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
import { tuple } from '../_util/type';
|
||||
import useConfigInject from '../_util/hooks/useConfigInject';
|
||||
import omit from '../_util/omit';
|
||||
import { useInjectFormItemContext } from '../form/FormItemContext';
|
||||
import { getTransitionName } from '../_util/transition';
|
||||
import type { SizeType } from '../config-provider';
|
||||
import { initDefaultProps } from '../_util/props-util';
|
||||
|
||||
type RawValue = string | number;
|
||||
|
||||
export type OptionProps = OptionPropsType;
|
||||
|
||||
export type OptionType = typeof Option;
|
||||
export type { OptionProps, BaseSelectRef as RefSelectProps, BaseOptionType, DefaultOptionType };
|
||||
|
||||
export interface LabeledValue {
|
||||
key?: string;
|
||||
value: RawValue;
|
||||
label: any;
|
||||
label?: any;
|
||||
}
|
||||
export type SelectValue = RawValue | RawValue[] | LabeledValue | LabeledValue[] | undefined;
|
||||
|
||||
|
@ -35,23 +37,28 @@ export const selectProps = () => ({
|
|||
notFoundContent: PropTypes.any,
|
||||
suffixIcon: PropTypes.any,
|
||||
itemIcon: PropTypes.any,
|
||||
size: PropTypes.oneOf(tuple('small', 'middle', 'large', 'default')),
|
||||
mode: PropTypes.oneOf(tuple('multiple', 'tags', 'SECRET_COMBOBOX_MODE_DO_NOT_USE')),
|
||||
bordered: PropTypes.looseBool.def(true),
|
||||
transitionName: PropTypes.string,
|
||||
choiceTransitionName: PropTypes.string.def(''),
|
||||
size: String as PropType<SizeType>,
|
||||
mode: String as PropType<'multiple' | 'tags' | 'SECRET_COMBOBOX_MODE_DO_NOT_USE'>,
|
||||
bordered: { type: Boolean, default: true },
|
||||
transitionName: String,
|
||||
choiceTransitionName: { type: String, default: '' },
|
||||
'onUpdate:value': Function as PropType<(val: SelectValue) => void>,
|
||||
});
|
||||
|
||||
export type SelectProps = Partial<ExtractPropTypes<ReturnType<typeof selectProps>>>;
|
||||
|
||||
const SECRET_COMBOBOX_MODE_DO_NOT_USE = 'SECRET_COMBOBOX_MODE_DO_NOT_USE';
|
||||
const Select = defineComponent({
|
||||
name: 'ASelect',
|
||||
Option,
|
||||
OptGroup,
|
||||
inheritAttrs: false,
|
||||
props: selectProps(),
|
||||
SECRET_COMBOBOX_MODE_DO_NOT_USE: 'SECRET_COMBOBOX_MODE_DO_NOT_USE',
|
||||
emits: ['change', 'update:value', 'blur'],
|
||||
props: initDefaultProps(selectProps(), {
|
||||
listHeight: 256,
|
||||
listItemHeight: 24,
|
||||
}),
|
||||
SECRET_COMBOBOX_MODE_DO_NOT_USE,
|
||||
// emits: ['change', 'update:value', 'blur'],
|
||||
slots: [
|
||||
'notFoundContent',
|
||||
'suffixIcon',
|
||||
|
@ -61,20 +68,21 @@ const Select = defineComponent({
|
|||
'dropdownRender',
|
||||
'option',
|
||||
'placeholder',
|
||||
'tagRender',
|
||||
],
|
||||
setup(props, { attrs, emit, slots, expose }) {
|
||||
const selectRef = ref();
|
||||
const selectRef = ref<BaseSelectRef>();
|
||||
const formItemContext = useInjectFormItemContext();
|
||||
const focus = () => {
|
||||
if (selectRef.value) {
|
||||
selectRef.value.focus();
|
||||
}
|
||||
selectRef.value?.focus();
|
||||
};
|
||||
|
||||
const blur = () => {
|
||||
if (selectRef.value) {
|
||||
selectRef.value.blur();
|
||||
}
|
||||
selectRef.value?.blur();
|
||||
};
|
||||
|
||||
const scrollTo: BaseSelectRef['scrollTo'] = arg => {
|
||||
selectRef.value?.scrollTo(arg);
|
||||
};
|
||||
|
||||
const mode = computed(() => {
|
||||
|
@ -84,7 +92,7 @@ const Select = defineComponent({
|
|||
return undefined;
|
||||
}
|
||||
|
||||
if (mode === Select.SECRET_COMBOBOX_MODE_DO_NOT_USE) {
|
||||
if (mode === SECRET_COMBOBOX_MODE_DO_NOT_USE) {
|
||||
return 'combobox';
|
||||
}
|
||||
|
||||
|
@ -103,19 +111,21 @@ const Select = defineComponent({
|
|||
[`${prefixCls.value}-borderless`]: !props.bordered,
|
||||
}),
|
||||
);
|
||||
const triggerChange = (...args: any[]) => {
|
||||
const triggerChange: SelectProps['onChange'] = (...args) => {
|
||||
emit('update:value', args[0]);
|
||||
emit('change', ...args);
|
||||
formItemContext.onFieldChange();
|
||||
};
|
||||
const handleBlur = (e: InputEvent) => {
|
||||
const handleBlur: SelectProps['onBlur'] = e => {
|
||||
emit('blur', e);
|
||||
formItemContext.onFieldBlur();
|
||||
};
|
||||
expose({
|
||||
blur,
|
||||
focus,
|
||||
scrollTo,
|
||||
});
|
||||
const isMultiple = computed(() => mode.value === 'multiple' || mode.value === 'tags');
|
||||
return () => {
|
||||
const {
|
||||
notFoundContent,
|
||||
|
@ -131,8 +141,6 @@ const Select = defineComponent({
|
|||
|
||||
const { renderEmpty, getPopupContainer: getContextPopupContainer } = configProvider;
|
||||
|
||||
const isMultiple = mode.value === 'multiple' || mode.value === 'tags';
|
||||
|
||||
// ===================== Empty =====================
|
||||
let mergedNotFound: any;
|
||||
if (notFoundContent !== undefined) {
|
||||
|
@ -149,7 +157,7 @@ const Select = defineComponent({
|
|||
const { suffixIcon, itemIcon, removeIcon, clearIcon } = getIcons(
|
||||
{
|
||||
...props,
|
||||
multiple: isMultiple,
|
||||
multiple: isMultiple.value,
|
||||
prefixCls: prefixCls.value,
|
||||
},
|
||||
slots,
|
||||
|
@ -195,9 +203,9 @@ const Select = defineComponent({
|
|||
dropdownRender={selectProps.dropdownRender || slots.dropdownRender}
|
||||
v-slots={{ option: slots.option }}
|
||||
transitionName={transitionName.value}
|
||||
>
|
||||
{slots.default?.()}
|
||||
</RcSelect>
|
||||
children={slots.default?.()}
|
||||
tagRender={props.tagRender || slots.tagRender}
|
||||
></RcSelect>
|
||||
);
|
||||
};
|
||||
},
|
||||
|
|
|
@ -56,6 +56,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/_0XzgOis7/Select.svg
|
|||
| clearIcon | 自定义的多选框清空图标 | VNode \| slot | - | |
|
||||
| menuItemSelectedIcon | 自定义当前选中的条目图标 | VNode \| slot | - | |
|
||||
| tokenSeparators | 在 tags 和 multiple 模式下自动分词的分隔符 | string\[] | | |
|
||||
| tagRender | 自定义 tag 内容 render,仅在 `mode` 为 `multiple` 或 `tags` 时生效 | slot \| (props) => any | - | 3.0 |
|
||||
| value(v-model) | 指定当前选中的条目 | string\|string\[]\|number\|number\[] | - | |
|
||||
| options | options 数据,如果设置则不需要手动构造 selectOption 节点 | array<{value, label, [disabled, key, title]}> | \[] | |
|
||||
| option | 通过 option 插槽,自定义节点 | v-slot:option="{value, label, [disabled, key, title]}" | - | 2.2.5 |
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
@import '../../style/themes/index';
|
||||
@import '../../style/mixins/index';
|
||||
@import '../../input/style/mixin';
|
||||
|
||||
@import './single';
|
||||
@import './multiple';
|
||||
|
||||
|
@ -59,6 +58,7 @@
|
|||
|
||||
&::-webkit-search-cancel-button {
|
||||
display: none;
|
||||
/* stylelint-disable-next-line property-no-vendor-prefix */
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
}
|
||||
|
@ -83,6 +83,7 @@
|
|||
&-selection-item {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
|
@ -117,7 +118,7 @@
|
|||
&-arrow {
|
||||
.iconfont-mixin();
|
||||
position: absolute;
|
||||
top: 53%;
|
||||
top: 50%;
|
||||
right: @control-padding-horizontal - 1px;
|
||||
width: @font-size-sm;
|
||||
height: @font-size-sm;
|
||||
|
@ -167,9 +168,11 @@
|
|||
opacity: 0;
|
||||
transition: color 0.3s ease, opacity 0.15s ease;
|
||||
text-rendering: auto;
|
||||
|
||||
&::before {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: @text-color-secondary;
|
||||
}
|
||||
|
@ -286,6 +289,9 @@
|
|||
}
|
||||
|
||||
&-disabled {
|
||||
&.@{select-prefix-cls}-item-option-selected {
|
||||
background-color: @select-multiple-disabled-background;
|
||||
}
|
||||
color: @disabled-color;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
@import './index';
|
||||
@import (reference) '../../style/themes/index';
|
||||
@select-prefix-cls: ~'@{ant-prefix}-select';
|
||||
|
||||
@select-overflow-prefix-cls: ~'@{select-prefix-cls}-selection-overflow';
|
||||
@select-multiple-item-border-width: 1px;
|
||||
|
@ -128,8 +129,6 @@
|
|||
.@{select-prefix-cls}-selection-search {
|
||||
position: relative;
|
||||
max-width: 100%;
|
||||
margin-top: @select-multiple-item-spacing-half;
|
||||
margin-bottom: @select-multiple-item-spacing-half;
|
||||
margin-inline-start: @input-padding-horizontal-base - @input-padding-vertical-base;
|
||||
|
||||
&-input,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
@import './index';
|
||||
@import (reference) '../../style/themes/index';
|
||||
@select-prefix-cls: ~'@{ant-prefix}-select';
|
||||
|
||||
@selection-item-padding: ceil(@font-size-base * 1.25);
|
||||
|
||||
|
@ -39,14 +40,15 @@
|
|||
}
|
||||
|
||||
.@{select-prefix-cls}-selection-placeholder {
|
||||
transition: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
// For common baseline align
|
||||
&::after,
|
||||
// For '' value baseline align
|
||||
/* For '' value baseline align */
|
||||
.@{select-prefix-cls}-selection-item::after,
|
||||
// For undefined value baseline align
|
||||
/* For undefined value baseline align */
|
||||
.@{select-prefix-cls}-selection-placeholder::after {
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
|
|
|
@ -1,162 +0,0 @@
|
|||
/**
|
||||
* To match accessibility requirement, we always provide an input in the component.
|
||||
* Other element will not set `tabIndex` to avoid `onBlur` sequence problem.
|
||||
* For focused select, we set `aria-live="polite"` to update the accessibility content.
|
||||
*
|
||||
* ref:
|
||||
* - keyboard: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role#Keyboard_interactions
|
||||
*
|
||||
* New api:
|
||||
* - listHeight
|
||||
* - listItemHeight
|
||||
* - component
|
||||
*
|
||||
* Remove deprecated api:
|
||||
* - multiple
|
||||
* - tags
|
||||
* - combobox
|
||||
* - firstActiveValue
|
||||
* - dropdownMenuStyle
|
||||
* - openClassName (Not list in api)
|
||||
*
|
||||
* Update:
|
||||
* - `backfill` only support `combobox` mode
|
||||
* - `combobox` mode not support `labelInValue` since it's meaningless
|
||||
* - `getInputElement` only support `combobox` mode
|
||||
* - `onChange` return OptionData instead of ReactNode
|
||||
* - `filterOption` `onChange` `onSelect` accept OptionData instead of ReactNode
|
||||
* - `combobox` mode trigger `onChange` will get `undefined` if no `value` match in Option
|
||||
* - `combobox` mode not support `optionLabelProp`
|
||||
*/
|
||||
|
||||
import BaseSelect, { baseSelectPropsWithoutPrivate, isMultiple } from './BaseSelect';
|
||||
import type {
|
||||
DisplayValueType,
|
||||
RenderNode,
|
||||
BaseSelectRef,
|
||||
BaseSelectPropsWithoutPrivate,
|
||||
BaseSelectProps,
|
||||
} from './BaseSelect';
|
||||
import OptionList from './OptionList';
|
||||
import Option from './Option';
|
||||
import OptGroup from './OptGroup';
|
||||
import useOptions from './hooks/useOptions';
|
||||
import SelectContext from './SelectContext';
|
||||
import useId from './hooks/useId';
|
||||
import useRefFunc from './hooks/useRefFunc';
|
||||
import { fillFieldNames, flattenOptions, injectPropsWithOption } from './utils/valueUtil';
|
||||
import warningProps from './utils/warningPropsUtil';
|
||||
import { toArray } from './utils/commonUtil';
|
||||
import useFilterOptions from './hooks/useFilterOptions';
|
||||
import useCache from './hooks/useCache';
|
||||
import type { Key } from '../_util/type';
|
||||
import { defineComponent } from 'vue';
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
|
||||
const OMIT_DOM_PROPS = ['inputValue'];
|
||||
|
||||
export type OnActiveValue = (
|
||||
active: RawValueType,
|
||||
index: number,
|
||||
info?: { source?: 'keyboard' | 'mouse' },
|
||||
) => void;
|
||||
|
||||
export type OnInternalSelect = (value: RawValueType, info: { selected: boolean }) => void;
|
||||
|
||||
export type RawValueType = string | number;
|
||||
export interface LabelInValueType {
|
||||
label: any;
|
||||
value: RawValueType;
|
||||
/** @deprecated `key` is useless since it should always same as `value` */
|
||||
key?: Key;
|
||||
}
|
||||
|
||||
export type DraftValueType =
|
||||
| RawValueType
|
||||
| LabelInValueType
|
||||
| DisplayValueType
|
||||
| (RawValueType | LabelInValueType | DisplayValueType)[];
|
||||
|
||||
export type FilterFunc<OptionType> = (inputValue: string, option?: OptionType) => boolean;
|
||||
|
||||
export interface FieldNames {
|
||||
value?: string;
|
||||
label?: string;
|
||||
options?: string;
|
||||
}
|
||||
|
||||
export interface BaseOptionType {
|
||||
disabled?: boolean;
|
||||
[name: string]: any;
|
||||
}
|
||||
|
||||
export interface DefaultOptionType extends BaseOptionType {
|
||||
label: any;
|
||||
value?: string | number | null;
|
||||
children?: Omit<DefaultOptionType, 'children'>[];
|
||||
}
|
||||
|
||||
export type SelectHandler<ValueType = any, OptionType extends BaseOptionType = DefaultOptionType> =
|
||||
| ((value: RawValueType | LabelInValueType, option: OptionType) => void)
|
||||
| ((value: ValueType, option: OptionType) => void);
|
||||
|
||||
export function selectProps<
|
||||
ValueType = any,
|
||||
OptionType extends BaseOptionType = DefaultOptionType,
|
||||
>() {
|
||||
return {
|
||||
...baseSelectPropsWithoutPrivate(),
|
||||
prefixCls: String,
|
||||
id: String,
|
||||
|
||||
backfill: { type: Boolean, default: undefined },
|
||||
|
||||
// >>> Field Names
|
||||
fieldNames: Object as PropType<FieldNames>,
|
||||
|
||||
// >>> Search
|
||||
/** @deprecated Use `searchValue` instead */
|
||||
inputValue: String,
|
||||
searchValue: String,
|
||||
onSearch: Function as PropType<(value: string) => void>,
|
||||
autoClearSearchValue: { type: Boolean, default: undefined },
|
||||
|
||||
// >>> Select
|
||||
onSelect: Function as PropType<SelectHandler<ValueType, OptionType>>,
|
||||
onDeselect: Function as PropType<SelectHandler<ValueType, OptionType>>,
|
||||
|
||||
// >>> Options
|
||||
/**
|
||||
* In Select, `false` means do nothing.
|
||||
* In TreeSelect, `false` will highlight match item.
|
||||
* It's by design.
|
||||
*/
|
||||
filterOption: {
|
||||
type: [Boolean, Function] as PropType<boolean | FilterFunc<OptionType>>,
|
||||
default: undefined,
|
||||
},
|
||||
filterSort: Function as PropType<(optionA: OptionType, optionB: OptionType) => number>,
|
||||
optionFilterProp: String,
|
||||
optionLabelProp: String,
|
||||
children: PropTypes.any,
|
||||
options: Array as PropType<OptionType[]>,
|
||||
defaultActiveFirstOption: { type: Boolean, default: undefined },
|
||||
virtual: { type: Boolean, default: undefined },
|
||||
listHeight: Number,
|
||||
listItemHeight: Number,
|
||||
|
||||
// >>> Icon
|
||||
menuItemSelectedIcon: PropTypes.any,
|
||||
|
||||
mode: String as PropType<'combobox' | 'multiple' | 'tags'>,
|
||||
labelInValue: { type: Boolean, default: undefined },
|
||||
value: PropTypes.any,
|
||||
defaultValue: PropTypes.any,
|
||||
onChange: Function as PropType<(value: ValueType, option: OptionType | OptionType[]) => void>,
|
||||
};
|
||||
}
|
||||
|
||||
export type SelectProps = Partial<ExtractPropTypes<ReturnType<typeof selectProps>>>;
|
||||
|
||||
export default defineComponent({});
|
File diff suppressed because it is too large
Load Diff
|
@ -1,73 +0,0 @@
|
|||
import type { VueNode } from '../../_util/type';
|
||||
|
||||
export type SelectSource = 'option' | 'selection' | 'input';
|
||||
|
||||
export const INTERNAL_PROPS_MARK = 'RC_SELECT_INTERNAL_PROPS_MARK';
|
||||
|
||||
// =================================== Shared Type ===================================
|
||||
export type Key = string | number;
|
||||
|
||||
export type RawValueType = string | number | null;
|
||||
|
||||
export interface LabelValueType extends Record<string, any> {
|
||||
key?: Key;
|
||||
value?: RawValueType;
|
||||
label?: any;
|
||||
isCacheable?: boolean;
|
||||
}
|
||||
export type DefaultValueType = RawValueType | RawValueType[] | LabelValueType | LabelValueType[];
|
||||
|
||||
export interface DisplayLabelValueType extends LabelValueType {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export type SingleType<MixType> = MixType extends (infer Single)[] ? Single : MixType;
|
||||
|
||||
export type OnClear = () => any;
|
||||
|
||||
export type CustomTagProps = {
|
||||
label: any;
|
||||
value: DefaultValueType;
|
||||
disabled: boolean;
|
||||
onClose: (event?: MouseEvent) => void;
|
||||
closable: boolean;
|
||||
};
|
||||
|
||||
// ==================================== Generator ====================================
|
||||
export type GetLabeledValue<FOT extends FlattenOptionsType> = (
|
||||
value: RawValueType,
|
||||
config: {
|
||||
options: FOT;
|
||||
prevValueMap: Map<RawValueType, LabelValueType>;
|
||||
labelInValue: boolean;
|
||||
optionLabelProp: string;
|
||||
},
|
||||
) => LabelValueType;
|
||||
|
||||
export type FilterOptions<OptionsType extends object[]> = (
|
||||
searchValue: string,
|
||||
options: OptionsType,
|
||||
/** Component props, since Select & TreeSelect use different prop name, use any here */
|
||||
config: {
|
||||
optionFilterProp: string;
|
||||
filterOption: boolean | FilterFunc<OptionsType[number]>;
|
||||
},
|
||||
) => OptionsType;
|
||||
|
||||
export type FilterFunc<OptionType> = (inputValue: string, option?: OptionType) => boolean;
|
||||
|
||||
export type FlattenOptionsType<OptionType = object> = {
|
||||
key: Key;
|
||||
data: OptionType;
|
||||
label?: any;
|
||||
value?: RawValueType;
|
||||
/** Used for customize data */
|
||||
[name: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
}[];
|
||||
|
||||
export type DropdownObject = {
|
||||
menuNode?: VueNode;
|
||||
props?: Record<string, any>;
|
||||
};
|
||||
|
||||
export type DropdownRender = (opt?: DropdownObject) => VueNode;
|
|
@ -1,62 +0,0 @@
|
|||
import type { VueNode } from '../../_util/type';
|
||||
import type { VNode, CSSProperties } from 'vue';
|
||||
import type { Key, RawValueType } from './generator';
|
||||
|
||||
export type RenderDOMFunc = (props: any) => HTMLElement;
|
||||
|
||||
export type RenderNode = VueNode | ((props: any) => VueNode);
|
||||
|
||||
export type Mode = 'multiple' | 'tags' | 'combobox';
|
||||
|
||||
// ======================== Option ========================
|
||||
|
||||
export interface FieldNames {
|
||||
value?: string;
|
||||
label?: string;
|
||||
options?: string;
|
||||
}
|
||||
|
||||
export type OnActiveValue = (
|
||||
active: RawValueType,
|
||||
index: number,
|
||||
info?: { source?: 'keyboard' | 'mouse' },
|
||||
) => void;
|
||||
|
||||
export interface OptionCoreData {
|
||||
key?: Key;
|
||||
disabled?: boolean;
|
||||
value?: Key;
|
||||
title?: string;
|
||||
class?: string;
|
||||
style?: CSSProperties;
|
||||
label?: VueNode;
|
||||
/** @deprecated Only works when use `children` as option data */
|
||||
children?: VNode[] | JSX.Element[];
|
||||
}
|
||||
|
||||
export interface OptionData extends OptionCoreData {
|
||||
/** Save for customize data */
|
||||
[prop: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
}
|
||||
|
||||
export interface OptionGroupData {
|
||||
key?: Key;
|
||||
label?: VueNode;
|
||||
options: OptionData[];
|
||||
class?: string;
|
||||
style?: CSSProperties;
|
||||
|
||||
/** Save for customize data */
|
||||
[prop: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
}
|
||||
|
||||
export type OptionsType = (OptionData | OptionGroupData)[];
|
||||
|
||||
export interface FlattenOptionData {
|
||||
group?: boolean;
|
||||
groupOption?: boolean;
|
||||
key: string | number;
|
||||
data: OptionData | OptionGroupData;
|
||||
label?: any;
|
||||
value?: RawValueType;
|
||||
}
|
|
@ -22,6 +22,7 @@ import {
|
|||
getCurrentInstance,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
provide,
|
||||
ref,
|
||||
toRefs,
|
||||
watch,
|
||||
|
@ -36,6 +37,7 @@ import { toReactive } from '../_util/toReactive';
|
|||
import classNames from '../_util/classNames';
|
||||
import OptionList from './OptionList';
|
||||
import createRef from '../_util/createRef';
|
||||
import type { BaseOptionType } from './Select';
|
||||
|
||||
const DEFAULT_OMIT_PROPS = [
|
||||
'value',
|
||||
|
@ -50,6 +52,8 @@ const DEFAULT_OMIT_PROPS = [
|
|||
'onInputKeyDown',
|
||||
'onPopupScroll',
|
||||
'tabindex',
|
||||
'OptionList',
|
||||
'notFoundContent',
|
||||
] as const;
|
||||
|
||||
export type RenderNode = VueNode | ((props: any) => VueNode);
|
||||
|
@ -74,20 +78,22 @@ export type CustomTagProps = {
|
|||
disabled: boolean;
|
||||
onClose: (event?: MouseEvent) => void;
|
||||
closable: boolean;
|
||||
option: BaseOptionType;
|
||||
};
|
||||
|
||||
export interface DisplayValueType {
|
||||
key?: Key;
|
||||
value?: RawValueType;
|
||||
label?: any;
|
||||
disabled: boolean;
|
||||
disabled?: boolean;
|
||||
option?: BaseOptionType;
|
||||
}
|
||||
|
||||
export interface BaseSelectRef {
|
||||
export type BaseSelectRef = {
|
||||
focus: () => void;
|
||||
blur: () => void;
|
||||
scrollTo: ScrollTo;
|
||||
}
|
||||
};
|
||||
|
||||
const baseSelectPrivateProps = () => {
|
||||
return {
|
||||
|
@ -251,7 +257,7 @@ export default defineComponent({
|
|||
name: 'BaseSelect',
|
||||
inheritAttrs: false,
|
||||
props: initDefaultProps(baseSelectProps(), { showAction: [], notFoundContent: 'Not Found' }),
|
||||
setup(props, { attrs, expose }) {
|
||||
setup(props, { attrs, expose, slots }) {
|
||||
const multiple = computed(() => isMultiple(props.mode));
|
||||
|
||||
const mergedShowSearch = computed(() =>
|
||||
|
@ -533,7 +539,10 @@ export default defineComponent({
|
|||
props.onBlur(...args);
|
||||
}
|
||||
};
|
||||
|
||||
provide('VCSelectContainerEvent', {
|
||||
focus: onContainerFocus,
|
||||
blur: onContainerBlur,
|
||||
});
|
||||
const activeTimeoutIds: any[] = [];
|
||||
|
||||
onMounted(() => {
|
||||
|
@ -743,7 +752,7 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
// =========================== OptionList ===========================
|
||||
const optionList = <OptionList ref={listRef} />;
|
||||
const optionList = <OptionList ref={listRef} v-slots={{ option: slots.option }} />;
|
||||
|
||||
// ============================= Select =============================
|
||||
const mergedClassName = classNames(prefixCls, attrs.class, {
|
||||
|
@ -822,14 +831,14 @@ export default defineComponent({
|
|||
} else {
|
||||
renderNode = (
|
||||
<div
|
||||
class={mergedClassName}
|
||||
{...domProps}
|
||||
class={mergedClassName}
|
||||
ref={containerRef}
|
||||
onMousedown={onInternalMouseDown}
|
||||
onKeydown={onInternalKeyDown}
|
||||
onKeyup={onInternalKeyUp}
|
||||
onFocus={onContainerFocus}
|
||||
onBlur={onContainerBlur}
|
||||
// onFocus={onContainerFocus}
|
||||
// onBlur={onContainerBlur}
|
||||
>
|
||||
{mockFocused && !mergedOpen.value && (
|
||||
<span
|
|
@ -278,7 +278,7 @@ const OptionList = defineComponent({
|
|||
// Group
|
||||
if (group) {
|
||||
return (
|
||||
<div class={classNames(itemPrefixCls, `${itemPrefixCls.value}-group`)}>
|
||||
<div class={classNames(itemPrefixCls.value, `${itemPrefixCls.value}-group`)}>
|
||||
{renderOption ? renderOption(data) : label !== undefined ? label : key}
|
||||
</div>
|
||||
);
|
||||
|
@ -298,12 +298,18 @@ const OptionList = defineComponent({
|
|||
const selected = rawValues.has(value);
|
||||
|
||||
const optionPrefixCls = `${itemPrefixCls.value}-option`;
|
||||
const optionClassName = classNames(itemPrefixCls, optionPrefixCls, cls, className, {
|
||||
[`${optionPrefixCls}-grouped`]: groupOption,
|
||||
[`${optionPrefixCls}-active`]: activeIndex === itemIndex && !disabled,
|
||||
[`${optionPrefixCls}-disabled`]: disabled,
|
||||
[`${optionPrefixCls}-selected`]: selected,
|
||||
});
|
||||
const optionClassName = classNames(
|
||||
itemPrefixCls.value,
|
||||
optionPrefixCls,
|
||||
cls,
|
||||
className,
|
||||
{
|
||||
[`${optionPrefixCls}-grouped`]: groupOption,
|
||||
[`${optionPrefixCls}-active`]: activeIndex === itemIndex && !disabled,
|
||||
[`${optionPrefixCls}-disabled`]: disabled,
|
||||
[`${optionPrefixCls}-selected`]: selected,
|
||||
},
|
||||
);
|
||||
|
||||
const mergedLabel = getLabel(item);
|
||||
|
|
@ -0,0 +1,642 @@
|
|||
/**
|
||||
* To match accessibility requirement, we always provide an input in the component.
|
||||
* Other element will not set `tabindex` to avoid `onBlur` sequence problem.
|
||||
* For focused select, we set `aria-live="polite"` to update the accessibility content.
|
||||
*
|
||||
* ref:
|
||||
* - keyboard: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role#Keyboard_interactions
|
||||
*
|
||||
* New api:
|
||||
* - listHeight
|
||||
* - listItemHeight
|
||||
* - component
|
||||
*
|
||||
* Remove deprecated api:
|
||||
* - multiple
|
||||
* - tags
|
||||
* - combobox
|
||||
* - firstActiveValue
|
||||
* - dropdownMenuStyle
|
||||
* - openClassName (Not list in api)
|
||||
*
|
||||
* Update:
|
||||
* - `backfill` only support `combobox` mode
|
||||
* - `combobox` mode not support `labelInValue` since it's meaningless
|
||||
* - `getInputElement` only support `combobox` mode
|
||||
* - `onChange` return OptionData instead of ReactNode
|
||||
* - `filterOption` `onChange` `onSelect` accept OptionData instead of ReactNode
|
||||
* - `combobox` mode trigger `onChange` will get `undefined` if no `value` match in Option
|
||||
* - `combobox` mode not support `optionLabelProp`
|
||||
*/
|
||||
|
||||
import BaseSelect, { baseSelectPropsWithoutPrivate, isMultiple } from './BaseSelect';
|
||||
import type { DisplayValueType, BaseSelectRef, BaseSelectProps } from './BaseSelect';
|
||||
import OptionList from './OptionList';
|
||||
import useOptions from './hooks/useOptions';
|
||||
import type { SelectContextProps } from './SelectContext';
|
||||
import { useProvideSelectProps } from './SelectContext';
|
||||
import useId from './hooks/useId';
|
||||
import { fillFieldNames, flattenOptions, injectPropsWithOption } from './utils/valueUtil';
|
||||
import warningProps from './utils/warningPropsUtil';
|
||||
import { toArray } from './utils/commonUtil';
|
||||
import useFilterOptions from './hooks/useFilterOptions';
|
||||
import useCache from './hooks/useCache';
|
||||
import type { Key, VueNode } from '../_util/type';
|
||||
import { computed, defineComponent, ref, toRef, watchEffect } from 'vue';
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import PropTypes from '../_util/vue-types';
|
||||
import { initDefaultProps } from '../_util/props-util';
|
||||
import useMergedState from '../_util/hooks/useMergedState';
|
||||
import useState from '../_util/hooks/useState';
|
||||
import { toReactive } from '../_util/toReactive';
|
||||
import omit from '../_util/omit';
|
||||
|
||||
const OMIT_DOM_PROPS = ['inputValue'];
|
||||
|
||||
export type OnActiveValue = (
|
||||
active: RawValueType,
|
||||
index: number,
|
||||
info?: { source?: 'keyboard' | 'mouse' },
|
||||
) => void;
|
||||
|
||||
export type OnInternalSelect = (value: RawValueType, info: { selected: boolean }) => void;
|
||||
|
||||
export type RawValueType = string | number;
|
||||
export interface LabelInValueType {
|
||||
label: any;
|
||||
value: RawValueType;
|
||||
/** @deprecated `key` is useless since it should always same as `value` */
|
||||
key?: Key;
|
||||
}
|
||||
|
||||
export type DraftValueType =
|
||||
| RawValueType
|
||||
| LabelInValueType
|
||||
| DisplayValueType
|
||||
| (RawValueType | LabelInValueType | DisplayValueType)[];
|
||||
|
||||
export type FilterFunc<OptionType> = (inputValue: string, option?: OptionType) => boolean;
|
||||
|
||||
export interface FieldNames {
|
||||
value?: string;
|
||||
label?: string;
|
||||
options?: string;
|
||||
}
|
||||
|
||||
export interface BaseOptionType {
|
||||
disabled?: boolean;
|
||||
[name: string]: any;
|
||||
}
|
||||
|
||||
export interface DefaultOptionType extends BaseOptionType {
|
||||
label?: any;
|
||||
value?: string | number | null;
|
||||
children?: Omit<DefaultOptionType, 'children'>[];
|
||||
}
|
||||
|
||||
export type SelectHandler<ValueType = any, OptionType extends BaseOptionType = DefaultOptionType> =
|
||||
| ((value: RawValueType | LabelInValueType, option: OptionType) => void)
|
||||
| ((value: ValueType, option: OptionType) => void);
|
||||
|
||||
export function selectProps<
|
||||
ValueType = any,
|
||||
OptionType extends BaseOptionType = DefaultOptionType,
|
||||
>() {
|
||||
return {
|
||||
...baseSelectPropsWithoutPrivate(),
|
||||
prefixCls: String,
|
||||
id: String,
|
||||
|
||||
backfill: { type: Boolean, default: undefined },
|
||||
|
||||
// >>> Field Names
|
||||
fieldNames: Object as PropType<FieldNames>,
|
||||
|
||||
// >>> Search
|
||||
/** @deprecated Use `searchValue` instead */
|
||||
inputValue: String,
|
||||
searchValue: String,
|
||||
onSearch: Function as PropType<(value: string) => void>,
|
||||
autoClearSearchValue: { type: Boolean, default: undefined },
|
||||
|
||||
// >>> Select
|
||||
onSelect: Function as PropType<SelectHandler<ValueType, OptionType>>,
|
||||
onDeselect: Function as PropType<SelectHandler<ValueType, OptionType>>,
|
||||
|
||||
// >>> Options
|
||||
/**
|
||||
* In Select, `false` means do nothing.
|
||||
* In TreeSelect, `false` will highlight match item.
|
||||
* It's by design.
|
||||
*/
|
||||
filterOption: {
|
||||
type: [Boolean, Function] as PropType<boolean | FilterFunc<OptionType>>,
|
||||
default: undefined,
|
||||
},
|
||||
filterSort: Function as PropType<(optionA: OptionType, optionB: OptionType) => number>,
|
||||
optionFilterProp: String,
|
||||
optionLabelProp: String,
|
||||
options: Array as PropType<OptionType[]>,
|
||||
defaultActiveFirstOption: { type: Boolean, default: undefined },
|
||||
virtual: { type: Boolean, default: undefined },
|
||||
listHeight: Number,
|
||||
listItemHeight: Number,
|
||||
|
||||
// >>> Icon
|
||||
menuItemSelectedIcon: PropTypes.any,
|
||||
|
||||
mode: String as PropType<'combobox' | 'multiple' | 'tags'>,
|
||||
labelInValue: { type: Boolean, default: undefined },
|
||||
value: PropTypes.any,
|
||||
defaultValue: PropTypes.any,
|
||||
onChange: Function as PropType<(value: ValueType, option: OptionType | OptionType[]) => void>,
|
||||
children: Array as PropType<VueNode[]>,
|
||||
};
|
||||
}
|
||||
|
||||
export type SelectProps = Partial<ExtractPropTypes<ReturnType<typeof selectProps>>>;
|
||||
|
||||
function isRawValue(value: DraftValueType): value is RawValueType {
|
||||
return !value || typeof value !== 'object';
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Select',
|
||||
inheritAttrs: false,
|
||||
props: initDefaultProps(selectProps(), {
|
||||
prefixCls: 'vc-select',
|
||||
autoClearSearchValue: true,
|
||||
listHeight: 200,
|
||||
listItemHeight: 20,
|
||||
}),
|
||||
setup(props, { expose, attrs, slots }) {
|
||||
const mergedId = useId(toRef(props, 'id'));
|
||||
const multiple = computed(() => isMultiple(props.mode));
|
||||
const childrenAsData = computed(() => !!(!props.options && props.children));
|
||||
|
||||
const mergedFilterOption = computed(() => {
|
||||
if (props.filterOption === undefined && props.mode === 'combobox') {
|
||||
return false;
|
||||
}
|
||||
return props.filterOption;
|
||||
});
|
||||
|
||||
// ========================= FieldNames =========================
|
||||
const mergedFieldNames = computed(() => fillFieldNames(props.fieldNames, childrenAsData.value));
|
||||
|
||||
// =========================== Search ===========================
|
||||
const [mergedSearchValue, setSearchValue] = useMergedState('', {
|
||||
value: computed(() =>
|
||||
props.searchValue !== undefined ? props.searchValue : props.inputValue,
|
||||
),
|
||||
postState: search => search || '',
|
||||
});
|
||||
|
||||
// =========================== Option ===========================
|
||||
const parsedOptions = useOptions(
|
||||
toRef(props, 'options'),
|
||||
toRef(props, 'children'),
|
||||
mergedFieldNames,
|
||||
);
|
||||
const { valueOptions, labelOptions, options: mergedOptions } = parsedOptions;
|
||||
|
||||
// ========================= Wrap Value =========================
|
||||
const convert2LabelValues = (draftValues: DraftValueType) => {
|
||||
// Convert to array
|
||||
const valueList = toArray(draftValues);
|
||||
|
||||
// Convert to labelInValue type
|
||||
return valueList.map(val => {
|
||||
let rawValue: RawValueType;
|
||||
let rawLabel: any;
|
||||
let rawKey: Key;
|
||||
let rawDisabled: boolean | undefined;
|
||||
|
||||
// Fill label & value
|
||||
if (isRawValue(val)) {
|
||||
rawValue = val;
|
||||
} else {
|
||||
rawKey = val.key;
|
||||
rawLabel = val.label;
|
||||
rawValue = val.value ?? rawKey;
|
||||
}
|
||||
|
||||
const option = valueOptions.value.get(rawValue);
|
||||
if (option) {
|
||||
// Fill missing props
|
||||
if (rawLabel === undefined)
|
||||
rawLabel = option?.[props.optionLabelProp || mergedFieldNames.value.label];
|
||||
if (rawKey === undefined) rawKey = option?.key ?? rawValue;
|
||||
rawDisabled = option?.disabled;
|
||||
|
||||
// Warning if label not same as provided
|
||||
// if (process.env.NODE_ENV !== 'production' && !isRawValue(val)) {
|
||||
// const optionLabel = option?.[mergedFieldNames.value.label];
|
||||
// if (optionLabel !== undefined && optionLabel !== rawLabel) {
|
||||
// warning(false, '`label` of `value` is not same as `label` in Select options.');
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
return {
|
||||
label: rawLabel,
|
||||
value: rawValue,
|
||||
key: rawKey,
|
||||
disabled: rawDisabled,
|
||||
option,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// =========================== Values ===========================
|
||||
const [internalValue, setInternalValue] = useMergedState(props.defaultValue, {
|
||||
value: toRef(props, 'value'),
|
||||
});
|
||||
|
||||
// Merged value with LabelValueType
|
||||
const rawLabeledValues = computed(() => {
|
||||
const values = convert2LabelValues(internalValue.value);
|
||||
|
||||
// combobox no need save value when it's empty
|
||||
if (props.mode === 'combobox' && !values[0]?.value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return values;
|
||||
});
|
||||
|
||||
// Fill label with cache to avoid option remove
|
||||
const [mergedValues, getMixedOption] = useCache(rawLabeledValues, valueOptions);
|
||||
|
||||
const displayValues = computed(() => {
|
||||
// `null` need show as placeholder instead
|
||||
// https://github.com/ant-design/ant-design/issues/25057
|
||||
if (!props.mode && mergedValues.value.length === 1) {
|
||||
const firstValue = mergedValues.value[0];
|
||||
if (
|
||||
firstValue.value === null &&
|
||||
(firstValue.label === null || firstValue.label === undefined)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
return mergedValues.value.map(item => ({
|
||||
...item,
|
||||
label: item.label ?? item.value,
|
||||
}));
|
||||
});
|
||||
|
||||
/** Convert `displayValues` to raw value type set */
|
||||
const rawValues = computed(() => new Set(mergedValues.value.map(val => val.value)));
|
||||
|
||||
watchEffect(
|
||||
() => {
|
||||
if (props.mode === 'combobox') {
|
||||
const strValue = mergedValues.value[0]?.value;
|
||||
|
||||
if (strValue !== undefined && strValue !== null) {
|
||||
setSearchValue(String(strValue));
|
||||
}
|
||||
}
|
||||
},
|
||||
{ flush: 'post' },
|
||||
);
|
||||
|
||||
// ======================= Display Option =======================
|
||||
// Create a placeholder item if not exist in `options`
|
||||
const createTagOption = (val: RawValueType, label?: any) => {
|
||||
const mergedLabel = label ?? val;
|
||||
return {
|
||||
[mergedFieldNames.value.value]: val,
|
||||
[mergedFieldNames.value.label]: mergedLabel,
|
||||
} as DefaultOptionType;
|
||||
};
|
||||
|
||||
// Fill tag as option if mode is `tags`
|
||||
const filledTagOptions = computed(() => {
|
||||
if (props.mode !== 'tags') {
|
||||
return mergedOptions.value;
|
||||
}
|
||||
|
||||
// >>> Tag mode
|
||||
const cloneOptions = [...mergedOptions.value];
|
||||
|
||||
// Check if value exist in options (include new patch item)
|
||||
const existOptions = (val: RawValueType) => valueOptions.value.has(val);
|
||||
|
||||
// Fill current value as option
|
||||
[...mergedValues.value]
|
||||
.sort((a, b) => (a.value < b.value ? -1 : 1))
|
||||
.forEach(item => {
|
||||
const val = item.value;
|
||||
|
||||
if (!existOptions(val)) {
|
||||
cloneOptions.push(createTagOption(val, item.label));
|
||||
}
|
||||
});
|
||||
|
||||
return cloneOptions;
|
||||
});
|
||||
|
||||
const filteredOptions = useFilterOptions(
|
||||
filledTagOptions,
|
||||
mergedFieldNames,
|
||||
mergedSearchValue,
|
||||
mergedFilterOption,
|
||||
toRef(props, 'optionFilterProp'),
|
||||
);
|
||||
|
||||
// Fill options with search value if needed
|
||||
const filledSearchOptions = computed(() => {
|
||||
if (
|
||||
props.mode !== 'tags' ||
|
||||
!mergedSearchValue.value ||
|
||||
filteredOptions.value.some(
|
||||
item => item[props.optionFilterProp || 'value'] === mergedSearchValue.value,
|
||||
)
|
||||
) {
|
||||
return filteredOptions.value;
|
||||
}
|
||||
|
||||
// Fill search value as option
|
||||
return [createTagOption(mergedSearchValue.value), ...filteredOptions.value];
|
||||
});
|
||||
|
||||
const orderedFilteredOptions = computed(() => {
|
||||
if (!props.filterSort) {
|
||||
return filledSearchOptions.value;
|
||||
}
|
||||
|
||||
return [...filledSearchOptions.value].sort((a, b) => props.filterSort(a, b));
|
||||
});
|
||||
|
||||
const displayOptions = computed(() =>
|
||||
flattenOptions(orderedFilteredOptions.value, {
|
||||
fieldNames: mergedFieldNames.value,
|
||||
childrenAsData: childrenAsData.value,
|
||||
}),
|
||||
);
|
||||
|
||||
// =========================== Change ===========================
|
||||
const triggerChange = (values: DraftValueType) => {
|
||||
const labeledValues = convert2LabelValues(values);
|
||||
setInternalValue(labeledValues);
|
||||
|
||||
if (
|
||||
props.onChange &&
|
||||
// Trigger event only when value changed
|
||||
(labeledValues.length !== mergedValues.value.length ||
|
||||
labeledValues.some((newVal, index) => mergedValues.value[index]?.value !== newVal?.value))
|
||||
) {
|
||||
const returnValues = props.labelInValue ? labeledValues : labeledValues.map(v => v.value);
|
||||
const returnOptions = labeledValues.map(v =>
|
||||
injectPropsWithOption(getMixedOption(v.value)),
|
||||
);
|
||||
|
||||
props.onChange(
|
||||
// Value
|
||||
multiple.value ? returnValues : returnValues[0],
|
||||
// Option
|
||||
multiple.value ? returnOptions : returnOptions[0],
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// ======================= Accessibility ========================
|
||||
const [activeValue, setActiveValue] = useState<string>(null);
|
||||
const [accessibilityIndex, setAccessibilityIndex] = useState(0);
|
||||
const mergedDefaultActiveFirstOption = computed(() =>
|
||||
props.defaultActiveFirstOption !== undefined
|
||||
? props.defaultActiveFirstOption
|
||||
: props.mode !== 'combobox',
|
||||
);
|
||||
|
||||
const onActiveValue: OnActiveValue = (active, index, { source = 'keyboard' } = {}) => {
|
||||
setAccessibilityIndex(index);
|
||||
|
||||
if (props.backfill && props.mode === 'combobox' && active !== null && source === 'keyboard') {
|
||||
setActiveValue(String(active));
|
||||
}
|
||||
};
|
||||
|
||||
// ========================= OptionList =========================
|
||||
const triggerSelect = (val: RawValueType, selected: boolean) => {
|
||||
const getSelectEnt = (): [RawValueType | LabelInValueType, DefaultOptionType] => {
|
||||
const option = getMixedOption(val);
|
||||
return [
|
||||
props.labelInValue
|
||||
? {
|
||||
label: option?.[mergedFieldNames.value.label],
|
||||
value: val,
|
||||
key: option.key ?? val,
|
||||
}
|
||||
: val,
|
||||
injectPropsWithOption(option),
|
||||
];
|
||||
};
|
||||
|
||||
if (selected && props.onSelect) {
|
||||
const [wrappedValue, option] = getSelectEnt();
|
||||
props.onSelect(wrappedValue, option);
|
||||
} else if (!selected && props.onDeselect) {
|
||||
const [wrappedValue, option] = getSelectEnt();
|
||||
props.onDeselect(wrappedValue, option);
|
||||
}
|
||||
};
|
||||
|
||||
// Used for OptionList selection
|
||||
const onInternalSelect = (val, info) => {
|
||||
let cloneValues: (RawValueType | DisplayValueType)[];
|
||||
|
||||
// Single mode always trigger select only with option list
|
||||
const mergedSelect = multiple.value ? info.selected : true;
|
||||
|
||||
if (mergedSelect) {
|
||||
cloneValues = multiple.value ? [...mergedValues.value, val] : [val];
|
||||
} else {
|
||||
cloneValues = mergedValues.value.filter(v => v.value !== val);
|
||||
}
|
||||
|
||||
triggerChange(cloneValues);
|
||||
triggerSelect(val, mergedSelect);
|
||||
|
||||
// Clean search value if single or configured
|
||||
if (props.mode === 'combobox') {
|
||||
// setSearchValue(String(val));
|
||||
setActiveValue('');
|
||||
} else if (!multiple.value || props.autoClearSearchValue) {
|
||||
setSearchValue('');
|
||||
setActiveValue('');
|
||||
}
|
||||
};
|
||||
|
||||
// ======================= Display Change =======================
|
||||
// BaseSelect display values change
|
||||
const onDisplayValuesChange: BaseSelectProps['onDisplayValuesChange'] = (nextValues, info) => {
|
||||
triggerChange(nextValues);
|
||||
|
||||
if (info.type === 'remove' || info.type === 'clear') {
|
||||
info.values.forEach(item => {
|
||||
triggerSelect(item.value, false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// =========================== Search ===========================
|
||||
const onInternalSearch: BaseSelectProps['onSearch'] = (searchText, info) => {
|
||||
setSearchValue(searchText);
|
||||
setActiveValue(null);
|
||||
|
||||
// [Submit] Tag mode should flush input
|
||||
if (info.source === 'submit') {
|
||||
const formatted = (searchText || '').trim();
|
||||
// prevent empty tags from appearing when you click the Enter button
|
||||
if (formatted) {
|
||||
const newRawValues = Array.from(new Set<RawValueType>([...rawValues.value, formatted]));
|
||||
triggerChange(newRawValues);
|
||||
triggerSelect(formatted, true);
|
||||
setSearchValue('');
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (info.source !== 'blur') {
|
||||
if (props.mode === 'combobox') {
|
||||
triggerChange(searchText);
|
||||
}
|
||||
|
||||
props.onSearch?.(searchText);
|
||||
}
|
||||
};
|
||||
|
||||
const onInternalSearchSplit: BaseSelectProps['onSearchSplit'] = words => {
|
||||
let patchValues: RawValueType[] = words;
|
||||
|
||||
if (props.mode !== 'tags') {
|
||||
patchValues = words
|
||||
.map(word => {
|
||||
const opt = labelOptions.value.get(word);
|
||||
return opt?.value;
|
||||
})
|
||||
.filter(val => val !== undefined);
|
||||
}
|
||||
|
||||
const newRawValues = Array.from(new Set<RawValueType>([...rawValues.value, ...patchValues]));
|
||||
triggerChange(newRawValues);
|
||||
newRawValues.forEach(newRawValue => {
|
||||
triggerSelect(newRawValue, true);
|
||||
});
|
||||
};
|
||||
const realVirtual = computed(
|
||||
() => props.virtual !== false && props.dropdownMatchSelectWidth !== false,
|
||||
);
|
||||
useProvideSelectProps(
|
||||
toReactive({
|
||||
...parsedOptions,
|
||||
flattenOptions: displayOptions,
|
||||
onActiveValue,
|
||||
defaultActiveFirstOption: mergedDefaultActiveFirstOption,
|
||||
onSelect: onInternalSelect,
|
||||
menuItemSelectedIcon: toRef(props, 'menuItemSelectedIcon'),
|
||||
rawValues,
|
||||
fieldNames: mergedFieldNames,
|
||||
virtual: realVirtual,
|
||||
listHeight: toRef(props, 'listHeight'),
|
||||
listItemHeight: toRef(props, 'listItemHeight'),
|
||||
childrenAsData,
|
||||
} as unknown as SelectContextProps),
|
||||
);
|
||||
|
||||
// ========================== Warning ===========================
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
watchEffect(
|
||||
() => {
|
||||
warningProps(props);
|
||||
},
|
||||
{ flush: 'post' },
|
||||
);
|
||||
}
|
||||
const selectRef = ref<BaseSelectRef>();
|
||||
expose({
|
||||
focus() {
|
||||
selectRef.value?.focus();
|
||||
},
|
||||
blur() {
|
||||
selectRef.value?.blur();
|
||||
},
|
||||
scrollTo(arg) {
|
||||
selectRef.value?.scrollTo(arg);
|
||||
},
|
||||
} as BaseSelectRef);
|
||||
const pickProps = computed(() => {
|
||||
return omit(props, [
|
||||
'id',
|
||||
'mode',
|
||||
'prefixCls',
|
||||
'backfill',
|
||||
'fieldNames',
|
||||
|
||||
// Search
|
||||
'inputValue',
|
||||
'searchValue',
|
||||
'onSearch',
|
||||
'autoClearSearchValue',
|
||||
|
||||
// Select
|
||||
'onSelect',
|
||||
'onDeselect',
|
||||
'dropdownMatchSelectWidth',
|
||||
|
||||
// Options
|
||||
'filterOption',
|
||||
'filterSort',
|
||||
'optionFilterProp',
|
||||
'optionLabelProp',
|
||||
'options',
|
||||
'children',
|
||||
'defaultActiveFirstOption',
|
||||
'menuItemSelectedIcon',
|
||||
'virtual',
|
||||
'listHeight',
|
||||
'listItemHeight',
|
||||
|
||||
// Value
|
||||
'value',
|
||||
'defaultValue',
|
||||
'labelInValue',
|
||||
'onChange',
|
||||
]);
|
||||
});
|
||||
return () => {
|
||||
return (
|
||||
<BaseSelect
|
||||
{...pickProps.value}
|
||||
{...attrs}
|
||||
// >>> MISC
|
||||
id={mergedId}
|
||||
prefixCls={props.prefixCls}
|
||||
ref={selectRef}
|
||||
omitDomProps={OMIT_DOM_PROPS}
|
||||
mode={props.mode}
|
||||
// >>> Values
|
||||
displayValues={displayValues.value}
|
||||
onDisplayValuesChange={onDisplayValuesChange}
|
||||
// >>> Search
|
||||
searchValue={mergedSearchValue.value}
|
||||
onSearch={onInternalSearch}
|
||||
onSearchSplit={onInternalSearchSplit}
|
||||
dropdownMatchSelectWidth={props.dropdownMatchSelectWidth}
|
||||
// >>> OptionList
|
||||
OptionList={OptionList}
|
||||
emptyOptions={!displayOptions.value.length}
|
||||
// >>> Accessibility
|
||||
activeValue={activeValue.value}
|
||||
activeDescendantId={`${mergedId}_list_${accessibilityIndex.value}`}
|
||||
v-slots={slots}
|
||||
/>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -9,6 +9,7 @@ import PropTypes from '../../_util/vue-types';
|
|||
import type { VueNode } from '../../_util/type';
|
||||
import Overflow from '../../vc-overflow';
|
||||
import type { DisplayValueType, RenderNode, CustomTagProps, RawValueType } from '../BaseSelect';
|
||||
import type { BaseOptionType } from '../Select';
|
||||
|
||||
type SelectorProps = InnerSelectorProps & {
|
||||
// Icon
|
||||
|
@ -140,12 +141,12 @@ const SelectSelector = defineComponent<SelectorProps>({
|
|||
itemDisabled: boolean,
|
||||
closable: boolean,
|
||||
onClose: (e: MouseEvent) => void,
|
||||
option: BaseOptionType,
|
||||
) {
|
||||
const onMouseDown = (e: MouseEvent) => {
|
||||
onPreventMouseDown(e);
|
||||
props.onToggleOpen(!open);
|
||||
};
|
||||
|
||||
return (
|
||||
<span onMousedown={onMouseDown}>
|
||||
{props.tagRender({
|
||||
|
@ -154,13 +155,14 @@ const SelectSelector = defineComponent<SelectorProps>({
|
|||
disabled: itemDisabled,
|
||||
closable,
|
||||
onClose,
|
||||
option,
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function renderItem(valueItem: DisplayValueType) {
|
||||
const { disabled: itemDisabled, label, value } = valueItem;
|
||||
const { disabled: itemDisabled, label, value, option } = valueItem;
|
||||
const closable = !props.disabled && !itemDisabled;
|
||||
|
||||
let displayLabel = label;
|
||||
|
@ -180,7 +182,7 @@ const SelectSelector = defineComponent<SelectorProps>({
|
|||
};
|
||||
|
||||
return typeof props.tagRender === 'function'
|
||||
? customizeRenderSelector(value, displayLabel, itemDisabled, closable, onClose)
|
||||
? customizeRenderSelector(value, displayLabel, itemDisabled, closable, onClose, option)
|
||||
: defaultRenderSelector(label, displayLabel, itemDisabled, closable, onClose);
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Ref } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { shallowRef, watchEffect } from 'vue';
|
||||
import type { FieldNames, RawValueType } from '../Select';
|
||||
import { convertChildrenToData } from '../utils/legacyUtil';
|
||||
|
||||
|
@ -12,35 +12,40 @@ export default function useOptions<OptionType>(
|
|||
children: Ref<any>,
|
||||
fieldNames: Ref<FieldNames>,
|
||||
) {
|
||||
return computed(() => {
|
||||
let mergedOptions = options.value;
|
||||
const mergedOptions = shallowRef();
|
||||
const valueOptions = shallowRef();
|
||||
const labelOptions = shallowRef();
|
||||
watchEffect(() => {
|
||||
let newOptions = options.value;
|
||||
const childrenAsData = !options.value;
|
||||
|
||||
if (childrenAsData) {
|
||||
mergedOptions = convertChildrenToData(children.value);
|
||||
newOptions = convertChildrenToData(children.value);
|
||||
}
|
||||
|
||||
const valueOptions = new Map<RawValueType, OptionType>();
|
||||
const labelOptions = new Map<any, OptionType>();
|
||||
const newValueOptions = new Map<RawValueType, OptionType>();
|
||||
const newLabelOptions = new Map<any, OptionType>();
|
||||
|
||||
function dig(optionList: OptionType[], isChildren = false) {
|
||||
// for loop to speed up collection speed
|
||||
for (let i = 0; i < optionList.length; i += 1) {
|
||||
const option = optionList[i];
|
||||
if (!option[fieldNames.value.options] || isChildren) {
|
||||
valueOptions.set(option[fieldNames.value.value], option);
|
||||
labelOptions.set(option[fieldNames.value.label], option);
|
||||
newValueOptions.set(option[fieldNames.value.value], option);
|
||||
newLabelOptions.set(option[fieldNames.value.label], option);
|
||||
} else {
|
||||
dig(option[fieldNames.value.options], true);
|
||||
}
|
||||
}
|
||||
}
|
||||
dig(mergedOptions);
|
||||
|
||||
return {
|
||||
options: mergedOptions,
|
||||
valueOptions,
|
||||
labelOptions,
|
||||
};
|
||||
dig(newOptions);
|
||||
mergedOptions.value = newOptions;
|
||||
valueOptions.value = newValueOptions;
|
||||
labelOptions.value = newLabelOptions;
|
||||
});
|
||||
return {
|
||||
options: mergedOptions,
|
||||
valueOptions,
|
||||
labelOptions,
|
||||
};
|
||||
}
|
|
@ -25,7 +25,7 @@ function convertNodeToOption<OptionType extends BaseOptionType = DefaultOptionTy
|
|||
}
|
||||
|
||||
export function convertChildrenToData<OptionType extends BaseOptionType = DefaultOptionType>(
|
||||
nodes: VueNode,
|
||||
nodes: VueNode[],
|
||||
optionOnly = false,
|
||||
): OptionType[] {
|
||||
const dd = flattenChildren(nodes as [])
|
Loading…
Reference in New Issue