refactor: select

refactor-cascader
tangjinzhou 2022-01-12 10:49:30 +08:00
parent ca17b5a928
commit bb91ce7592
54 changed files with 1090 additions and 5442 deletions

View File

@ -1,7 +1,7 @@
import type { FunctionalComponent } from 'vue'; import type { FunctionalComponent } from 'vue';
import type { OptionGroupData } from '../vc-select/interface'; import type { DefaultOptionType } from '../select';
export type OptGroupProps = Omit<OptionGroupData, 'options'>; export type OptGroupProps = Omit<DefaultOptionType, 'options'>;
export interface OptionGroupFC extends FunctionalComponent<OptGroupProps> { export interface OptionGroupFC extends FunctionalComponent<OptGroupProps> {
/** Legacy for check if is a Option Group */ /** Legacy for check if is a Option Group */

View File

@ -1,10 +1,10 @@
import type { App, PropType, Plugin, ExtractPropTypes } from 'vue'; import type { App, PropType, Plugin, ExtractPropTypes } from 'vue';
import { computed, defineComponent, ref } from 'vue'; import { computed, defineComponent, ref } from 'vue';
import classNames from '../_util/classNames'; import classNames from '../_util/classNames';
import type { BaseSelectRef } from '../vc-select2'; import type { BaseSelectRef } from '../vc-select';
import RcSelect, { selectProps as vcSelectProps, Option, OptGroup } from '../vc-select2'; import RcSelect, { selectProps as vcSelectProps, Option, OptGroup } from '../vc-select';
import type { BaseOptionType, DefaultOptionType } from '../vc-select2/Select'; import type { BaseOptionType, DefaultOptionType } from '../vc-select/Select';
import type { OptionProps } from '../vc-select2/Option'; import type { OptionProps } from '../vc-select/Option';
import getIcons from './utils/iconUtil'; import getIcons from './utils/iconUtil';
import PropTypes from '../_util/vue-types'; import PropTypes from '../_util/vue-types';
import useConfigInject from '../_util/hooks/useConfigInject'; import useConfigInject from '../_util/hooks/useConfigInject';

View File

@ -1,8 +1,8 @@
import type { FunctionalComponent } from 'vue'; import type { FunctionalComponent } from 'vue';
import type { OptionGroupData } from './interface'; import type { DefaultOptionType } from './Select';
export type OptGroupProps = Omit<OptionGroupData, 'options'>; export type OptGroupProps = Omit<DefaultOptionType, 'options'>;
export interface OptionGroupFC extends FunctionalComponent<OptGroupProps> { export interface OptionGroupFC extends FunctionalComponent<OptGroupProps> {
/** Legacy for check if is a Option Group */ /** Legacy for check if is a Option Group */

View File

@ -1,8 +1,8 @@
import type { FunctionalComponent } from 'vue'; import type { FunctionalComponent } from 'vue';
import type { OptionCoreData } from './interface'; import type { DefaultOptionType } from './Select';
export interface OptionProps extends Omit<OptionCoreData, 'label'> { export interface OptionProps extends Omit<DefaultOptionType, 'label'> {
/** Save for customize data */ /** Save for customize data */
[prop: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any [prop: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
} }

View File

@ -1,22 +1,12 @@
import TransBtn from './TransBtn'; import TransBtn from './TransBtn';
import PropTypes from '../_util/vue-types';
import KeyCode from '../_util/KeyCode'; import KeyCode from '../_util/KeyCode';
import classNames from '../_util/classNames'; import classNames from '../_util/classNames';
import pickAttrs from '../_util/pickAttrs'; import pickAttrs from '../_util/pickAttrs';
import { isValidElement } from '../_util/props-util'; import { isValidElement } from '../_util/props-util';
import createRef from '../_util/createRef'; import createRef from '../_util/createRef';
import type { PropType } from 'vue';
import { computed, defineComponent, nextTick, reactive, watch } from 'vue'; import { computed, defineComponent, nextTick, reactive, watch } from 'vue';
import List from '../vc-virtual-list'; import List from '../vc-virtual-list';
import type {
OptionsType as SelectOptionsType,
OptionData,
RenderNode,
OnActiveValue,
FieldNames,
} from './interface';
import type { RawValueType, FlattenOptionsType } from './interface/generator';
import { fillFieldNames } from './utils/valueUtil';
import useMemo from '../_util/hooks/useMemo'; import useMemo from '../_util/hooks/useMemo';
import { isPlatformMac } from './utils/platformUtil'; import { isPlatformMac } from './utils/platformUtil';
@ -28,78 +18,28 @@ export interface RefOptionListProps {
import type { EventHandler } from '../_util/EventInterface'; import type { EventHandler } from '../_util/EventInterface';
import omit from '../_util/omit'; import omit from '../_util/omit';
export interface OptionListProps<OptionType extends object> { import useBaseProps from './hooks/useBaseProps';
prefixCls: string; import type { RawValueType } from './Select';
id: string; import useSelectProps from './SelectContext';
options: OptionType[]; // export interface OptionListProps<OptionsType extends object[]> {
fieldNames?: FieldNames; export type OptionListProps = Record<string, never>;
flattenOptions: FlattenOptionsType<OptionType>;
height: number;
itemHeight: number;
values: Set<RawValueType>;
multiple: boolean;
open: boolean;
defaultActiveFirstOption?: boolean;
notFoundContent?: any;
menuItemSelectedIcon?: RenderNode;
childrenAsData: boolean;
searchValue: string;
virtual: boolean;
direction?: 'ltr' | 'rtl';
onSelect: (value: RawValueType, option: { selected: boolean }) => void;
onToggleOpen: (open?: boolean) => void;
/** Tell Select that some value is now active to make accessibility work */
onActiveValue: OnActiveValue;
onScroll: EventHandler;
/** Tell Select that mouse enter the popup to force re-render */
onMouseenter?: EventHandler;
}
const OptionListProps = {
prefixCls: PropTypes.string,
id: PropTypes.string,
options: PropTypes.array,
fieldNames: PropTypes.object,
flattenOptions: PropTypes.array,
height: PropTypes.number,
itemHeight: PropTypes.number,
values: PropTypes.any,
multiple: PropTypes.looseBool,
open: PropTypes.looseBool,
defaultActiveFirstOption: PropTypes.looseBool,
notFoundContent: PropTypes.any,
menuItemSelectedIcon: PropTypes.any,
childrenAsData: PropTypes.looseBool,
searchValue: PropTypes.string,
virtual: PropTypes.looseBool,
direction: PropTypes.string,
onSelect: PropTypes.func,
onToggleOpen: { type: Function as PropType<(open?: boolean) => void> },
/** Tell Select that some value is now active to make accessibility work */
onActiveValue: PropTypes.func,
onScroll: PropTypes.func,
/** Tell Select that mouse enter the popup to force re-render */
onMouseenter: PropTypes.func,
};
/** /**
* Using virtual list of option display. * Using virtual list of option display.
* Will fallback to dom if use customize render. * Will fallback to dom if use customize render.
*/ */
const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, { state?: any }>({ const OptionList = defineComponent({
name: 'OptionList', name: 'OptionList',
inheritAttrs: false, inheritAttrs: false,
slots: ['option'], slots: ['option'],
setup(props) { setup(_, { expose, slots }) {
const itemPrefixCls = computed(() => `${props.prefixCls}-item`); const baseProps = useBaseProps();
const props = useSelectProps();
const itemPrefixCls = computed(() => `${baseProps.prefixCls}-item`);
const memoFlattenOptions = useMemo( const memoFlattenOptions = useMemo(
() => props.flattenOptions, () => props.flattenOptions,
[() => props.open, () => props.flattenOptions], [() => baseProps.open, () => props.flattenOptions],
next => next[0], next => next[0],
); );
@ -124,7 +64,7 @@ const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, {
const current = (index + i * offset + len) % len; const current = (index + i * offset + len) % len;
const { group, data } = memoFlattenOptions.value[current]; const { group, data } = memoFlattenOptions.value[current];
if (!group && !(data as OptionData).disabled) { if (!group && !data.disabled) {
return current; return current;
} }
} }
@ -152,7 +92,7 @@ const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, {
// Auto active first item when list length or searchValue changed // Auto active first item when list length or searchValue changed
watch( watch(
[() => memoFlattenOptions.value.length, () => props.searchValue], [() => memoFlattenOptions.value.length, () => baseProps.searchValue],
() => { () => {
setActive(props.defaultActiveFirstOption !== false ? getEnabledActiveIndex(0) : -1); setActive(props.defaultActiveFirstOption !== false ? getEnabledActiveIndex(0) : -1);
}, },
@ -161,10 +101,10 @@ const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, {
// Auto scroll to item position in single mode // Auto scroll to item position in single mode
watch( watch(
[() => props.open, () => props.searchValue], [() => baseProps.open, () => baseProps.searchValue],
() => { () => {
if (!props.multiple && props.open && props.values.size === 1) { if (!baseProps.multiple && baseProps.open && props.rawValues.size === 1) {
const value = Array.from(props.values)[0]; const value = Array.from(props.rawValues)[0];
const index = memoFlattenOptions.value.findIndex(({ data }) => data.value === value); const index = memoFlattenOptions.value.findIndex(({ data }) => data.value === value);
if (index !== -1) { if (index !== -1) {
setActive(index); setActive(index);
@ -174,7 +114,7 @@ const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, {
} }
} }
// Force trigger scrollbar visible when open // Force trigger scrollbar visible when open
if (props.open) { if (baseProps.open) {
nextTick(() => { nextTick(() => {
listRef.current?.scrollTo(undefined); listRef.current?.scrollTo(undefined);
}); });
@ -186,262 +126,253 @@ const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, {
// ========================== Values ========================== // ========================== Values ==========================
const onSelectValue = (value?: RawValueType) => { const onSelectValue = (value?: RawValueType) => {
if (value !== undefined) { if (value !== undefined) {
props.onSelect(value, { selected: !props.values.has(value) }); props.onSelect(value, { selected: !props.rawValues.has(value) });
} }
// Single mode should always close by select // Single mode should always close by select
if (!props.multiple) { if (!baseProps.multiple) {
props.onToggleOpen(false); baseProps.toggleOpen(false);
} }
}; };
const getLabel = (item: Record<string, any>) => item.label;
function renderItem(index: number) { function renderItem(index: number) {
const item = memoFlattenOptions.value[index]; const item = memoFlattenOptions.value[index];
if (!item) return null; if (!item) return null;
const itemData = (item.data || {}) as OptionData; const itemData = item.data || {};
const { value, label, children } = itemData; const { value } = itemData;
const { group } = item;
const attrs = pickAttrs(itemData, true); const attrs = pickAttrs(itemData, true);
const mergedLabel = props.childrenAsData ? children : label; const mergedLabel = getLabel(item);
return item ? ( return item ? (
<div <div
aria-label={typeof mergedLabel === 'string' ? mergedLabel : undefined} aria-label={typeof mergedLabel === 'string' && !group ? mergedLabel : null}
{...attrs} {...attrs}
key={index} key={index}
role="option" role={group ? 'presentation' : 'option'}
id={`${props.id}_list_${index}`} id={`${baseProps.id}_list_${index}`}
aria-selected={props.values.has(value)} aria-selected={props.rawValues.has(value)}
> >
{value} {value}
</div> </div>
) : null; ) : null;
} }
return { const onKeydown = (event: KeyboardEvent) => {
memoFlattenOptions, const { which, ctrlKey } = event;
renderItem, switch (which) {
listRef, // >>> Arrow keys & ctrl + n/p on Mac
state, case KeyCode.N:
onListMouseDown, case KeyCode.P:
itemPrefixCls, case KeyCode.UP:
setActive, case KeyCode.DOWN: {
onSelectValue, let offset = 0;
onKeydown: (event: KeyboardEvent) => { if (which === KeyCode.UP) {
const { which, ctrlKey } = event; offset = -1;
switch (which) { } else if (which === KeyCode.DOWN) {
// >>> Arrow keys & ctrl + n/p on Mac offset = 1;
case KeyCode.N: } else if (isPlatformMac() && ctrlKey) {
case KeyCode.P: if (which === KeyCode.N) {
case KeyCode.UP:
case KeyCode.DOWN: {
let offset = 0;
if (which === KeyCode.UP) {
offset = -1;
} else if (which === KeyCode.DOWN) {
offset = 1; offset = 1;
} else if (isPlatformMac() && ctrlKey) { } else if (which === KeyCode.P) {
if (which === KeyCode.N) { offset = -1;
offset = 1;
} else if (which === KeyCode.P) {
offset = -1;
}
} }
if (offset !== 0) {
const nextActiveIndex = getEnabledActiveIndex(state.activeIndex + offset, offset);
scrollIntoView(nextActiveIndex);
setActive(nextActiveIndex, true);
}
break;
} }
// >>> Select if (offset !== 0) {
case KeyCode.ENTER: { const nextActiveIndex = getEnabledActiveIndex(state.activeIndex + offset, offset);
// value scrollIntoView(nextActiveIndex);
const item = memoFlattenOptions.value[state.activeIndex]; setActive(nextActiveIndex, true);
if (item && !item.data.disabled) {
onSelectValue(item.data.value);
} else {
onSelectValue(undefined);
}
if (props.open) {
event.preventDefault();
}
break;
} }
// >>> Close break;
case KeyCode.ESC: { }
props.onToggleOpen(false);
if (props.open) { // >>> Select
event.stopPropagation(); case KeyCode.ENTER: {
} // value
const item = memoFlattenOptions.value[state.activeIndex];
if (item && !item.data.disabled) {
onSelectValue(item.data.value);
} else {
onSelectValue(undefined);
}
if (baseProps.open) {
event.preventDefault();
}
break;
}
// >>> Close
case KeyCode.ESC: {
baseProps.toggleOpen(false);
if (baseProps.open) {
event.stopPropagation();
} }
} }
}, }
onKeyup: () => {},
scrollTo: (index: number) => {
scrollIntoView(index);
},
}; };
}, const onKeyup = () => {};
render() {
const { const scrollTo = (index: number) => {
renderItem, scrollIntoView(index);
listRef, };
onListMouseDown, expose({
itemPrefixCls, onKeydown,
setActive, onKeyup,
onSelectValue, scrollTo,
memoFlattenOptions, });
$slots, return () => {
} = this as any; // const {
const { // renderItem,
id, // listRef,
childrenAsData, // onListMouseDown,
values, // itemPrefixCls,
height, // setActive,
itemHeight, // onSelectValue,
menuItemSelectedIcon, // memoFlattenOptions,
notFoundContent, // $slots,
virtual, // } = this as any;
fieldNames, const { id, notFoundContent, onPopupScroll } = baseProps;
onScroll, const { menuItemSelectedIcon, rawValues, fieldNames, virtual, listHeight, listItemHeight } =
onMouseenter, props;
} = this.$props;
const renderOption = $slots.option; const renderOption = slots.option;
const { activeIndex } = this.state; const { activeIndex } = state;
const omitFieldNameList = Object.values(fillFieldNames(fieldNames)); const omitFieldNameList = Object.keys(fieldNames).map(key => fieldNames[key]);
// ========================== Render ========================== // ========================== Render ==========================
if (memoFlattenOptions.length === 0) { if (memoFlattenOptions.value.length === 0) {
return (
<div
role="listbox"
id={`${id}_list`}
class={`${itemPrefixCls.value}-empty`}
onMousedown={onListMouseDown}
>
{notFoundContent}
</div>
);
}
return ( return (
<div <>
role="listbox" <div role="listbox" id={`${id}_list`} style={{ height: 0, width: 0, overflow: 'hidden' }}>
id={`${id}_list`} {renderItem(activeIndex - 1)}
class={`${itemPrefixCls}-empty`} {renderItem(activeIndex)}
onMousedown={onListMouseDown} {renderItem(activeIndex + 1)}
> </div>
{notFoundContent} <List
</div> itemKey="key"
); ref={listRef}
} data={memoFlattenOptions.value}
return ( height={listHeight}
<> itemHeight={listItemHeight}
<div role="listbox" id={`${id}_list`} style={{ height: 0, width: 0, overflow: 'hidden' }}> fullHeight={false}
{renderItem(activeIndex - 1)} onMousedown={onListMouseDown}
{renderItem(activeIndex)} onScroll={onPopupScroll}
{renderItem(activeIndex + 1)} virtual={virtual}
</div> v-slots={{
<List default: (item, itemIndex) => {
itemKey="key" const { group, groupOption, data, label, value } = item;
ref={listRef} const { key } = data;
data={memoFlattenOptions} // Group
height={height} if (group) {
itemHeight={itemHeight} return (
fullHeight={false} <div class={classNames(itemPrefixCls.value, `${itemPrefixCls.value}-group`)}>
onMousedown={onListMouseDown} {renderOption ? renderOption(data) : label !== undefined ? label : key}
onScroll={onScroll} </div>
virtual={virtual} );
onMouseenter={onMouseenter} }
v-slots={{
default: ({ group, groupOption, data, label, value }, itemIndex) => { const {
const { key } = data; disabled,
// Group title,
if (group) { children,
style,
class: cls,
className,
...otherProps
} = data;
const passedProps = omit(otherProps, omitFieldNameList);
// Option
const selected = rawValues.has(value);
const optionPrefixCls = `${itemPrefixCls.value}-option`;
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);
const iconVisible =
!menuItemSelectedIcon || typeof menuItemSelectedIcon === 'function' || selected;
const content = mergedLabel || value;
// https://github.com/ant-design/ant-design/issues/26717
let optionTitle =
typeof content === 'string' || typeof content === 'number'
? content.toString()
: undefined;
if (title !== undefined) {
optionTitle = title;
}
return ( return (
<div class={classNames(itemPrefixCls, `${itemPrefixCls}-group`)}> <div
{renderOption ? renderOption(data) : label !== undefined ? label : key} {...passedProps}
aria-selected={selected}
class={optionClassName}
title={optionTitle}
onMousemove={e => {
if (otherProps.onMousemove) {
otherProps.onMousemove(e);
}
if (activeIndex === itemIndex || disabled) {
return;
}
setActive(itemIndex);
}}
onClick={e => {
if (!disabled) {
onSelectValue(value);
}
if (otherProps.onClick) {
otherProps.onClick(e);
}
}}
style={style}
>
<div class={`${optionPrefixCls}-content`}>
{renderOption ? renderOption(data) : content}
</div>
{isValidElement(menuItemSelectedIcon) || selected}
{iconVisible && (
<TransBtn
class={`${itemPrefixCls.value}-option-state`}
customizeIcon={menuItemSelectedIcon}
customizeIconProps={{ isSelected: selected }}
>
{selected ? '✓' : null}
</TransBtn>
)}
</div> </div>
); );
} },
}}
const { ></List>
disabled, </>
title, );
children, };
style,
class: cls,
className,
...otherProps
} = data;
const passedProps = omit(otherProps, omitFieldNameList);
// Option
const selected = values.has(value);
const optionPrefixCls = `${itemPrefixCls}-option`;
const optionClassName = classNames(itemPrefixCls, optionPrefixCls, cls, className, {
[`${optionPrefixCls}-grouped`]: groupOption,
[`${optionPrefixCls}-active`]: activeIndex === itemIndex && !disabled,
[`${optionPrefixCls}-disabled`]: disabled,
[`${optionPrefixCls}-selected`]: selected,
});
const mergedLabel = childrenAsData ? children : label;
const iconVisible =
!menuItemSelectedIcon || typeof menuItemSelectedIcon === 'function' || selected;
const content = mergedLabel || value;
// https://github.com/ant-design/ant-design/issues/26717
let optionTitle =
typeof content === 'string' || typeof content === 'number'
? content.toString()
: undefined;
if (title !== undefined) {
optionTitle = title;
}
return (
<div
{...passedProps}
aria-selected={selected}
class={optionClassName}
title={optionTitle}
onMousemove={e => {
if (otherProps.onMousemove) {
otherProps.onMousemove(e);
}
if (activeIndex === itemIndex || disabled) {
return;
}
setActive(itemIndex);
}}
onClick={e => {
if (!disabled) {
onSelectValue(value);
}
if (otherProps.onClick) {
otherProps.onClick(e);
}
}}
style={style}
>
<div class={`${optionPrefixCls}-content`}>
{renderOption ? renderOption(data) : content}
</div>
{isValidElement(menuItemSelectedIcon) || selected}
{iconVisible && (
<TransBtn
class={`${itemPrefixCls}-option-state`}
customizeIcon={menuItemSelectedIcon}
customizeIconProps={{ isSelected: selected }}
>
{selected ? '✓' : null}
</TransBtn>
)}
</div>
);
},
}}
></List>
</>
);
}, },
}); });
OptionList.props = OptionListProps;
export default OptionList; export default OptionList;

View File

@ -1,6 +1,6 @@
/** /**
* To match accessibility requirement, we always provide an input in the component. * To match accessibility requirement, we always provide an input in the component.
* Other element will not set `tabIndex` to avoid `onBlur` sequence problem. * Other element will not set `tabindex` to avoid `onBlur` sequence problem.
* For focused select, we set `aria-live="polite"` to update the accessibility content. * For focused select, we set `aria-live="polite"` to update the accessibility content.
* *
* ref: * ref:
@ -29,76 +29,614 @@
* - `combobox` mode not support `optionLabelProp` * - `combobox` mode not support `optionLabelProp`
*/ */
import type { OptionsType as SelectOptionsType } from './interface'; import BaseSelect, { baseSelectPropsWithoutPrivate, isMultiple } from './BaseSelect';
import SelectOptionList from './OptionList'; import type { DisplayValueType, BaseSelectRef, BaseSelectProps } from './BaseSelect';
import Option from './Option'; import OptionList from './OptionList';
import OptGroup from './OptGroup'; import useOptions from './hooks/useOptions';
import { convertChildrenToData as convertSelectChildrenToData } from './utils/legacyUtil'; import type { SelectContextProps } from './SelectContext';
import { import { useProvideSelectProps } from './SelectContext';
getLabeledValue as getSelectLabeledValue, import useId from './hooks/useId';
filterOptions as selectDefaultFilterOptions, import { fillFieldNames, flattenOptions, injectPropsWithOption } from './utils/valueUtil';
isValueDisabled as isSelectValueDisabled,
findValueOption as findSelectValueOption,
flattenOptions,
fillOptionsWithMissingValue,
} from './utils/valueUtil';
import type { SelectProps } from './generate';
import generateSelector, { selectBaseProps } from './generate';
import type { DefaultValueType } from './interface/generator';
import warningProps from './utils/warningPropsUtil'; import warningProps from './utils/warningPropsUtil';
import { defineComponent, ref } from 'vue'; 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 RefSelect = generateSelector<SelectOptionsType[number]>({ const OMIT_DOM_PROPS = ['inputValue'];
prefixCls: 'rc-select',
components: {
optionList: SelectOptionList as any,
},
convertChildrenToData: convertSelectChildrenToData,
flattenOptions,
getLabeledValue: getSelectLabeledValue,
filterOptions: selectDefaultFilterOptions,
isValueDisabled: isSelectValueDisabled,
findValueOption: findSelectValueOption,
warningProps,
fillOptionsWithMissingValue,
});
export type ExportedSelectProps<T extends DefaultValueType = DefaultValueType> = SelectProps< export type OnActiveValue = (
SelectOptionsType[number], active: RawValueType,
T index: number,
>; info?: { source?: 'keyboard' | 'mouse' },
) => void;
export function selectProps<T>() { export type OnInternalSelect = (value: RawValueType, info: { selected: boolean }) => void;
return selectBaseProps<SelectOptionsType[number], T>();
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;
} }
const Select = defineComponent({ 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', name: 'Select',
inheritAttrs: false, inheritAttrs: false,
Option, props: initDefaultProps(selectProps(), {
OptGroup, prefixCls: 'vc-select',
props: RefSelect.props, autoClearSearchValue: true,
setup(props, { attrs, expose, slots }) { listHeight: 200,
const selectRef = ref(); 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({ expose({
focus: () => { focus() {
selectRef.value?.focus(); selectRef.value?.focus();
}, },
blur: () => { blur() {
selectRef.value?.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 () => {
return ( return (
<RefSelect <BaseSelect
ref={selectRef} {...pickProps.value}
{...(props as any)}
{...attrs} {...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} v-slots={slots}
children={slots.default?.() || []}
/> />
); );
}; };
}, },
}); });
export default Select;

View File

@ -1,19 +1,12 @@
import Trigger from '../vc-trigger'; import Trigger from '../vc-trigger';
import PropTypes from '../_util/vue-types'; import PropTypes from '../_util/vue-types';
import { getSlot } from '../_util/props-util';
import classNames from '../_util/classNames'; import classNames from '../_util/classNames';
import createRef from '../_util/createRef';
import type { CSSProperties } from 'vue'; import type { CSSProperties } from 'vue';
import { defineComponent } from 'vue'; import { computed, ref, defineComponent } from 'vue';
import type { RenderDOMFunc } from './interface';
import type { DropdownRender } from './interface/generator';
import type { Placement } from './generate';
import type { VueNode } from '../_util/type'; import type { VueNode } from '../_util/type';
import type { DropdownRender, Placement, RenderDOMFunc } from './BaseSelect';
const getBuiltInPlacements = (dropdownMatchSelectWidth: number | boolean) => { const getBuiltInPlacements = (adjustX: number) => {
// Enable horizontal overflow auto-adjustment when a custom dropdown width is provided
const adjustX = typeof dropdownMatchSelectWidth !== 'number' ? 0 : 1;
return { return {
bottomLeft: { bottomLeft: {
points: ['tl', 'bl'], points: ['tl', 'bl'],
@ -49,6 +42,19 @@ const getBuiltInPlacements = (dropdownMatchSelectWidth: number | boolean) => {
}, },
}; };
}; };
const getAdjustX = (
adjustXDependencies: Pick<SelectTriggerProps, 'autoAdjustOverflow' | 'dropdownMatchSelectWidth'>,
) => {
const { autoAdjustOverflow, dropdownMatchSelectWidth } = adjustXDependencies;
if (!!autoAdjustOverflow) return 1;
// Enable horizontal overflow auto-adjustment when a custom dropdown width is provided
return typeof dropdownMatchSelectWidth !== 'number' ? 0 : 1;
};
export interface RefTriggerProps {
getPopupElement: () => HTMLDivElement;
}
export interface SelectTriggerProps { export interface SelectTriggerProps {
prefixCls: string; prefixCls: string;
disabled: boolean; disabled: boolean;
@ -66,103 +72,122 @@ export interface SelectTriggerProps {
getPopupContainer?: RenderDOMFunc; getPopupContainer?: RenderDOMFunc;
dropdownAlign: object; dropdownAlign: object;
empty: boolean; empty: boolean;
autoAdjustOverflow?: boolean;
getTriggerDOMNode: () => any; getTriggerDOMNode: () => any;
onPopupVisibleChange?: (visible: boolean) => void;
onPopupMouseEnter: () => void;
} }
const SelectTrigger = defineComponent<SelectTriggerProps, { popupRef: any }>({ const SelectTrigger = defineComponent<SelectTriggerProps, { popupRef: any }>({
name: 'SelectTrigger', name: 'SelectTrigger',
inheritAttrs: false, inheritAttrs: false,
created() { props: {
this.popupRef = createRef(); dropdownAlign: PropTypes.object,
}, visible: PropTypes.looseBool,
disabled: PropTypes.looseBool,
dropdownClassName: PropTypes.string,
dropdownStyle: PropTypes.object,
placement: PropTypes.string,
empty: PropTypes.looseBool,
autoAdjustOverflow: PropTypes.looseBool,
prefixCls: PropTypes.string,
popupClassName: PropTypes.string,
animation: PropTypes.string,
transitionName: PropTypes.string,
getPopupContainer: PropTypes.func,
dropdownRender: PropTypes.func,
containerWidth: PropTypes.number,
dropdownMatchSelectWidth: PropTypes.oneOfType([Number, Boolean]).def(true),
popupElement: PropTypes.any,
direction: PropTypes.string,
getTriggerDOMNode: PropTypes.func,
onPopupVisibleChange: PropTypes.func,
onPopupMouseEnter: PropTypes.func,
} as any,
setup(props, { slots, attrs, expose }) {
const builtInPlacements = computed(() => {
const { autoAdjustOverflow, dropdownMatchSelectWidth } = props;
return getBuiltInPlacements(
getAdjustX({
autoAdjustOverflow,
dropdownMatchSelectWidth,
}),
);
});
const popupRef = ref();
expose({
getPopupElement: () => {
return popupRef.value;
},
});
return () => {
const { empty = false, ...restProps } = { ...props, ...attrs };
const {
visible,
dropdownAlign,
prefixCls,
popupElement,
dropdownClassName,
dropdownStyle,
direction = 'ltr',
placement,
dropdownMatchSelectWidth,
containerWidth,
dropdownRender,
animation,
transitionName,
getPopupContainer,
getTriggerDOMNode,
onPopupVisibleChange,
onPopupMouseEnter,
} = restProps as SelectTriggerProps;
const dropdownPrefixCls = `${prefixCls}-dropdown`;
methods: { let popupNode = popupElement;
getPopupElement() { if (dropdownRender) {
return this.popupRef.current; popupNode = dropdownRender({ menuNode: popupElement, props });
}, }
},
render() { const mergedTransitionName = animation ? `${dropdownPrefixCls}-${animation}` : transitionName;
const { empty = false, ...props } = { ...this.$props, ...this.$attrs };
const {
visible,
dropdownAlign,
prefixCls,
popupElement,
dropdownClassName,
dropdownStyle,
direction = 'ltr',
placement,
dropdownMatchSelectWidth,
containerWidth,
dropdownRender,
animation,
transitionName,
getPopupContainer,
getTriggerDOMNode,
} = props as SelectTriggerProps;
const dropdownPrefixCls = `${prefixCls}-dropdown`;
let popupNode = popupElement; const popupStyle = { minWidth: `${containerWidth}px`, ...dropdownStyle };
if (dropdownRender) {
popupNode = dropdownRender({ menuNode: popupElement, props });
}
const builtInPlacements = getBuiltInPlacements(dropdownMatchSelectWidth); if (typeof dropdownMatchSelectWidth === 'number') {
popupStyle.width = `${dropdownMatchSelectWidth}px`;
const mergedTransitionName = animation ? `${dropdownPrefixCls}-${animation}` : transitionName; } else if (dropdownMatchSelectWidth) {
popupStyle.width = `${containerWidth}px`;
const popupStyle = { minWidth: `${containerWidth}px`, ...dropdownStyle }; }
return (
if (typeof dropdownMatchSelectWidth === 'number') { <Trigger
popupStyle.width = `${dropdownMatchSelectWidth}px`; {...props}
} else if (dropdownMatchSelectWidth) { showAction={[]}
popupStyle.width = `${containerWidth}px`; hideAction={[]}
} popupPlacement={placement || (direction === 'rtl' ? 'bottomRight' : 'bottomLeft')}
return ( builtinPlacements={builtInPlacements.value}
<Trigger prefixCls={dropdownPrefixCls}
{...props} popupTransitionName={mergedTransitionName}
showAction={[]} popupAlign={dropdownAlign}
hideAction={[]} popupVisible={visible}
popupPlacement={placement || (direction === 'rtl' ? 'bottomRight' : 'bottomLeft')} getPopupContainer={getPopupContainer}
builtinPlacements={builtInPlacements} popupClassName={classNames(dropdownClassName, {
prefixCls={dropdownPrefixCls} [`${dropdownPrefixCls}-empty`]: empty,
popupTransitionName={mergedTransitionName} })}
popup={<div ref={this.popupRef}>{popupNode}</div>} popupStyle={popupStyle}
popupAlign={dropdownAlign} getTriggerDOMNode={getTriggerDOMNode}
popupVisible={visible} onPopupVisibleChange={onPopupVisibleChange}
getPopupContainer={getPopupContainer} v-slots={{
popupClassName={classNames(dropdownClassName, { default: slots.default,
[`${dropdownPrefixCls}-empty`]: empty, popup: () => (
})} <div ref={popupRef} onMouseenter={onPopupMouseEnter}>
popupStyle={popupStyle} {popupNode}
getTriggerDOMNode={getTriggerDOMNode} </div>
> ),
{getSlot(this)[0]} }}
</Trigger> ></Trigger>
); );
};
}, },
}); });
SelectTrigger.props = {
dropdownAlign: PropTypes.object,
visible: PropTypes.looseBool,
disabled: PropTypes.looseBool,
dropdownClassName: PropTypes.string,
dropdownStyle: PropTypes.object,
placement: PropTypes.string,
empty: PropTypes.looseBool,
prefixCls: PropTypes.string,
popupClassName: PropTypes.string,
animation: PropTypes.string,
transitionName: PropTypes.string,
getPopupContainer: PropTypes.func,
dropdownRender: PropTypes.func,
containerWidth: PropTypes.number,
dropdownMatchSelectWidth: PropTypes.oneOfType([Number, Boolean]).def(true),
popupElement: PropTypes.any,
direction: PropTypes.string,
getTriggerDOMNode: PropTypes.func,
};
export default SelectTrigger; export default SelectTrigger;

View File

@ -16,7 +16,7 @@ interface InputProps {
autofocus: boolean; autofocus: boolean;
autocomplete: string; autocomplete: string;
editable: boolean; editable: boolean;
accessibilityIndex: number; activeDescendantId?: string;
value: string; value: string;
open: boolean; open: boolean;
tabindex: number | string; tabindex: number | string;
@ -45,7 +45,7 @@ const Input = defineComponent({
autofocus: PropTypes.looseBool, autofocus: PropTypes.looseBool,
autocomplete: PropTypes.string, autocomplete: PropTypes.string,
editable: PropTypes.looseBool, editable: PropTypes.looseBool,
accessibilityIndex: PropTypes.number, activeDescendantId: PropTypes.string,
value: PropTypes.string, value: PropTypes.string,
open: PropTypes.looseBool, open: PropTypes.looseBool,
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
@ -86,7 +86,7 @@ const Input = defineComponent({
autofocus, autofocus,
autocomplete, autocomplete,
editable, editable,
accessibilityIndex, activeDescendantId,
value, value,
onKeydown, onKeydown,
onMousedown, onMousedown,
@ -131,7 +131,7 @@ const Input = defineComponent({
'aria-owns': `${id}_list`, 'aria-owns': `${id}_list`,
'aria-autocomplete': 'list', 'aria-autocomplete': 'list',
'aria-controls': `${id}_list`, 'aria-controls': `${id}_list`,
'aria-activedescendant': `${id}_list_${accessibilityIndex}`, 'aria-activedescendant': activeDescendantId,
...attrs, ...attrs,
value: editable ? value : '', value: editable ? value : '',
readonly: !editable, readonly: !editable,

View File

@ -1,12 +1,4 @@
import TransBtn from '../TransBtn'; import TransBtn from '../TransBtn';
import type {
LabelValueType,
RawValueType,
CustomTagProps,
DefaultValueType,
DisplayLabelValueType,
} from '../interface/generator';
import type { RenderNode } from '../interface';
import type { InnerSelectorProps } from './interface'; import type { InnerSelectorProps } from './interface';
import Input from './Input'; import Input from './Input';
import type { Ref, PropType } from 'vue'; import type { Ref, PropType } from 'vue';
@ -16,6 +8,8 @@ import pickAttrs from '../../_util/pickAttrs';
import PropTypes from '../../_util/vue-types'; import PropTypes from '../../_util/vue-types';
import type { VueNode } from '../../_util/type'; import type { VueNode } from '../../_util/type';
import Overflow from '../../vc-overflow'; import Overflow from '../../vc-overflow';
import type { DisplayValueType, RenderNode, CustomTagProps, RawValueType } from '../BaseSelect';
import type { BaseOptionType } from '../Select';
type SelectorProps = InnerSelectorProps & { type SelectorProps = InnerSelectorProps & {
// Icon // Icon
@ -24,7 +18,7 @@ type SelectorProps = InnerSelectorProps & {
// Tags // Tags
maxTagCount?: number | 'responsive'; maxTagCount?: number | 'responsive';
maxTagTextLength?: number; maxTagTextLength?: number;
maxTagPlaceholder?: VueNode | ((omittedValues: LabelValueType[]) => VueNode); maxTagPlaceholder?: VueNode | ((omittedValues: DisplayValueType[]) => VueNode);
tokenSeparators?: string[]; tokenSeparators?: string[];
tagRender?: (props: CustomTagProps) => VueNode; tagRender?: (props: CustomTagProps) => VueNode;
onToggleOpen: any; onToggleOpen: any;
@ -33,7 +27,7 @@ type SelectorProps = InnerSelectorProps & {
choiceTransitionName?: string; choiceTransitionName?: string;
// Event // Event
onSelect: (value: RawValueType, option: { selected: boolean }) => void; onRemove: (value: DisplayValueType) => void;
}; };
const props = { const props = {
@ -49,7 +43,7 @@ const props = {
showSearch: PropTypes.looseBool, showSearch: PropTypes.looseBool,
autofocus: PropTypes.looseBool, autofocus: PropTypes.looseBool,
autocomplete: PropTypes.string, autocomplete: PropTypes.string,
accessibilityIndex: PropTypes.number, activeDescendantId: PropTypes.string,
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
removeIcon: PropTypes.any, removeIcon: PropTypes.any,
@ -58,12 +52,12 @@ const props = {
maxTagCount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), maxTagCount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
maxTagTextLength: PropTypes.number, maxTagTextLength: PropTypes.number,
maxTagPlaceholder: PropTypes.any.def( maxTagPlaceholder: PropTypes.any.def(
() => (omittedValues: LabelValueType[]) => `+ ${omittedValues.length} ...`, () => (omittedValues: DisplayValueType[]) => `+ ${omittedValues.length} ...`,
), ),
tagRender: PropTypes.func, tagRender: PropTypes.func,
onToggleOpen: { type: Function as PropType<(open?: boolean) => void> }, onToggleOpen: { type: Function as PropType<(open?: boolean) => void> },
onSelect: PropTypes.func, onRemove: PropTypes.func,
onInputChange: PropTypes.func, onInputChange: PropTypes.func,
onInputPaste: PropTypes.func, onInputPaste: PropTypes.func,
onInputKeyDown: PropTypes.func, onInputKeyDown: PropTypes.func,
@ -111,6 +105,7 @@ const SelectSelector = defineComponent<SelectorProps>({
// ===================== Render ====================== // ===================== Render ======================
// >>> Render Selector Node. Includes Item & Rest // >>> Render Selector Node. Includes Item & Rest
function defaultRenderSelector( function defaultRenderSelector(
title: VueNode,
content: VueNode, content: VueNode,
itemDisabled: boolean, itemDisabled: boolean,
closable?: boolean, closable?: boolean,
@ -122,9 +117,7 @@ const SelectSelector = defineComponent<SelectorProps>({
[`${selectionPrefixCls.value}-item-disabled`]: itemDisabled, [`${selectionPrefixCls.value}-item-disabled`]: itemDisabled,
})} })}
title={ title={
typeof content === 'string' || typeof content === 'number' typeof title === 'string' || typeof title === 'number' ? title.toString() : undefined
? content.toString()
: undefined
} }
> >
<span class={`${selectionPrefixCls.value}-item-content`}>{content}</span> <span class={`${selectionPrefixCls.value}-item-content`}>{content}</span>
@ -143,17 +136,17 @@ const SelectSelector = defineComponent<SelectorProps>({
} }
function customizeRenderSelector( function customizeRenderSelector(
value: DefaultValueType, value: RawValueType,
content: VueNode, content: VueNode,
itemDisabled: boolean, itemDisabled: boolean,
closable: boolean, closable: boolean,
onClose: (e: MouseEvent) => void, onClose: (e: MouseEvent) => void,
option: BaseOptionType,
) { ) {
const onMouseDown = (e: MouseEvent) => { const onMouseDown = (e: MouseEvent) => {
onPreventMouseDown(e); onPreventMouseDown(e);
props.onToggleOpen(!open); props.onToggleOpen(!open);
}; };
return ( return (
<span onMousedown={onMouseDown}> <span onMousedown={onMouseDown}>
{props.tagRender({ {props.tagRender({
@ -162,12 +155,14 @@ const SelectSelector = defineComponent<SelectorProps>({
disabled: itemDisabled, disabled: itemDisabled,
closable, closable,
onClose, onClose,
option,
})} })}
</span> </span>
); );
} }
function renderItem({ disabled: itemDisabled, label, value }: DisplayLabelValueType) { function renderItem(valueItem: DisplayValueType) {
const { disabled: itemDisabled, label, value, option } = valueItem;
const closable = !props.disabled && !itemDisabled; const closable = !props.disabled && !itemDisabled;
let displayLabel = label; let displayLabel = label;
@ -183,24 +178,22 @@ const SelectSelector = defineComponent<SelectorProps>({
} }
const onClose = (event?: MouseEvent) => { const onClose = (event?: MouseEvent) => {
if (event) event.stopPropagation(); if (event) event.stopPropagation();
props.onSelect(value, { selected: false }); props.onRemove?.(valueItem);
}; };
return typeof props.tagRender === 'function' return typeof props.tagRender === 'function'
? customizeRenderSelector(value, displayLabel, itemDisabled, closable, onClose) ? customizeRenderSelector(value, displayLabel, itemDisabled, closable, onClose, option)
: defaultRenderSelector(displayLabel, itemDisabled, closable, onClose); : defaultRenderSelector(label, displayLabel, itemDisabled, closable, onClose);
} }
function renderRest(omittedValues: DisplayLabelValueType[]) { function renderRest(omittedValues: DisplayValueType[]) {
const { const { maxTagPlaceholder = omittedValues => `+ ${omittedValues.length} ...` } = props;
maxTagPlaceholder = (omittedValues: LabelValueType[]) => `+ ${omittedValues.length} ...`,
} = props;
const content = const content =
typeof maxTagPlaceholder === 'function' typeof maxTagPlaceholder === 'function'
? maxTagPlaceholder(omittedValues) ? maxTagPlaceholder(omittedValues)
: maxTagPlaceholder; : maxTagPlaceholder;
return defaultRenderSelector(content, false); return defaultRenderSelector(content, content, false);
} }
return () => { return () => {
@ -214,7 +207,7 @@ const SelectSelector = defineComponent<SelectorProps>({
disabled, disabled,
autofocus, autofocus,
autocomplete, autocomplete,
accessibilityIndex, activeDescendantId,
tabindex, tabindex,
onInputChange, onInputChange,
onInputPaste, onInputPaste,
@ -241,7 +234,7 @@ const SelectSelector = defineComponent<SelectorProps>({
autofocus={autofocus} autofocus={autofocus}
autocomplete={autocomplete} autocomplete={autocomplete}
editable={inputEditable.value} editable={inputEditable.value}
accessibilityIndex={accessibilityIndex} activeDescendantId={activeDescendantId}
value={inputValue.value} value={inputValue.value}
onKeydown={onInputKeyDown} onKeydown={onInputKeyDown}
onMousedown={onInputMouseDown} onMousedown={onInputMouseDown}

View File

@ -9,7 +9,6 @@ import type { VueNode } from '../../_util/type';
interface SelectorProps extends InnerSelectorProps { interface SelectorProps extends InnerSelectorProps {
inputElement: VueNode; inputElement: VueNode;
activeValue: string; activeValue: string;
backfill?: boolean;
} }
const props = { const props = {
inputElement: PropTypes.any, inputElement: PropTypes.any,
@ -25,7 +24,7 @@ const props = {
showSearch: PropTypes.looseBool, showSearch: PropTypes.looseBool,
autofocus: PropTypes.looseBool, autofocus: PropTypes.looseBool,
autocomplete: PropTypes.string, autocomplete: PropTypes.string,
accessibilityIndex: PropTypes.number, activeDescendantId: PropTypes.string,
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
activeValue: PropTypes.string, activeValue: PropTypes.string,
backfill: PropTypes.looseBool, backfill: PropTypes.looseBool,
@ -64,7 +63,7 @@ const SingleSelector = defineComponent<SelectorProps>({
// Not show text when closed expect combobox mode // Not show text when closed expect combobox mode
const hasTextInput = computed(() => const hasTextInput = computed(() =>
props.mode !== 'combobox' && !props.open ? false : !!inputValue.value, props.mode !== 'combobox' && !props.open && !props.showSearch ? false : !!inputValue.value,
); );
const title = computed(() => { const title = computed(() => {
@ -74,6 +73,18 @@ const SingleSelector = defineComponent<SelectorProps>({
: undefined; : undefined;
}); });
const renderPlaceholder = () => {
if (props.values[0]) {
return null;
}
const hiddenStyle = hasTextInput.value ? { visibility: 'hidden' as const } : undefined;
return (
<span class={`${props.prefixCls}-selection-placeholder`} style={hiddenStyle}>
{props.placeholder}
</span>
);
};
return () => { return () => {
const { const {
inputElement, inputElement,
@ -84,9 +95,8 @@ const SingleSelector = defineComponent<SelectorProps>({
disabled, disabled,
autofocus, autofocus,
autocomplete, autocomplete,
accessibilityIndex, activeDescendantId,
open, open,
placeholder,
tabindex, tabindex,
onInputKeyDown, onInputKeyDown,
onInputMouseDown, onInputMouseDown,
@ -126,7 +136,7 @@ const SingleSelector = defineComponent<SelectorProps>({
autofocus={autofocus} autofocus={autofocus}
autocomplete={autocomplete} autocomplete={autocomplete}
editable={inputEditable.value} editable={inputEditable.value}
accessibilityIndex={accessibilityIndex} activeDescendantId={activeDescendantId}
value={inputValue.value} value={inputValue.value}
onKeydown={onInputKeyDown} onKeydown={onInputKeyDown}
onMousedown={onInputMouseDown} onMousedown={onInputMouseDown}
@ -150,9 +160,7 @@ const SingleSelector = defineComponent<SelectorProps>({
)} )}
{/* Display placeholder */} {/* Display placeholder */}
{!item && !hasTextInput.value && ( {renderPlaceholder()}
<span class={`${prefixCls}-selection-placeholder`}>{placeholder}</span>
)}
</> </>
); );
}; };

View File

@ -11,8 +11,8 @@
import KeyCode from '../../_util/KeyCode'; import KeyCode from '../../_util/KeyCode';
import MultipleSelector from './MultipleSelector'; import MultipleSelector from './MultipleSelector';
import SingleSelector from './SingleSelector'; import SingleSelector from './SingleSelector';
import type { LabelValueType, RawValueType, CustomTagProps } from '../interface/generator'; import type { CustomTagProps, DisplayValueType, Mode, RenderNode } from '../BaseSelect';
import type { RenderNode, Mode } from '../interface'; import { isValidateOpenKey } from '../utils/keyUtil';
import useLock from '../hooks/useLock'; import useLock from '../hooks/useLock';
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
@ -20,22 +20,22 @@ import createRef from '../../_util/createRef';
import PropTypes from '../../_util/vue-types'; import PropTypes from '../../_util/vue-types';
import type { VueNode } from '../../_util/type'; import type { VueNode } from '../../_util/type';
import type { EventHandler } from '../../_util/EventInterface'; import type { EventHandler } from '../../_util/EventInterface';
import type { ScrollTo } from '../../vc-virtual-list/List';
export interface SelectorProps { export interface SelectorProps {
id: string; id: string;
prefixCls: string; prefixCls: string;
showSearch?: boolean; showSearch?: boolean;
open: boolean; open: boolean;
/** Display in the Selector value, it's not same as `value` prop */ values: DisplayValueType[];
values: LabelValueType[]; multiple?: boolean;
multiple: boolean;
mode: Mode; mode: Mode;
searchValue: string; searchValue: string;
activeValue: string; activeValue: string;
inputElement: VueNode; inputElement: VueNode;
autofocus?: boolean; autofocus?: boolean;
accessibilityIndex: number; activeDescendantId?: string;
tabindex?: number | string; tabindex?: number | string;
disabled?: boolean; disabled?: boolean;
placeholder?: VueNode; placeholder?: VueNode;
@ -44,7 +44,7 @@ export interface SelectorProps {
// Tags // Tags
maxTagCount?: number | 'responsive'; maxTagCount?: number | 'responsive';
maxTagTextLength?: number; maxTagTextLength?: number;
maxTagPlaceholder?: VueNode | ((omittedValues: LabelValueType[]) => VueNode); maxTagPlaceholder?: VueNode | ((omittedValues: DisplayValueType[]) => VueNode);
tagRender?: (props: CustomTagProps) => VueNode; tagRender?: (props: CustomTagProps) => VueNode;
/** Check if `tokenSeparators` contains `\n` or `\r\n` */ /** Check if `tokenSeparators` contains `\n` or `\r\n` */
@ -57,7 +57,7 @@ export interface SelectorProps {
/** `onSearch` returns go next step boolean to check if need do toggle open */ /** `onSearch` returns go next step boolean to check if need do toggle open */
onSearch: (searchText: string, fromTyping: boolean, isCompositing: boolean) => boolean; onSearch: (searchText: string, fromTyping: boolean, isCompositing: boolean) => boolean;
onSearchSubmit: (searchText: string) => void; onSearchSubmit: (searchText: string) => void;
onSelect: (value: RawValueType, option: { selected: boolean }) => void; onRemove: (value: DisplayValueType) => void;
onInputKeyDown?: (e: KeyboardEvent) => void; onInputKeyDown?: (e: KeyboardEvent) => void;
/** /**
@ -66,6 +66,11 @@ export interface SelectorProps {
*/ */
domRef: () => HTMLDivElement; domRef: () => HTMLDivElement;
} }
export interface RefSelectorProps {
focus: () => void;
blur: () => void;
scrollTo?: ScrollTo;
}
const Selector = defineComponent<SelectorProps>({ const Selector = defineComponent<SelectorProps>({
name: 'Selector', name: 'Selector',
@ -84,7 +89,7 @@ const Selector = defineComponent<SelectorProps>({
inputElement: PropTypes.any, inputElement: PropTypes.any,
autofocus: PropTypes.looseBool, autofocus: PropTypes.looseBool,
accessibilityIndex: PropTypes.number, activeDescendantId: PropTypes.string,
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
disabled: PropTypes.looseBool, disabled: PropTypes.looseBool,
placeholder: PropTypes.any, placeholder: PropTypes.any,
@ -106,7 +111,7 @@ const Selector = defineComponent<SelectorProps>({
/** `onSearch` returns go next step boolean to check if need do toggle open */ /** `onSearch` returns go next step boolean to check if need do toggle open */
onSearch: PropTypes.func, onSearch: PropTypes.func,
onSearchSubmit: PropTypes.func, onSearchSubmit: PropTypes.func,
onSelect: PropTypes.func, onRemove: PropTypes.func,
onInputKeyDown: { type: Function as PropType<EventHandler> }, onInputKeyDown: { type: Function as PropType<EventHandler> },
/** /**
@ -115,7 +120,7 @@ const Selector = defineComponent<SelectorProps>({
*/ */
domRef: PropTypes.func, domRef: PropTypes.func,
} as any, } as any,
setup(props) { setup(props, { expose }) {
const inputRef = createRef(); const inputRef = createRef();
let compositionStatus = false; let compositionStatus = false;
@ -139,7 +144,7 @@ const Selector = defineComponent<SelectorProps>({
props.onSearchSubmit((event.target as HTMLInputElement).value); props.onSearchSubmit((event.target as HTMLInputElement).value);
} }
if (![KeyCode.SHIFT, KeyCode.TAB, KeyCode.BACKSPACE, KeyCode.ESC].includes(which)) { if (isValidateOpenKey(which)) {
props.onToggleOpen(true); props.onToggleOpen(true);
} }
}; };
@ -227,58 +232,44 @@ const Selector = defineComponent<SelectorProps>({
props.onToggleOpen(); props.onToggleOpen();
} }
}; };
expose({
return {
focus: () => { focus: () => {
inputRef.current.focus(); inputRef.current.focus();
}, },
blur: () => { blur: () => {
inputRef.current.blur(); inputRef.current.blur();
}, },
onMousedown, });
onClick,
onInputPaste, return () => {
inputRef, const { prefixCls, domRef, mode } = props as SelectorProps;
onInternalInputKeyDown, const sharedProps = {
onInternalInputMouseDown, inputRef,
onInputChange, onInputKeyDown: onInternalInputKeyDown,
onInputCompositionEnd, onInputMouseDown: onInternalInputMouseDown,
onInputCompositionStart, onInputChange,
onInputPaste,
onInputCompositionStart,
onInputCompositionEnd,
};
const selectNode =
mode === 'multiple' || mode === 'tags' ? (
<MultipleSelector {...props} {...sharedProps} />
) : (
<SingleSelector {...props} {...sharedProps} />
);
return (
<div
ref={domRef}
class={`${prefixCls}-selector`}
onClick={onClick}
onMousedown={onMousedown}
>
{selectNode}
</div>
);
}; };
}, },
render() {
const { prefixCls, domRef, multiple } = this.$props as SelectorProps;
const {
onMousedown,
onClick,
inputRef,
onInputPaste,
onInternalInputKeyDown,
onInternalInputMouseDown,
onInputChange,
onInputCompositionStart,
onInputCompositionEnd,
} = this as any;
const sharedProps = {
inputRef,
onInputKeyDown: onInternalInputKeyDown,
onInputMouseDown: onInternalInputMouseDown,
onInputChange,
onInputPaste,
onInputCompositionStart,
onInputCompositionEnd,
};
const selectNode = multiple ? (
<MultipleSelector {...this.$props} {...sharedProps} />
) : (
<SingleSelector {...this.$props} {...sharedProps} />
);
return (
<div ref={domRef} class={`${prefixCls}-selector`} onClick={onClick} onMousedown={onMousedown}>
{selectNode}
</div>
);
},
}); });
export default Selector; export default Selector;

View File

@ -1,8 +1,8 @@
import type { RefObject } from '../../_util/createRef'; import type { RefObject } from '../../_util/createRef';
import type { Mode } from '../interface';
import type { LabelValueType } from '../interface/generator';
import type { EventHandler } from '../../_util/EventInterface'; import type { EventHandler } from '../../_util/EventInterface';
import type { VueNode } from '../../_util/type'; import type { VueNode } from '../../_util/type';
import type { Mode, DisplayValueType } from '../BaseSelect';
export interface InnerSelectorProps { export interface InnerSelectorProps {
prefixCls: string; prefixCls: string;
@ -13,10 +13,10 @@ export interface InnerSelectorProps {
disabled?: boolean; disabled?: boolean;
autofocus?: boolean; autofocus?: boolean;
autocomplete?: string; autocomplete?: string;
values: LabelValueType[]; values: DisplayValueType[];
showSearch?: boolean; showSearch?: boolean;
searchValue: string; searchValue: string;
accessibilityIndex: number; activeDescendantId: string;
open: boolean; open: boolean;
tabindex?: number | string; tabindex?: number | string;
onInputKeyDown: EventHandler; onInputKeyDown: EventHandler;

View File

@ -1,10 +1,11 @@
import type { FunctionalComponent } from 'vue'; import type { FunctionalComponent } from 'vue';
import type { VueNode } from '../_util/type'; import type { VueNode } from '../_util/type';
import PropTypes from '../_util/vue-types'; import PropTypes from '../_util/vue-types';
import type { RenderNode } from './BaseSelect';
export interface TransBtnProps { export interface TransBtnProps {
class: string; class: string;
customizeIcon: VueNode | ((props?: any) => VueNode); customizeIcon: RenderNode;
customizeIconProps?: any; customizeIconProps?: any;
onMousedown?: (payload: MouseEvent) => void; onMousedown?: (payload: MouseEvent) => void;
onClick?: (payload: MouseEvent) => void; onClick?: (payload: MouseEvent) => void;

View File

@ -1,345 +0,0 @@
@select-prefix: ~'rc-select';
* {
box-sizing: border-box;
}
.search-input-without-border() {
.@{select-prefix}-selection-search-input {
border: none;
outline: none;
background: rgba(255, 0, 0, 0.2);
width: 100%;
}
}
.@{select-prefix} {
display: inline-block;
font-size: 12px;
width: 100px;
position: relative;
&-disabled {
&,
& input {
cursor: not-allowed;
}
.@{select-prefix}-selector {
opacity: 0.3;
}
}
&-show-arrow&-loading {
.@{select-prefix}-arrow {
&-icon::after {
box-sizing: border-box;
width: 12px;
height: 12px;
border-radius: 100%;
border: 2px solid #999;
border-top-color: transparent;
border-bottom-color: transparent;
transform: none;
margin-top: 4px;
animation: rcSelectLoadingIcon 0.5s infinite;
}
}
}
// ============== Selector ===============
.@{select-prefix}-selection-placeholder {
opacity: 0.4;
}
// ============== Search ===============
.@{select-prefix}-selection-search-input {
appearance: none;
&::-webkit-search-cancel-button {
display: none;
appearance: none;
}
}
// --------------- Single ----------------
&-single {
.@{select-prefix}-selector {
display: flex;
position: relative;
.@{select-prefix}-selection-search {
width: 100%;
&-input {
width: 100%;
}
}
.@{select-prefix}-selection-item,
.@{select-prefix}-selection-placeholder {
position: absolute;
top: 1px;
left: 3px;
pointer-events: none;
}
}
// Not customize
&:not(.@{select-prefix}-customize-input) {
.@{select-prefix}-selector {
padding: 1px;
border: 1px solid #000;
.search-input-without-border();
}
}
}
// -------------- Multiple ---------------
&-multiple .@{select-prefix}-selector {
display: flex;
flex-wrap: wrap;
padding: 1px;
border: 1px solid #000;
.@{select-prefix}-selection-item {
flex: none;
background: #bbb;
border-radius: 4px;
margin-right: 2px;
padding: 0 8px;
&-disabled {
cursor: not-allowed;
opacity: 0.5;
}
}
.@{select-prefix}-selection-search {
position: relative;
&-input,
&-mirror {
padding: 1px;
font-family: system-ui;
}
&-mirror {
position: absolute;
z-index: 999;
white-space: nowrap;
position: none;
left: 0;
top: 0;
visibility: hidden;
}
}
.search-input-without-border();
}
// ================ Icons ================
&-allow-clear {
&.@{select-prefix}-multiple .@{select-prefix}-selector {
padding-right: 20px;
}
.@{select-prefix}-clear {
position: absolute;
right: 20px;
top: 0;
}
}
&-show-arrow {
&.@{select-prefix}-multiple .@{select-prefix}-selector {
padding-right: 20px;
}
.@{select-prefix}-arrow {
pointer-events: none;
position: absolute;
right: 5px;
top: 0;
&-icon::after {
content: '';
border: 5px solid transparent;
width: 0;
height: 0;
display: inline-block;
border-top-color: #999;
transform: translateY(5px);
}
}
}
// =============== Focused ===============
&-focused {
.@{select-prefix}-selector {
border-color: blue !important;
}
}
// ============== Dropdown ===============
&-dropdown {
border: 1px solid green;
min-height: 100px;
position: absolute;
background: #fff;
&-hidden {
display: none;
}
}
// =============== Option ================
&-item {
font-size: 16px;
line-height: 1.5;
padding: 4px 16px;
// >>> Group
&-group {
color: #999;
font-weight: bold;
font-size: 80%;
}
// >>> Option
&-option {
position: relative;
&-grouped {
padding-left: 24px;
}
.@{select-prefix}-item-option-state {
position: absolute;
right: 0;
top: 4px;
pointer-events: none;
}
// ------- Active -------
&-active {
background: green;
}
// ------ Disabled ------
&-disabled {
color: #999;
}
}
// >>> Empty
&-empty {
text-align: center;
color: #999;
}
}
}
.@{select-prefix}-selection__choice-zoom {
transition: all 0.3s;
}
.@{select-prefix}-selection__choice-zoom-appear {
opacity: 0;
transform: scale(0.5);
&&-active {
opacity: 1;
transform: scale(1);
}
}
.@{select-prefix}-selection__choice-zoom-leave {
opacity: 1;
transform: scale(1);
&&-active {
opacity: 0;
transform: scale(0.5);
}
}
.effect() {
animation-duration: 0.3s;
animation-fill-mode: both;
transform-origin: 0 0;
}
.@{select-prefix}-dropdown {
&-slide-up-enter,
&-slide-up-appear {
.effect();
opacity: 0;
animation-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1);
animation-play-state: paused;
}
&-slide-up-leave {
.effect();
opacity: 1;
animation-timing-function: cubic-bezier(0.6, 0.04, 0.98, 0.34);
animation-play-state: paused;
}
&-slide-up-enter&-slide-up-enter-active&-placement-bottomLeft,
&-slide-up-appear&-slide-up-appear-active&-placement-bottomLeft {
animation-name: rcSelectDropdownSlideUpIn;
animation-play-state: running;
}
&-slide-up-leave&-slide-up-leave-active&-placement-bottomLeft {
animation-name: rcSelectDropdownSlideUpOut;
animation-play-state: running;
}
&-slide-up-enter&-slide-up-enter-active&-placement-topLeft,
&-slide-up-appear&-slide-up-appear-active&-placement-topLeft {
animation-name: rcSelectDropdownSlideDownIn;
animation-play-state: running;
}
&-slide-up-leave&-slide-up-leave-active&-placement-topLeft {
animation-name: rcSelectDropdownSlideDownOut;
animation-play-state: running;
}
}
@keyframes rcSelectDropdownSlideUpIn {
0% {
opacity: 0;
transform-origin: 0% 0%;
transform: scaleY(0);
}
100% {
opacity: 1;
transform-origin: 0% 0%;
transform: scaleY(1);
}
}
@keyframes rcSelectDropdownSlideUpOut {
0% {
opacity: 1;
transform-origin: 0% 0%;
transform: scaleY(1);
}
100% {
opacity: 0;
transform-origin: 0% 0%;
transform: scaleY(0);
}
}
@keyframes rcSelectLoadingIcon {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,36 +0,0 @@
import type { ComputedRef, Ref } from 'vue';
import { computed } from 'vue';
import type { DisplayLabelValueType } from '../interface/generator';
export default function useCacheDisplayValue(
values: Ref<DisplayLabelValueType[]>,
): ComputedRef<DisplayLabelValueType[]> {
let prevValues = [...values.value];
const mergedValues = computed(() => {
// Create value - label map
const valueLabels = new Map();
prevValues.forEach(({ value, label }) => {
if (value !== label) {
valueLabels.set(value, label);
}
});
const resultValues = values.value.map(item => {
const cacheLabel = valueLabels.get(item.value);
if (item.isCacheable && cacheLabel) {
return {
...item,
label: cacheLabel,
};
}
return item;
});
prevValues = resultValues;
return resultValues;
});
return mergedValues;
}

View File

@ -1,26 +0,0 @@
import type { Ref } from 'vue';
import { computed } from 'vue';
import type { RawValueType, FlattenOptionsType, Key } from '../interface/generator';
export default function useCacheOptions<
OptionType extends {
value?: RawValueType;
label?: any;
key?: Key;
disabled?: boolean;
},
>(options: Ref) {
const optionMap = computed(() => {
const map: Map<RawValueType, FlattenOptionsType<OptionType>[number]> = new Map();
options.value.forEach(item => {
const { value } = item;
map.set(value, item);
});
return map;
});
const getValueOption = (valueList: RawValueType[]) =>
valueList.map(value => optionMap.value.get(value)).filter(Boolean);
return getValueOption;
}

View File

@ -1,11 +1,12 @@
import type { ExportedSelectProps } from './Select'; import type { SelectProps } from './Select';
import Select, { selectProps } from './Select'; import Select, { selectProps } from './Select';
import Option from './Option'; import Option from './Option';
import OptGroup from './OptGroup'; import OptGroup from './OptGroup';
import { selectBaseProps } from './generate'; import BaseSelect from './BaseSelect';
import type { ExtractPropTypes } from 'vue'; import type { BaseSelectProps, BaseSelectRef, BaseSelectPropsWithoutPrivate } from './BaseSelect';
import useBaseProps from './hooks/useBaseProps';
export type SelectProps<T = any> = Partial<ExtractPropTypes<ExportedSelectProps<T>>>; export { Option, OptGroup, selectProps, BaseSelect, useBaseProps };
export { Option, OptGroup, selectBaseProps, selectProps }; export type { BaseSelectProps, BaseSelectRef, BaseSelectPropsWithoutPrivate, SelectProps };
export default Select; export default Select;

View File

@ -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;

View File

@ -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;
}

View File

@ -1,11 +1,3 @@
import type {
RawValueType,
GetLabeledValue,
LabelValueType,
DefaultValueType,
FlattenOptionsType,
} from '../interface/generator';
export function toArray<T>(value: T | T[]): T[] { export function toArray<T>(value: T | T[]): T[] {
if (Array.isArray(value)) { if (Array.isArray(value)) {
return value; return value;
@ -13,116 +5,8 @@ export function toArray<T>(value: T | T[]): T[] {
return value !== undefined ? [value] : []; return value !== undefined ? [value] : [];
} }
/**
* Convert outer props value into internal value
*/
export function toInnerValue(
value: DefaultValueType,
{ labelInValue, combobox }: { labelInValue: boolean; combobox: boolean },
): [RawValueType[], Map<RawValueType, LabelValueType>] {
const valueMap = new Map<RawValueType, LabelValueType>();
if (value === undefined || (value === '' && combobox)) {
return [[], valueMap];
}
const values = Array.isArray(value) ? value : [value];
let rawValues = values as RawValueType[];
if (labelInValue) {
rawValues = (values as LabelValueType[])
.filter(item => item !== null)
.map((itemValue: LabelValueType) => {
const { key, value: val } = itemValue;
const finalVal = val !== undefined ? val : key;
valueMap.set(finalVal, itemValue);
return finalVal;
});
}
return [rawValues, valueMap];
}
/**
* Convert internal value into out event value
*/
export function toOuterValues<FOT extends FlattenOptionsType>(
valueList: RawValueType[],
{
optionLabelProp,
labelInValue,
prevValueMap,
options,
getLabeledValue,
}: {
optionLabelProp: string;
labelInValue: boolean;
getLabeledValue: GetLabeledValue<FOT>;
options: FOT;
prevValueMap: Map<RawValueType, LabelValueType>;
},
): RawValueType[] | LabelValueType[] | DefaultValueType {
let values: DefaultValueType = valueList;
if (labelInValue) {
values = values.map(val =>
getLabeledValue(val, {
options,
prevValueMap,
labelInValue,
optionLabelProp,
}),
);
}
return values;
}
export function removeLastEnabledValue<
T extends { disabled?: boolean },
P extends RawValueType | object,
>(measureValues: T[], values: P[]): { values: P[]; removedValue: P } {
const newValues = [...values];
let removeIndex: number;
for (removeIndex = measureValues.length - 1; removeIndex >= 0; removeIndex -= 1) {
if (!measureValues[removeIndex].disabled) {
break;
}
}
let removedValue = null;
if (removeIndex !== -1) {
removedValue = newValues[removeIndex];
newValues.splice(removeIndex, 1);
}
return {
values: newValues,
removedValue,
};
}
export const isClient = export const isClient =
typeof window !== 'undefined' && window.document && window.document.documentElement; typeof window !== 'undefined' && window.document && window.document.documentElement;
/** Is client side and not jsdom */ /** Is client side and not jsdom */
export const isBrowserClient = process.env.NODE_ENV !== 'test' && isClient; export const isBrowserClient = process.env.NODE_ENV !== 'test' && isClient;
let uuid = 0;
/** Get unique id for accessibility usage */
export function getUUID(): number | string {
let retId: string | number;
// Test never reach
/* istanbul ignore if */
if (isBrowserClient) {
retId = uuid;
uuid += 1;
} else {
retId = 'TEST_OR_SSR';
}
return retId;
}

View File

@ -1,9 +1,11 @@
import { flattenChildren, isValidElement } from '../../_util/props-util'; import { flattenChildren, isValidElement } from '../../_util/props-util';
import type { VNode } from 'vue'; import type { VNode } from 'vue';
import type { OptionData, OptionGroupData, OptionsType } from '../interface'; import type { BaseOptionType, DefaultOptionType } from '../Select';
import type { VueNode } from '../../_util/type'; import type { VueNode } from '../../_util/type';
function convertNodeToOption(node: VNode): OptionData { function convertNodeToOption<OptionType extends BaseOptionType = DefaultOptionType>(
node: VNode,
): OptionType {
const { const {
key, key,
children, children,
@ -18,13 +20,16 @@ function convertNodeToOption(node: VNode): OptionData {
value: value !== undefined ? value : key, value: value !== undefined ? value : key,
children: child, children: child,
disabled: disabled || disabled === '', // support <a-select-option disabled /> disabled: disabled || disabled === '', // support <a-select-option disabled />
...(restProps as Omit<typeof restProps, 'key'>), ...(restProps as any),
}; };
} }
export function convertChildrenToData(nodes: VueNode, optionOnly = false): OptionsType { export function convertChildrenToData<OptionType extends BaseOptionType = DefaultOptionType>(
nodes: VueNode[],
optionOnly = false,
): OptionType[] {
const dd = flattenChildren(nodes as []) const dd = flattenChildren(nodes as [])
.map((node: VNode, index: number): OptionData | OptionGroupData | null => { .map((node: VNode, index: number): OptionType | null => {
if (!isValidElement(node) || !node.type) { if (!isValidElement(node) || !node.type) {
return null; return null;
} }

View File

@ -1,24 +1,8 @@
import type { BaseOptionType, DefaultOptionType, RawValueType, FieldNames } from '../Select';
import { warning } from '../../vc-util/warning'; import { warning } from '../../vc-util/warning';
import { cloneVNode, isVNode } from 'vue'; import type { FlattenOptionData } from '../interface';
import type {
OptionsType as SelectOptionsType,
OptionData,
OptionGroupData,
FlattenOptionData,
FieldNames,
} from '../interface';
import type {
LabelValueType,
FilterFunc,
RawValueType,
GetLabeledValue,
DefaultValueType,
} from '../interface/generator';
import { toArray } from './commonUtil'; function getKey(data: BaseOptionType, index: number) {
import type { VueNode } from '../../_util/type';
function getKey(data: OptionData | OptionGroupData, index: number) {
const { key } = data; const { key } = data;
let value: RawValueType; let value: RawValueType;
@ -35,11 +19,11 @@ function getKey(data: OptionData | OptionGroupData, index: number) {
return `rc-index-key-${index}`; return `rc-index-key-${index}`;
} }
export function fillFieldNames(fieldNames?: FieldNames) { export function fillFieldNames(fieldNames: FieldNames | undefined, childrenAsData: boolean) {
const { label, value, options } = fieldNames || {}; const { label, value, options } = fieldNames || {};
return { return {
label: label || 'label', label: label || (childrenAsData ? 'children' : 'label'),
value: value || 'value', value: value || 'value',
options: options || 'options', options: options || 'options',
}; };
@ -50,38 +34,43 @@ export function fillFieldNames(fieldNames?: FieldNames) {
* We use `optionOnly` here is aim to avoid user use nested option group. * We use `optionOnly` here is aim to avoid user use nested option group.
* Here is simply set `key` to the index if not provided. * Here is simply set `key` to the index if not provided.
*/ */
export function flattenOptions( export function flattenOptions<OptionType extends BaseOptionType = DefaultOptionType>(
options: SelectOptionsType, options: OptionType[],
{ fieldNames }: { fieldNames?: FieldNames } = {}, { fieldNames, childrenAsData }: { fieldNames?: FieldNames; childrenAsData?: boolean } = {},
): FlattenOptionData[] { ): FlattenOptionData<OptionType>[] {
const flattenList: FlattenOptionData[] = []; const flattenList: FlattenOptionData<OptionType>[] = [];
const { const {
label: fieldLabel, label: fieldLabel,
value: fieldValue, value: fieldValue,
options: fieldOptions, options: fieldOptions,
} = fillFieldNames(fieldNames); } = fillFieldNames(fieldNames, false);
function dig(list: SelectOptionsType, isGroupOption: boolean) { function dig(list: OptionType[], isGroupOption: boolean) {
list.forEach(data => { list.forEach(data => {
const label = data[fieldLabel]; const label = data[fieldLabel];
if (isGroupOption || !(fieldOptions in data)) { if (isGroupOption || !(fieldOptions in data)) {
const value = data[fieldValue];
// Option // Option
flattenList.push({ flattenList.push({
key: getKey(data, flattenList.length), key: getKey(data, flattenList.length),
groupOption: isGroupOption, groupOption: isGroupOption,
data, data,
label, label,
value: data[fieldValue], value,
}); });
} else { } else {
let grpLabel = label;
if (grpLabel === undefined && childrenAsData) {
grpLabel = data.label;
}
// Option Group // Option Group
flattenList.push({ flattenList.push({
key: getKey(data, flattenList.length), key: getKey(data, flattenList.length),
group: true, group: true,
data, data,
label, label: grpLabel,
}); });
dig(data[fieldOptions], true); dig(data[fieldOptions], true);
@ -97,7 +86,7 @@ export function flattenOptions(
/** /**
* Inject `props` into `option` for legacy usage * Inject `props` into `option` for legacy usage
*/ */
function injectPropsWithOption<T>(option: T): T { export function injectPropsWithOption<T>(option: T): T {
const newOption = { ...option }; const newOption = { ...option };
if (!('props' in newOption)) { if (!('props' in newOption)) {
Object.defineProperty(newOption, 'props', { Object.defineProperty(newOption, 'props', {
@ -114,154 +103,6 @@ function injectPropsWithOption<T>(option: T): T {
return newOption; return newOption;
} }
export function findValueOption(
values: RawValueType[],
options: FlattenOptionData[],
{ prevValueOptions = [] }: { prevValueOptions?: OptionData[] } = {},
): OptionData[] {
const optionMap: Map<RawValueType, OptionData> = new Map();
options.forEach(({ data, group, value }) => {
if (!group) {
// Check if match
optionMap.set(value, data as OptionData);
}
});
return values.map(val => {
let option = optionMap.get(val);
// Fallback to try to find prev options
if (!option) {
option = {
// eslint-disable-next-line no-underscore-dangle
...prevValueOptions.find(opt => opt._INTERNAL_OPTION_VALUE_ === val),
};
}
return injectPropsWithOption(option);
});
}
export const getLabeledValue: GetLabeledValue<FlattenOptionData[]> = (
value,
{ options, prevValueMap, labelInValue, optionLabelProp },
) => {
const item = findValueOption([value], options)[0];
const result: LabelValueType = {
value,
};
const prevValItem: LabelValueType = labelInValue ? prevValueMap.get(value) : undefined;
if (prevValItem && typeof prevValItem === 'object' && 'label' in prevValItem) {
result.label = prevValItem.label;
if (
item &&
typeof prevValItem.label === 'string' &&
typeof item[optionLabelProp] === 'string' &&
prevValItem.label.trim() !== item[optionLabelProp].trim()
) {
warning(false, '`label` of `value` is not same as `label` in Select options.');
}
} else if (item && optionLabelProp in item) {
if (Array.isArray(item[optionLabelProp])) {
result.label = isVNode(item[optionLabelProp][0])
? cloneVNode(item[optionLabelProp][0])
: item[optionLabelProp];
} else {
result.label = item[optionLabelProp];
}
} else {
result.label = value;
result.isCacheable = true;
}
// Used for motion control
result.key = result.value;
return result;
};
function toRawString(content: VueNode): string {
return toArray(content)
.map(item => {
if (isVNode(item)) {
return item?.el?.innerText || item?.el?.wholeText;
} else {
return item;
}
})
.join('');
}
/** Filter single option if match the search text */
function getFilterFunction(optionFilterProp: string) {
return (searchValue: string, option: OptionData | OptionGroupData) => {
const lowerSearchText = searchValue.toLowerCase();
// Group label search
if ('options' in option) {
return toRawString(option.label).toLowerCase().includes(lowerSearchText);
}
// Option value search
const rawValue = option[optionFilterProp];
const value = toRawString(rawValue).toLowerCase();
return value.includes(lowerSearchText);
};
}
/** Filter options and return a new options by the search text */
export function filterOptions(
searchValue: string,
options: SelectOptionsType,
{
optionFilterProp,
filterOption,
}: { optionFilterProp: string; filterOption: boolean | FilterFunc<SelectOptionsType[number]> },
) {
const filteredOptions: SelectOptionsType = [];
let filterFunc: FilterFunc<SelectOptionsType[number]>;
if (filterOption === false) {
return [...options];
}
if (typeof filterOption === 'function') {
filterFunc = filterOption;
} else {
filterFunc = getFilterFunction(optionFilterProp);
}
options.forEach(item => {
// Group should check child options
if ('options' in item) {
// Check group first
const matchGroup = filterFunc(searchValue, item);
if (matchGroup) {
filteredOptions.push(item);
} else {
// Check option
const subOptions = item.options.filter(subItem => filterFunc(searchValue, subItem));
if (subOptions.length) {
filteredOptions.push({
...item,
options: subOptions,
});
}
}
return;
}
if (filterFunc(searchValue, injectPropsWithOption(item))) {
filteredOptions.push(item);
}
});
return filteredOptions;
}
export function getSeparatedContent(text: string, tokens: string[]): string[] { export function getSeparatedContent(text: string, tokens: string[]): string[] {
if (!tokens || !tokens.length) { if (!tokens || !tokens.length) {
return null; return null;
@ -285,53 +126,3 @@ export function getSeparatedContent(text: string, tokens: string[]): string[] {
const list = separate(text, tokens); const list = separate(text, tokens);
return match ? list : null; return match ? list : null;
} }
export function isValueDisabled(value: RawValueType, options: FlattenOptionData[]): boolean {
const option = findValueOption([value], options)[0];
return option.disabled;
}
/**
* `tags` mode should fill un-list item into the option list
*/
export function fillOptionsWithMissingValue(
options: SelectOptionsType,
value: DefaultValueType,
optionLabelProp: string,
labelInValue: boolean,
): SelectOptionsType {
const values = toArray<RawValueType | LabelValueType>(value).slice().sort();
const cloneOptions = [...options];
// Convert options value to set
const optionValues = new Set<RawValueType>();
options.forEach(opt => {
if (opt.options) {
opt.options.forEach((subOpt: OptionData) => {
optionValues.add(subOpt.value);
});
} else {
optionValues.add((opt as OptionData).value);
}
});
// Fill missing value
values.forEach(item => {
const val: RawValueType = labelInValue
? (item as LabelValueType).value
: (item as RawValueType);
if (!optionValues.has(val)) {
cloneOptions.push(
labelInValue
? {
[optionLabelProp]: (item as LabelValueType).label,
value: val,
}
: { value: val },
);
}
});
return cloneOptions;
}

View File

@ -1,10 +1,10 @@
import warning, { noteOnce } from '../../vc-util/warning'; import warning, { noteOnce } from '../../vc-util/warning';
import type { SelectProps } from '..';
import { convertChildrenToData } from './legacyUtil'; import { convertChildrenToData } from './legacyUtil';
import { toArray } from './commonUtil'; import { toArray } from './commonUtil';
import type { RawValueType, LabelValueType } from '../interface/generator';
import { isValidElement } from '../../_util/props-util'; import { isValidElement } from '../../_util/props-util';
import type { VNode } from 'vue'; import type { VNode } from 'vue';
import type { RawValueType, LabelInValueType, SelectProps } from '../Select';
import { isMultiple } from '../BaseSelect';
function warningProps(props: SelectProps) { function warningProps(props: SelectProps) {
const { const {
@ -25,13 +25,13 @@ function warningProps(props: SelectProps) {
optionLabelProp, optionLabelProp,
} = props; } = props;
const multiple = mode === 'multiple' || mode === 'tags'; const multiple = isMultiple(mode);
const mergedShowSearch = showSearch !== undefined ? showSearch : multiple || mode === 'combobox'; const mergedShowSearch = showSearch !== undefined ? showSearch : multiple || mode === 'combobox';
const mergedOptions = options || convertChildrenToData(children); const mergedOptions = options || convertChildrenToData(children);
// `tags` should not set option as disabled // `tags` should not set option as disabled
warning( warning(
mode !== 'tags' || mergedOptions.every((opt: any) => !opt.disabled), mode !== 'tags' || mergedOptions.every((opt: { disabled?: boolean }) => !opt.disabled),
'Please avoid setting option to disabled in tags mode since user can always type text as tag.', 'Please avoid setting option to disabled in tags mode since user can always type text as tag.',
); );
@ -67,7 +67,7 @@ function warningProps(props: SelectProps) {
); );
if (value !== undefined && value !== null) { if (value !== undefined && value !== null) {
const values = toArray<RawValueType | LabelValueType>(value); const values = toArray<RawValueType | LabelInValueType>(value);
warning( warning(
!labelInValue || !labelInValue ||
values.every(val => typeof val === 'object' && ('key' in val || 'value' in val)), values.every(val => typeof val === 'object' && ('key' in val || 'value' in val)),

View File

@ -1,15 +0,0 @@
import type { FunctionalComponent } from 'vue';
import type { DefaultOptionType } from './Select';
export type OptGroupProps = Omit<DefaultOptionType, 'options'>;
export interface OptionGroupFC extends FunctionalComponent<OptGroupProps> {
/** Legacy for check if is a Option Group */
isSelectOptGroup: boolean;
}
const OptGroup: OptionGroupFC = () => null;
OptGroup.isSelectOptGroup = true;
OptGroup.displayName = 'ASelectOptGroup';
export default OptGroup;

View File

@ -1,18 +0,0 @@
import type { FunctionalComponent } from 'vue';
import type { DefaultOptionType } from './Select';
export interface OptionProps extends Omit<DefaultOptionType, 'label'> {
/** Save for customize data */
[prop: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
}
export interface OptionFC extends FunctionalComponent<OptionProps> {
/** Legacy for check if is a Option Group */
isSelectOption: boolean;
}
const Option: OptionFC = () => null;
Option.isSelectOption = true;
Option.displayName = 'ASelectOption';
export default Option;

View File

@ -1,378 +0,0 @@
import TransBtn from './TransBtn';
import KeyCode from '../_util/KeyCode';
import classNames from '../_util/classNames';
import pickAttrs from '../_util/pickAttrs';
import { isValidElement } from '../_util/props-util';
import createRef from '../_util/createRef';
import { computed, defineComponent, nextTick, reactive, watch } from 'vue';
import List from '../vc-virtual-list';
import useMemo from '../_util/hooks/useMemo';
import { isPlatformMac } from './utils/platformUtil';
export interface RefOptionListProps {
onKeydown: (e?: KeyboardEvent) => void;
onKeyup: (e?: KeyboardEvent) => void;
scrollTo?: (index: number) => void;
}
import type { EventHandler } from '../_util/EventInterface';
import omit from '../_util/omit';
import useBaseProps from './hooks/useBaseProps';
import type { RawValueType } from './Select';
import useSelectProps from './SelectContext';
// export interface OptionListProps<OptionsType extends object[]> {
export type OptionListProps = Record<string, never>;
/**
* Using virtual list of option display.
* Will fallback to dom if use customize render.
*/
const OptionList = defineComponent({
name: 'OptionList',
inheritAttrs: false,
slots: ['option'],
setup(_, { expose, slots }) {
const baseProps = useBaseProps();
const props = useSelectProps();
const itemPrefixCls = computed(() => `${baseProps.prefixCls}-item`);
const memoFlattenOptions = useMemo(
() => props.flattenOptions,
[() => baseProps.open, () => props.flattenOptions],
next => next[0],
);
// =========================== List ===========================
const listRef = createRef();
const onListMouseDown: EventHandler = event => {
event.preventDefault();
};
const scrollIntoView = (index: number) => {
if (listRef.current) {
listRef.current.scrollTo({ index });
}
};
// ========================== Active ==========================
const getEnabledActiveIndex = (index: number, offset = 1) => {
const len = memoFlattenOptions.value.length;
for (let i = 0; i < len; i += 1) {
const current = (index + i * offset + len) % len;
const { group, data } = memoFlattenOptions.value[current];
if (!group && !data.disabled) {
return current;
}
}
return -1;
};
const state = reactive({
activeIndex: getEnabledActiveIndex(0),
});
const setActive = (index: number, fromKeyboard = false) => {
state.activeIndex = index;
const info = { source: fromKeyboard ? ('keyboard' as const) : ('mouse' as const) };
// Trigger active event
const flattenItem = memoFlattenOptions.value[index];
if (!flattenItem) {
props.onActiveValue(null, -1, info);
return;
}
props.onActiveValue(flattenItem.data.value, index, info);
};
// Auto active first item when list length or searchValue changed
watch(
[() => memoFlattenOptions.value.length, () => baseProps.searchValue],
() => {
setActive(props.defaultActiveFirstOption !== false ? getEnabledActiveIndex(0) : -1);
},
{ immediate: true },
);
// Auto scroll to item position in single mode
watch(
[() => baseProps.open, () => baseProps.searchValue],
() => {
if (!baseProps.multiple && baseProps.open && props.rawValues.size === 1) {
const value = Array.from(props.rawValues)[0];
const index = memoFlattenOptions.value.findIndex(({ data }) => data.value === value);
if (index !== -1) {
setActive(index);
nextTick(() => {
scrollIntoView(index);
});
}
}
// Force trigger scrollbar visible when open
if (baseProps.open) {
nextTick(() => {
listRef.current?.scrollTo(undefined);
});
}
},
{ immediate: true, flush: 'post' },
);
// ========================== Values ==========================
const onSelectValue = (value?: RawValueType) => {
if (value !== undefined) {
props.onSelect(value, { selected: !props.rawValues.has(value) });
}
// Single mode should always close by select
if (!baseProps.multiple) {
baseProps.toggleOpen(false);
}
};
const getLabel = (item: Record<string, any>) => item.label;
function renderItem(index: number) {
const item = memoFlattenOptions.value[index];
if (!item) return null;
const itemData = item.data || {};
const { value } = itemData;
const { group } = item;
const attrs = pickAttrs(itemData, true);
const mergedLabel = getLabel(item);
return item ? (
<div
aria-label={typeof mergedLabel === 'string' && !group ? mergedLabel : null}
{...attrs}
key={index}
role={group ? 'presentation' : 'option'}
id={`${baseProps.id}_list_${index}`}
aria-selected={props.rawValues.has(value)}
>
{value}
</div>
) : null;
}
const onKeydown = (event: KeyboardEvent) => {
const { which, ctrlKey } = event;
switch (which) {
// >>> Arrow keys & ctrl + n/p on Mac
case KeyCode.N:
case KeyCode.P:
case KeyCode.UP:
case KeyCode.DOWN: {
let offset = 0;
if (which === KeyCode.UP) {
offset = -1;
} else if (which === KeyCode.DOWN) {
offset = 1;
} else if (isPlatformMac() && ctrlKey) {
if (which === KeyCode.N) {
offset = 1;
} else if (which === KeyCode.P) {
offset = -1;
}
}
if (offset !== 0) {
const nextActiveIndex = getEnabledActiveIndex(state.activeIndex + offset, offset);
scrollIntoView(nextActiveIndex);
setActive(nextActiveIndex, true);
}
break;
}
// >>> Select
case KeyCode.ENTER: {
// value
const item = memoFlattenOptions.value[state.activeIndex];
if (item && !item.data.disabled) {
onSelectValue(item.data.value);
} else {
onSelectValue(undefined);
}
if (baseProps.open) {
event.preventDefault();
}
break;
}
// >>> Close
case KeyCode.ESC: {
baseProps.toggleOpen(false);
if (baseProps.open) {
event.stopPropagation();
}
}
}
};
const onKeyup = () => {};
const scrollTo = (index: number) => {
scrollIntoView(index);
};
expose({
onKeydown,
onKeyup,
scrollTo,
});
return () => {
// const {
// renderItem,
// listRef,
// onListMouseDown,
// itemPrefixCls,
// setActive,
// onSelectValue,
// memoFlattenOptions,
// $slots,
// } = this as any;
const { id, notFoundContent, onPopupScroll } = baseProps;
const { menuItemSelectedIcon, rawValues, fieldNames, virtual, listHeight, listItemHeight } =
props;
const renderOption = slots.option;
const { activeIndex } = state;
const omitFieldNameList = Object.keys(fieldNames).map(key => fieldNames[key]);
// ========================== Render ==========================
if (memoFlattenOptions.value.length === 0) {
return (
<div
role="listbox"
id={`${id}_list`}
class={`${itemPrefixCls.value}-empty`}
onMousedown={onListMouseDown}
>
{notFoundContent}
</div>
);
}
return (
<>
<div role="listbox" id={`${id}_list`} style={{ height: 0, width: 0, overflow: 'hidden' }}>
{renderItem(activeIndex - 1)}
{renderItem(activeIndex)}
{renderItem(activeIndex + 1)}
</div>
<List
itemKey="key"
ref={listRef}
data={memoFlattenOptions.value}
height={listHeight}
itemHeight={listItemHeight}
fullHeight={false}
onMousedown={onListMouseDown}
onScroll={onPopupScroll}
virtual={virtual}
v-slots={{
default: (item, itemIndex) => {
const { group, groupOption, data, label, value } = item;
const { key } = data;
// Group
if (group) {
return (
<div class={classNames(itemPrefixCls.value, `${itemPrefixCls.value}-group`)}>
{renderOption ? renderOption(data) : label !== undefined ? label : key}
</div>
);
}
const {
disabled,
title,
children,
style,
class: cls,
className,
...otherProps
} = data;
const passedProps = omit(otherProps, omitFieldNameList);
// Option
const selected = rawValues.has(value);
const optionPrefixCls = `${itemPrefixCls.value}-option`;
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);
const iconVisible =
!menuItemSelectedIcon || typeof menuItemSelectedIcon === 'function' || selected;
const content = mergedLabel || value;
// https://github.com/ant-design/ant-design/issues/26717
let optionTitle =
typeof content === 'string' || typeof content === 'number'
? content.toString()
: undefined;
if (title !== undefined) {
optionTitle = title;
}
return (
<div
{...passedProps}
aria-selected={selected}
class={optionClassName}
title={optionTitle}
onMousemove={e => {
if (otherProps.onMousemove) {
otherProps.onMousemove(e);
}
if (activeIndex === itemIndex || disabled) {
return;
}
setActive(itemIndex);
}}
onClick={e => {
if (!disabled) {
onSelectValue(value);
}
if (otherProps.onClick) {
otherProps.onClick(e);
}
}}
style={style}
>
<div class={`${optionPrefixCls}-content`}>
{renderOption ? renderOption(data) : content}
</div>
{isValidElement(menuItemSelectedIcon) || selected}
{iconVisible && (
<TransBtn
class={`${itemPrefixCls.value}-option-state`}
customizeIcon={menuItemSelectedIcon}
customizeIconProps={{ isSelected: selected }}
>
{selected ? '✓' : null}
</TransBtn>
)}
</div>
);
},
}}
></List>
</>
);
};
},
});
export default OptionList;

View File

@ -1,642 +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, 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}
/>
);
};
},
});

View File

@ -1,193 +0,0 @@
import Trigger from '../vc-trigger';
import PropTypes from '../_util/vue-types';
import classNames from '../_util/classNames';
import type { CSSProperties } from 'vue';
import { computed, ref, defineComponent } from 'vue';
import type { VueNode } from '../_util/type';
import type { DropdownRender, Placement, RenderDOMFunc } from './BaseSelect';
const getBuiltInPlacements = (adjustX: number) => {
return {
bottomLeft: {
points: ['tl', 'bl'],
offset: [0, 4],
overflow: {
adjustX,
adjustY: 1,
},
},
bottomRight: {
points: ['tr', 'br'],
offset: [0, 4],
overflow: {
adjustX,
adjustY: 1,
},
},
topLeft: {
points: ['bl', 'tl'],
offset: [0, -4],
overflow: {
adjustX,
adjustY: 1,
},
},
topRight: {
points: ['br', 'tr'],
offset: [0, -4],
overflow: {
adjustX,
adjustY: 1,
},
},
};
};
const getAdjustX = (
adjustXDependencies: Pick<SelectTriggerProps, 'autoAdjustOverflow' | 'dropdownMatchSelectWidth'>,
) => {
const { autoAdjustOverflow, dropdownMatchSelectWidth } = adjustXDependencies;
if (!!autoAdjustOverflow) return 1;
// Enable horizontal overflow auto-adjustment when a custom dropdown width is provided
return typeof dropdownMatchSelectWidth !== 'number' ? 0 : 1;
};
export interface RefTriggerProps {
getPopupElement: () => HTMLDivElement;
}
export interface SelectTriggerProps {
prefixCls: string;
disabled: boolean;
visible: boolean;
popupElement: VueNode;
animation?: string;
transitionName?: string;
containerWidth: number;
placement?: Placement;
dropdownStyle: CSSProperties;
dropdownClassName: string;
direction: string;
dropdownMatchSelectWidth?: boolean | number;
dropdownRender?: DropdownRender;
getPopupContainer?: RenderDOMFunc;
dropdownAlign: object;
empty: boolean;
autoAdjustOverflow?: boolean;
getTriggerDOMNode: () => any;
onPopupVisibleChange?: (visible: boolean) => void;
onPopupMouseEnter: () => void;
}
const SelectTrigger = defineComponent<SelectTriggerProps, { popupRef: any }>({
name: 'SelectTrigger',
inheritAttrs: false,
props: {
dropdownAlign: PropTypes.object,
visible: PropTypes.looseBool,
disabled: PropTypes.looseBool,
dropdownClassName: PropTypes.string,
dropdownStyle: PropTypes.object,
placement: PropTypes.string,
empty: PropTypes.looseBool,
autoAdjustOverflow: PropTypes.looseBool,
prefixCls: PropTypes.string,
popupClassName: PropTypes.string,
animation: PropTypes.string,
transitionName: PropTypes.string,
getPopupContainer: PropTypes.func,
dropdownRender: PropTypes.func,
containerWidth: PropTypes.number,
dropdownMatchSelectWidth: PropTypes.oneOfType([Number, Boolean]).def(true),
popupElement: PropTypes.any,
direction: PropTypes.string,
getTriggerDOMNode: PropTypes.func,
onPopupVisibleChange: PropTypes.func,
onPopupMouseEnter: PropTypes.func,
} as any,
setup(props, { slots, attrs, expose }) {
const builtInPlacements = computed(() => {
const { autoAdjustOverflow, dropdownMatchSelectWidth } = props;
return getBuiltInPlacements(
getAdjustX({
autoAdjustOverflow,
dropdownMatchSelectWidth,
}),
);
});
const popupRef = ref();
expose({
getPopupElement: () => {
return popupRef.value;
},
});
return () => {
const { empty = false, ...restProps } = { ...props, ...attrs };
const {
visible,
dropdownAlign,
prefixCls,
popupElement,
dropdownClassName,
dropdownStyle,
direction = 'ltr',
placement,
dropdownMatchSelectWidth,
containerWidth,
dropdownRender,
animation,
transitionName,
getPopupContainer,
getTriggerDOMNode,
onPopupVisibleChange,
onPopupMouseEnter,
} = restProps as SelectTriggerProps;
const dropdownPrefixCls = `${prefixCls}-dropdown`;
let popupNode = popupElement;
if (dropdownRender) {
popupNode = dropdownRender({ menuNode: popupElement, props });
}
const mergedTransitionName = animation ? `${dropdownPrefixCls}-${animation}` : transitionName;
const popupStyle = { minWidth: `${containerWidth}px`, ...dropdownStyle };
if (typeof dropdownMatchSelectWidth === 'number') {
popupStyle.width = `${dropdownMatchSelectWidth}px`;
} else if (dropdownMatchSelectWidth) {
popupStyle.width = `${containerWidth}px`;
}
return (
<Trigger
{...props}
showAction={[]}
hideAction={[]}
popupPlacement={placement || (direction === 'rtl' ? 'bottomRight' : 'bottomLeft')}
builtinPlacements={builtInPlacements.value}
prefixCls={dropdownPrefixCls}
popupTransitionName={mergedTransitionName}
popupAlign={dropdownAlign}
popupVisible={visible}
getPopupContainer={getPopupContainer}
popupClassName={classNames(dropdownClassName, {
[`${dropdownPrefixCls}-empty`]: empty,
})}
popupStyle={popupStyle}
getTriggerDOMNode={getTriggerDOMNode}
onPopupVisibleChange={onPopupVisibleChange}
v-slots={{
default: slots.default,
popup: () => (
<div ref={popupRef} onMouseenter={onPopupMouseEnter}>
{popupNode}
</div>
),
}}
></Trigger>
);
};
},
});
export default SelectTrigger;

View File

@ -1,218 +0,0 @@
import { cloneElement } from '../../_util/vnode';
import type { VNode } from 'vue';
import { defineComponent, getCurrentInstance, inject, onMounted, withDirectives } from 'vue';
import PropTypes from '../../_util/vue-types';
import type { RefObject } from '../../_util/createRef';
import antInput from '../../_util/antInputDirective';
import classNames from '../../_util/classNames';
import type { EventHandler } from '../../_util/EventInterface';
import type { VueNode } from '../../_util/type';
interface InputProps {
prefixCls: string;
id: string;
inputElement: VueNode;
disabled: boolean;
autofocus: boolean;
autocomplete: string;
editable: boolean;
activeDescendantId?: string;
value: string;
open: boolean;
tabindex: number | string;
/** Pass accessibility props to input */
attrs: object;
inputRef: RefObject;
onKeydown: EventHandler;
onMousedown: EventHandler;
onChange: EventHandler;
onPaste: EventHandler;
onCompositionstart: EventHandler;
onCompositionend: EventHandler;
onFocus: EventHandler;
onBlur: EventHandler;
}
const Input = defineComponent({
name: 'Input',
inheritAttrs: false,
props: {
inputRef: PropTypes.any,
prefixCls: PropTypes.string,
id: PropTypes.string,
inputElement: PropTypes.any,
disabled: PropTypes.looseBool,
autofocus: PropTypes.looseBool,
autocomplete: PropTypes.string,
editable: PropTypes.looseBool,
activeDescendantId: PropTypes.string,
value: PropTypes.string,
open: PropTypes.looseBool,
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
/** Pass accessibility props to input */
attrs: PropTypes.object,
onKeydown: PropTypes.func,
onMousedown: PropTypes.func,
onChange: PropTypes.func,
onPaste: PropTypes.func,
onCompositionstart: PropTypes.func,
onCompositionend: PropTypes.func,
onFocus: PropTypes.func,
onBlur: PropTypes.func,
},
setup(props) {
if (process.env.NODE_ENV === 'test') {
onMounted(() => {
const ins = getCurrentInstance();
if (props.autofocus) {
if (ins.vnode && ins.vnode.el) {
ins.vnode.el.focus();
}
}
});
}
return {
blurTimeout: null,
VCSelectContainerEvent: inject('VCSelectContainerEvent') as any,
};
},
render() {
const {
prefixCls,
id,
inputElement,
disabled,
tabindex,
autofocus,
autocomplete,
editable,
activeDescendantId,
value,
onKeydown,
onMousedown,
onChange,
onPaste,
onCompositionstart,
onCompositionend,
onFocus,
onBlur,
open,
inputRef,
attrs,
} = this.$props as InputProps;
let inputNode: any = inputElement || withDirectives((<input />) as VNode, [[antInput]]);
const inputProps = inputNode.props || {};
const {
onKeydown: onOriginKeyDown,
onInput: onOriginInput,
onFocus: onOriginFocus,
onBlur: onOriginBlur,
onMousedown: onOriginMouseDown,
onCompositionstart: onOriginCompositionStart,
onCompositionend: onOriginCompositionEnd,
style,
} = inputProps;
inputNode = cloneElement(
inputNode,
Object.assign(
{
id,
ref: inputRef,
disabled,
tabindex,
autocomplete: autocomplete || 'off',
autofocus,
class: classNames(`${prefixCls}-selection-search-input`, inputNode?.props?.className),
style: { ...style, opacity: editable ? null : 0 },
role: 'combobox',
'aria-expanded': open,
'aria-haspopup': 'listbox',
'aria-owns': `${id}_list`,
'aria-autocomplete': 'list',
'aria-controls': `${id}_list`,
'aria-activedescendant': activeDescendantId,
...attrs,
value: editable ? value : '',
readonly: !editable,
unselectable: !editable ? 'on' : null,
onKeydown: (event: KeyboardEvent) => {
onKeydown(event);
if (onOriginKeyDown) {
onOriginKeyDown(event);
}
},
onMousedown: (event: MouseEvent) => {
onMousedown(event);
if (onOriginMouseDown) {
onOriginMouseDown(event);
}
},
onInput: (event: Event) => {
onChange(event);
if (onOriginInput) {
onOriginInput(event);
}
},
onCompositionstart(event: CompositionEvent) {
onCompositionstart(event);
if (onOriginCompositionStart) {
onOriginCompositionStart(event);
}
},
onCompositionend(event: CompositionEvent) {
onCompositionend(event);
if (onOriginCompositionEnd) {
onOriginCompositionEnd(event);
}
},
onPaste,
onFocus: (...args: any[]) => {
clearTimeout(this.blurTimeout);
onOriginFocus && onOriginFocus(args[0]);
onFocus && onFocus(args[0]);
this.VCSelectContainerEvent?.focus(args[0]);
},
onBlur: (...args: any[]) => {
this.blurTimeout = setTimeout(() => {
onOriginBlur && onOriginBlur(args[0]);
onBlur && onBlur(args[0]);
this.VCSelectContainerEvent?.blur(args[0]);
}, 200);
},
},
inputNode.type === 'textarea' ? {} : { type: 'search' },
),
true,
true,
) as VNode;
return inputNode;
},
});
// Input.props = {
// inputRef: PropTypes.any,
// prefixCls: PropTypes.string,
// id: PropTypes.string,
// inputElement: PropTypes.any,
// disabled: PropTypes.looseBool,
// autofocus: PropTypes.looseBool,
// autocomplete: PropTypes.string,
// editable: PropTypes.looseBool,
// accessibilityIndex: PropTypes.number,
// value: PropTypes.string,
// open: PropTypes.looseBool,
// tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
// /** Pass accessibility props to input */
// attrs: PropTypes.object,
// onKeydown: PropTypes.func,
// onMousedown: PropTypes.func,
// onChange: PropTypes.func,
// onPaste: PropTypes.func,
// onCompositionstart: PropTypes.func,
// onCompositionend: PropTypes.func,
// onFocus: PropTypes.func,
// onBlur: PropTypes.func,
// };
export default Input;

View File

@ -1,283 +0,0 @@
import TransBtn from '../TransBtn';
import type { InnerSelectorProps } from './interface';
import Input from './Input';
import type { Ref, PropType } from 'vue';
import { computed, defineComponent, onMounted, ref, watch } from 'vue';
import classNames from '../../_util/classNames';
import pickAttrs from '../../_util/pickAttrs';
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
removeIcon?: RenderNode;
// Tags
maxTagCount?: number | 'responsive';
maxTagTextLength?: number;
maxTagPlaceholder?: VueNode | ((omittedValues: DisplayValueType[]) => VueNode);
tokenSeparators?: string[];
tagRender?: (props: CustomTagProps) => VueNode;
onToggleOpen: any;
// Motion
choiceTransitionName?: string;
// Event
onRemove: (value: DisplayValueType) => void;
};
const props = {
id: PropTypes.string,
prefixCls: PropTypes.string,
values: PropTypes.array,
open: PropTypes.looseBool,
searchValue: PropTypes.string,
inputRef: PropTypes.any,
placeholder: PropTypes.any,
disabled: PropTypes.looseBool,
mode: PropTypes.string,
showSearch: PropTypes.looseBool,
autofocus: PropTypes.looseBool,
autocomplete: PropTypes.string,
activeDescendantId: PropTypes.string,
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
removeIcon: PropTypes.any,
choiceTransitionName: PropTypes.string,
maxTagCount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
maxTagTextLength: PropTypes.number,
maxTagPlaceholder: PropTypes.any.def(
() => (omittedValues: DisplayValueType[]) => `+ ${omittedValues.length} ...`,
),
tagRender: PropTypes.func,
onToggleOpen: { type: Function as PropType<(open?: boolean) => void> },
onRemove: PropTypes.func,
onInputChange: PropTypes.func,
onInputPaste: PropTypes.func,
onInputKeyDown: PropTypes.func,
onInputMouseDown: PropTypes.func,
onInputCompositionStart: PropTypes.func,
onInputCompositionEnd: PropTypes.func,
};
const onPreventMouseDown = (event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
};
const SelectSelector = defineComponent<SelectorProps>({
name: 'MultipleSelectSelector',
inheritAttrs: false,
props: props as any,
setup(props) {
const measureRef = ref();
const inputWidth = ref(0);
const focused = ref(false);
const selectionPrefixCls = computed(() => `${props.prefixCls}-selection`);
// ===================== Search ======================
const inputValue = computed(() =>
props.open || props.mode === 'tags' ? props.searchValue : '',
);
const inputEditable: Ref<boolean> = computed(
() =>
props.mode === 'tags' || ((props.showSearch && (props.open || focused.value)) as boolean),
);
// We measure width and set to the input immediately
onMounted(() => {
watch(
inputValue,
() => {
inputWidth.value = measureRef.value.scrollWidth;
},
{ flush: 'post', immediate: true },
);
});
// ===================== Render ======================
// >>> Render Selector Node. Includes Item & Rest
function defaultRenderSelector(
title: VueNode,
content: VueNode,
itemDisabled: boolean,
closable?: boolean,
onClose?: (e: MouseEvent) => void,
) {
return (
<span
class={classNames(`${selectionPrefixCls.value}-item`, {
[`${selectionPrefixCls.value}-item-disabled`]: itemDisabled,
})}
title={
typeof title === 'string' || typeof title === 'number' ? title.toString() : undefined
}
>
<span class={`${selectionPrefixCls.value}-item-content`}>{content}</span>
{closable && (
<TransBtn
class={`${selectionPrefixCls.value}-item-remove`}
onMousedown={onPreventMouseDown}
onClick={onClose}
customizeIcon={props.removeIcon}
>
×
</TransBtn>
)}
</span>
);
}
function customizeRenderSelector(
value: RawValueType,
content: VueNode,
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({
label: content,
value,
disabled: itemDisabled,
closable,
onClose,
option,
})}
</span>
);
}
function renderItem(valueItem: DisplayValueType) {
const { disabled: itemDisabled, label, value, option } = valueItem;
const closable = !props.disabled && !itemDisabled;
let displayLabel = label;
if (typeof props.maxTagTextLength === 'number') {
if (typeof label === 'string' || typeof label === 'number') {
const strLabel = String(displayLabel);
if (strLabel.length > props.maxTagTextLength) {
displayLabel = `${strLabel.slice(0, props.maxTagTextLength)}...`;
}
}
}
const onClose = (event?: MouseEvent) => {
if (event) event.stopPropagation();
props.onRemove?.(valueItem);
};
return typeof props.tagRender === 'function'
? customizeRenderSelector(value, displayLabel, itemDisabled, closable, onClose, option)
: defaultRenderSelector(label, displayLabel, itemDisabled, closable, onClose);
}
function renderRest(omittedValues: DisplayValueType[]) {
const { maxTagPlaceholder = omittedValues => `+ ${omittedValues.length} ...` } = props;
const content =
typeof maxTagPlaceholder === 'function'
? maxTagPlaceholder(omittedValues)
: maxTagPlaceholder;
return defaultRenderSelector(content, content, false);
}
return () => {
const {
id,
prefixCls,
values,
open,
inputRef,
placeholder,
disabled,
autofocus,
autocomplete,
activeDescendantId,
tabindex,
onInputChange,
onInputPaste,
onInputKeyDown,
onInputMouseDown,
onInputCompositionStart,
onInputCompositionEnd,
} = props;
// >>> Input Node
const inputNode = (
<div
class={`${selectionPrefixCls.value}-search`}
style={{ width: inputWidth.value + 'px' }}
key="input"
>
<Input
inputRef={inputRef}
open={open}
prefixCls={prefixCls}
id={id}
inputElement={null}
disabled={disabled}
autofocus={autofocus}
autocomplete={autocomplete}
editable={inputEditable.value}
activeDescendantId={activeDescendantId}
value={inputValue.value}
onKeydown={onInputKeyDown}
onMousedown={onInputMouseDown}
onChange={onInputChange}
onPaste={onInputPaste}
onCompositionstart={onInputCompositionStart}
onCompositionend={onInputCompositionEnd}
tabindex={tabindex}
attrs={pickAttrs(props, true)}
onFocus={() => (focused.value = true)}
onBlur={() => (focused.value = false)}
/>
{/* Measure Node */}
<span ref={measureRef} class={`${selectionPrefixCls.value}-search-mirror`} aria-hidden>
{inputValue.value}&nbsp;
</span>
</div>
);
// >>> Selections
const selectionNode = (
<Overflow
prefixCls={`${selectionPrefixCls.value}-overflow`}
data={values}
renderItem={renderItem}
renderRest={renderRest}
suffix={inputNode}
itemKey="key"
maxCount={props.maxTagCount}
key="overflow"
/>
);
return (
<>
{selectionNode}
{!values.length && !inputValue.value && (
<span class={`${selectionPrefixCls.value}-placeholder`}>{placeholder}</span>
)}
</>
);
};
},
});
export default SelectSelector;

View File

@ -1,172 +0,0 @@
import pickAttrs from '../../_util/pickAttrs';
import Input from './Input';
import type { InnerSelectorProps } from './interface';
import { Fragment, computed, defineComponent, ref, watch } from 'vue';
import PropTypes from '../../_util/vue-types';
import { useInjectTreeSelectContext } from '../../vc-tree-select/Context';
import type { VueNode } from '../../_util/type';
interface SelectorProps extends InnerSelectorProps {
inputElement: VueNode;
activeValue: string;
}
const props = {
inputElement: PropTypes.any,
id: PropTypes.string,
prefixCls: PropTypes.string,
values: PropTypes.array,
open: PropTypes.looseBool,
searchValue: PropTypes.string,
inputRef: PropTypes.any,
placeholder: PropTypes.any,
disabled: PropTypes.looseBool,
mode: PropTypes.string,
showSearch: PropTypes.looseBool,
autofocus: PropTypes.looseBool,
autocomplete: PropTypes.string,
activeDescendantId: PropTypes.string,
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
activeValue: PropTypes.string,
backfill: PropTypes.looseBool,
onInputChange: PropTypes.func,
onInputPaste: PropTypes.func,
onInputKeyDown: PropTypes.func,
onInputMouseDown: PropTypes.func,
onInputCompositionStart: PropTypes.func,
onInputCompositionEnd: PropTypes.func,
};
const SingleSelector = defineComponent<SelectorProps>({
name: 'SingleSelector',
setup(props) {
const inputChanged = ref(false);
const combobox = computed(() => props.mode === 'combobox');
const inputEditable = computed(() => combobox.value || props.showSearch);
const inputValue = computed(() => {
let inputValue: string = props.searchValue || '';
if (combobox.value && props.activeValue && !inputChanged.value) {
inputValue = props.activeValue;
}
return inputValue;
});
const treeSelectContext = useInjectTreeSelectContext();
watch(
[combobox, () => props.activeValue],
() => {
if (combobox.value) {
inputChanged.value = false;
}
},
{ immediate: true },
);
// Not show text when closed expect combobox mode
const hasTextInput = computed(() =>
props.mode !== 'combobox' && !props.open && !props.showSearch ? false : !!inputValue.value,
);
const title = computed(() => {
const item = props.values[0];
return item && (typeof item.label === 'string' || typeof item.label === 'number')
? item.label.toString()
: undefined;
});
const renderPlaceholder = () => {
if (props.values[0]) {
return null;
}
const hiddenStyle = hasTextInput.value ? { visibility: 'hidden' as const } : undefined;
return (
<span class={`${props.prefixCls}-selection-placeholder`} style={hiddenStyle}>
{props.placeholder}
</span>
);
};
return () => {
const {
inputElement,
prefixCls,
id,
values,
inputRef,
disabled,
autofocus,
autocomplete,
activeDescendantId,
open,
tabindex,
onInputKeyDown,
onInputMouseDown,
onInputChange,
onInputPaste,
onInputCompositionStart,
onInputCompositionEnd,
} = props;
const item = values[0];
let titleNode = null;
// custom tree-select title by slot
if (item && treeSelectContext.value.slots) {
titleNode =
treeSelectContext.value.slots[item?.option?.data?.slots?.title] ||
treeSelectContext.value.slots.title ||
item.label;
if (typeof titleNode === 'function') {
titleNode = titleNode(item.option?.data || {});
}
// else if (treeSelectContext.value.slots.titleRender) {
// // title titleRender title titleRender
// titleNode = treeSelectContext.value.slots.titleRender(item.option?.data || {});
// }
} else {
titleNode = item?.label;
}
return (
<>
<span class={`${prefixCls}-selection-search`}>
<Input
inputRef={inputRef}
prefixCls={prefixCls}
id={id}
open={open}
inputElement={inputElement}
disabled={disabled}
autofocus={autofocus}
autocomplete={autocomplete}
editable={inputEditable.value}
activeDescendantId={activeDescendantId}
value={inputValue.value}
onKeydown={onInputKeyDown}
onMousedown={onInputMouseDown}
onChange={e => {
inputChanged.value = true;
onInputChange(e as any);
}}
onPaste={onInputPaste}
onCompositionstart={onInputCompositionStart}
onCompositionend={onInputCompositionEnd}
tabindex={tabindex}
attrs={pickAttrs(props, true)}
/>
</span>
{/* Display value */}
{!combobox.value && item && !hasTextInput.value && (
<span class={`${prefixCls}-selection-item`} title={title.value}>
<Fragment key={item.key || item.value}>{titleNode}</Fragment>
</span>
)}
{/* Display placeholder */}
{renderPlaceholder()}
</>
);
};
},
});
SingleSelector.props = props;
SingleSelector.inheritAttrs = false;
export default SingleSelector;

View File

@ -1,275 +0,0 @@
/**
* Cursor rule:
* 1. Only `showSearch` enabled
* 2. Only `open` is `true`
* 3. When typing, set `open` to `true` which hit rule of 2
*
* Accessibility:
* - https://www.w3.org/TR/wai-aria-practices/examples/combobox/aria1.1pattern/listbox-combo.html
*/
import KeyCode from '../../_util/KeyCode';
import MultipleSelector from './MultipleSelector';
import SingleSelector from './SingleSelector';
import type { CustomTagProps, DisplayValueType, Mode, RenderNode } from '../BaseSelect';
import { isValidateOpenKey } from '../utils/keyUtil';
import useLock from '../hooks/useLock';
import type { PropType } from 'vue';
import { defineComponent } from 'vue';
import createRef from '../../_util/createRef';
import PropTypes from '../../_util/vue-types';
import type { VueNode } from '../../_util/type';
import type { EventHandler } from '../../_util/EventInterface';
import type { ScrollTo } from '../../vc-virtual-list/List';
export interface SelectorProps {
id: string;
prefixCls: string;
showSearch?: boolean;
open: boolean;
values: DisplayValueType[];
multiple?: boolean;
mode: Mode;
searchValue: string;
activeValue: string;
inputElement: VueNode;
autofocus?: boolean;
activeDescendantId?: string;
tabindex?: number | string;
disabled?: boolean;
placeholder?: VueNode;
removeIcon?: RenderNode;
// Tags
maxTagCount?: number | 'responsive';
maxTagTextLength?: number;
maxTagPlaceholder?: VueNode | ((omittedValues: DisplayValueType[]) => VueNode);
tagRender?: (props: CustomTagProps) => VueNode;
/** Check if `tokenSeparators` contains `\n` or `\r\n` */
tokenWithEnter?: boolean;
// Motion
choiceTransitionName?: string;
onToggleOpen: (open?: boolean) => void | any;
/** `onSearch` returns go next step boolean to check if need do toggle open */
onSearch: (searchText: string, fromTyping: boolean, isCompositing: boolean) => boolean;
onSearchSubmit: (searchText: string) => void;
onRemove: (value: DisplayValueType) => void;
onInputKeyDown?: (e: KeyboardEvent) => void;
/**
* @private get real dom for trigger align.
* This may be removed after React provides replacement of `findDOMNode`
*/
domRef: () => HTMLDivElement;
}
export interface RefSelectorProps {
focus: () => void;
blur: () => void;
scrollTo?: ScrollTo;
}
const Selector = defineComponent<SelectorProps>({
name: 'Selector',
inheritAttrs: false,
props: {
id: PropTypes.string,
prefixCls: PropTypes.string,
showSearch: PropTypes.looseBool,
open: PropTypes.looseBool,
/** Display in the Selector value, it's not same as `value` prop */
values: PropTypes.array,
multiple: PropTypes.looseBool,
mode: PropTypes.string,
searchValue: PropTypes.string,
activeValue: PropTypes.string,
inputElement: PropTypes.any,
autofocus: PropTypes.looseBool,
activeDescendantId: PropTypes.string,
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
disabled: PropTypes.looseBool,
placeholder: PropTypes.any,
removeIcon: PropTypes.any,
// Tags
maxTagCount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
maxTagTextLength: PropTypes.number,
maxTagPlaceholder: PropTypes.any,
tagRender: PropTypes.func,
/** Check if `tokenSeparators` contains `\n` or `\r\n` */
tokenWithEnter: PropTypes.looseBool,
// Motion
choiceTransitionName: PropTypes.string,
onToggleOpen: { type: Function as PropType<(open?: boolean) => void> },
/** `onSearch` returns go next step boolean to check if need do toggle open */
onSearch: PropTypes.func,
onSearchSubmit: PropTypes.func,
onRemove: PropTypes.func,
onInputKeyDown: { type: Function as PropType<EventHandler> },
/**
* @private get real dom for trigger align.
* This may be removed after React provides replacement of `findDOMNode`
*/
domRef: PropTypes.func,
} as any,
setup(props, { expose }) {
const inputRef = createRef();
let compositionStatus = false;
// ====================== Input ======================
const [getInputMouseDown, setInputMouseDown] = useLock(0);
const onInternalInputKeyDown = (event: KeyboardEvent) => {
const { which } = event;
if (which === KeyCode.UP || which === KeyCode.DOWN) {
event.preventDefault();
}
if (props.onInputKeyDown) {
props.onInputKeyDown(event);
}
if (which === KeyCode.ENTER && props.mode === 'tags' && !compositionStatus && !props.open) {
// When menu isn't open, OptionList won't trigger a value change
// So when enter is pressed, the tag's input value should be emitted here to let selector know
props.onSearchSubmit((event.target as HTMLInputElement).value);
}
if (isValidateOpenKey(which)) {
props.onToggleOpen(true);
}
};
/**
* We can not use `findDOMNode` sine it will get warning,
* have to use timer to check if is input element.
*/
const onInternalInputMouseDown = () => {
setInputMouseDown(true);
};
// When paste come, ignore next onChange
let pastedText = null;
const triggerOnSearch = (value: string) => {
if (props.onSearch(value, true, compositionStatus) !== false) {
props.onToggleOpen(true);
}
};
const onInputCompositionStart = () => {
compositionStatus = true;
};
const onInputCompositionEnd = (e: InputEvent) => {
compositionStatus = false;
// Trigger search again to support `tokenSeparators` with typewriting
if (props.mode !== 'combobox') {
triggerOnSearch((e.target as HTMLInputElement).value);
}
};
const onInputChange = (event: { target: { value: any } }) => {
let {
target: { value },
} = event;
// Pasted text should replace back to origin content
if (props.tokenWithEnter && pastedText && /[\r\n]/.test(pastedText)) {
// CRLF will be treated as a single space for input element
const replacedText = pastedText
.replace(/[\r\n]+$/, '')
.replace(/\r\n/g, ' ')
.replace(/[\r\n]/g, ' ');
value = value.replace(replacedText, pastedText);
}
pastedText = null;
triggerOnSearch(value);
};
const onInputPaste = (e: ClipboardEvent) => {
const { clipboardData } = e;
const value = clipboardData.getData('text');
pastedText = value;
};
const onClick = ({ target }) => {
if (target !== inputRef.current) {
// Should focus input if click the selector
const isIE = (document.body.style as any).msTouchAction !== undefined;
if (isIE) {
setTimeout(() => {
inputRef.current.focus();
});
} else {
inputRef.current.focus();
}
}
};
const onMousedown = (event: MouseEvent) => {
const inputMouseDown = getInputMouseDown();
if (event.target !== inputRef.current && !inputMouseDown) {
event.preventDefault();
}
if ((props.mode !== 'combobox' && (!props.showSearch || !inputMouseDown)) || !props.open) {
if (props.open) {
props.onSearch('', true, false);
}
props.onToggleOpen();
}
};
expose({
focus: () => {
inputRef.current.focus();
},
blur: () => {
inputRef.current.blur();
},
});
return () => {
const { prefixCls, domRef, mode } = props as SelectorProps;
const sharedProps = {
inputRef,
onInputKeyDown: onInternalInputKeyDown,
onInputMouseDown: onInternalInputMouseDown,
onInputChange,
onInputPaste,
onInputCompositionStart,
onInputCompositionEnd,
};
const selectNode =
mode === 'multiple' || mode === 'tags' ? (
<MultipleSelector {...props} {...sharedProps} />
) : (
<SingleSelector {...props} {...sharedProps} />
);
return (
<div
ref={domRef}
class={`${prefixCls}-selector`}
onClick={onClick}
onMousedown={onMousedown}
>
{selectNode}
</div>
);
};
},
});
export default Selector;

View File

@ -1,28 +0,0 @@
import type { RefObject } from '../../_util/createRef';
import type { EventHandler } from '../../_util/EventInterface';
import type { VueNode } from '../../_util/type';
import type { Mode, DisplayValueType } from '../BaseSelect';
export interface InnerSelectorProps {
prefixCls: string;
id: string;
mode: Mode;
inputRef: RefObject;
placeholder?: VueNode;
disabled?: boolean;
autofocus?: boolean;
autocomplete?: string;
values: DisplayValueType[];
showSearch?: boolean;
searchValue: string;
activeDescendantId: string;
open: boolean;
tabindex?: number | string;
onInputKeyDown: EventHandler;
onInputMouseDown: EventHandler;
onInputChange: EventHandler;
onInputPaste: EventHandler;
onInputCompositionStart: EventHandler;
onInputCompositionEnd: EventHandler;
}

View File

@ -1,66 +0,0 @@
import type { FunctionalComponent } from 'vue';
import type { VueNode } from '../_util/type';
import PropTypes from '../_util/vue-types';
import type { RenderNode } from './BaseSelect';
export interface TransBtnProps {
class: string;
customizeIcon: RenderNode;
customizeIconProps?: any;
onMousedown?: (payload: MouseEvent) => void;
onClick?: (payload: MouseEvent) => void;
}
export interface TransBtnType extends FunctionalComponent<TransBtnProps> {
displayName: string;
}
const TransBtn: TransBtnType = (props, { slots }) => {
const { class: className, customizeIcon, customizeIconProps, onMousedown, onClick } = props;
let icon: VueNode;
if (typeof customizeIcon === 'function') {
icon = customizeIcon(customizeIconProps);
} else {
icon = customizeIcon;
}
return (
<span
class={className}
onMousedown={event => {
event.preventDefault();
if (onMousedown) {
onMousedown(event);
}
}}
style={{
userSelect: 'none',
WebkitUserSelect: 'none',
}}
unselectable="on"
onClick={onClick}
aria-hidden
>
{icon !== undefined ? (
icon
) : (
<span class={className.split(/\s+/).map((cls: any) => `${cls}-icon`)}>
{slots.default?.()}
</span>
)}
</span>
);
};
TransBtn.inheritAttrs = false;
TransBtn.displayName = 'TransBtn';
TransBtn.props = {
class: PropTypes.string,
customizeIcon: PropTypes.any,
customizeIconProps: PropTypes.any,
onMousedown: PropTypes.func,
onClick: PropTypes.func,
};
export default TransBtn;

View File

@ -1,32 +0,0 @@
import type { Ref } from 'vue';
import { onMounted, ref } from 'vue';
/**
* Similar with `useLock`, but this hook will always execute last value.
* When set to `true`, it will keep `true` for a short time even if `false` is set.
*/
export default function useDelayReset(
timeout = 10,
): [Ref<Boolean>, (val: boolean, callback?: () => void) => void, () => void] {
const bool = ref(false);
let delay: any;
const cancelLatest = () => {
clearTimeout(delay);
};
onMounted(() => {
cancelLatest();
});
const delaySetBool = (value: boolean, callback: () => void) => {
cancelLatest();
delay = setTimeout(() => {
bool.value = value;
if (callback) {
callback();
}
}, timeout);
};
return [bool, delaySetBool, cancelLatest];
}

View File

@ -1,29 +0,0 @@
import { onBeforeUpdate } from 'vue';
/**
* Locker return cached mark.
* If set to `true`, will return `true` in a short time even if set `false`.
* If set to `false` and then set to `true`, will change to `true`.
* And after time duration, it will back to `null` automatically.
*/
export default function useLock(duration = 250): [() => boolean | null, (lock: boolean) => void] {
let lock: boolean | null = null;
let timeout: any;
onBeforeUpdate(() => {
clearTimeout(timeout);
});
function doLock(locked: boolean) {
if (locked || lock === null) {
lock = locked;
}
clearTimeout(timeout);
timeout = setTimeout(() => {
lock = null;
}, duration);
}
return [() => lock, doLock];
}

View File

@ -1,32 +0,0 @@
import type { Ref } from 'vue';
import { onBeforeUnmount, onMounted } from 'vue';
export default function useSelectTriggerControl(
refs: Ref[],
open: Ref<boolean>,
triggerOpen: (open: boolean) => void,
) {
function onGlobalMouseDown(event: MouseEvent) {
let target = event.target as HTMLElement;
if (target.shadowRoot && event.composed) {
target = (event.composedPath()[0] || target) as HTMLElement;
}
const elements = [refs[0]?.value, refs[1]?.value?.getPopupElement()];
if (
open.value &&
elements.every(element => element && !element.contains(target) && element !== target)
) {
// Should trigger close
triggerOpen(false);
}
}
onMounted(() => {
window.addEventListener('mousedown', onGlobalMouseDown);
});
onBeforeUnmount(() => {
window.removeEventListener('mousedown', onGlobalMouseDown);
});
}

View File

@ -1,12 +0,0 @@
import type { SelectProps } from './Select';
import Select, { selectProps } from './Select';
import Option from './Option';
import OptGroup from './OptGroup';
import BaseSelect from './BaseSelect';
import type { BaseSelectProps, BaseSelectRef, BaseSelectPropsWithoutPrivate } from './BaseSelect';
import useBaseProps from './hooks/useBaseProps';
export { Option, OptGroup, selectProps, BaseSelect, useBaseProps };
export type { BaseSelectProps, BaseSelectRef, BaseSelectPropsWithoutPrivate, SelectProps };
export default Select;

View File

@ -1,12 +0,0 @@
export function toArray<T>(value: T | T[]): T[] {
if (Array.isArray(value)) {
return value;
}
return value !== undefined ? [value] : [];
}
export const isClient =
typeof window !== 'undefined' && window.document && window.document.documentElement;
/** Is client side and not jsdom */
export const isBrowserClient = process.env.NODE_ENV !== 'test' && isClient;

View File

@ -1,61 +0,0 @@
import { flattenChildren, isValidElement } from '../../_util/props-util';
import type { VNode } from 'vue';
import type { BaseOptionType, DefaultOptionType } from '../Select';
import type { VueNode } from '../../_util/type';
function convertNodeToOption<OptionType extends BaseOptionType = DefaultOptionType>(
node: VNode,
): OptionType {
const {
key,
children,
props: { value, disabled, ...restProps },
} = node as Omit<VNode, 'key'> & {
children: { default?: () => any };
key: string | number;
};
const child = children && children.default ? children.default() : undefined;
return {
key,
value: value !== undefined ? value : key,
children: child,
disabled: disabled || disabled === '', // support <a-select-option disabled />
...(restProps as any),
};
}
export function convertChildrenToData<OptionType extends BaseOptionType = DefaultOptionType>(
nodes: VueNode[],
optionOnly = false,
): OptionType[] {
const dd = flattenChildren(nodes as [])
.map((node: VNode, index: number): OptionType | null => {
if (!isValidElement(node) || !node.type) {
return null;
}
const {
type: { isSelectOptGroup },
key,
children,
props,
} = node as VNode & {
type: { isSelectOptGroup?: boolean };
children: { default?: () => any; label?: () => any };
};
if (optionOnly || !isSelectOptGroup) {
return convertNodeToOption(node);
}
const child = children && children.default ? children.default() : undefined;
const label = props?.label || children.label?.() || key;
return {
key: `__RC_SELECT_GRP__${key === null ? index : String(key)}__`,
...props,
label,
options: convertChildrenToData(child || []),
} as any;
})
.filter(data => data);
return dd;
}

View File

@ -1,4 +0,0 @@
/* istanbul ignore file */
export function isPlatformMac(): boolean {
return /(mac\sos|macintosh)/i.test(navigator.appVersion);
}

View File

@ -1,128 +0,0 @@
import type { BaseOptionType, DefaultOptionType, RawValueType, FieldNames } from '../Select';
import { warning } from '../../vc-util/warning';
import type { FlattenOptionData } from '../interface';
function getKey(data: BaseOptionType, index: number) {
const { key } = data;
let value: RawValueType;
if ('value' in data) {
({ value } = data);
}
if (key !== null && key !== undefined) {
return key;
}
if (value !== undefined) {
return value;
}
return `rc-index-key-${index}`;
}
export function fillFieldNames(fieldNames: FieldNames | undefined, childrenAsData: boolean) {
const { label, value, options } = fieldNames || {};
return {
label: label || (childrenAsData ? 'children' : 'label'),
value: value || 'value',
options: options || 'options',
};
}
/**
* Flat options into flatten list.
* We use `optionOnly` here is aim to avoid user use nested option group.
* Here is simply set `key` to the index if not provided.
*/
export function flattenOptions<OptionType extends BaseOptionType = DefaultOptionType>(
options: OptionType[],
{ fieldNames, childrenAsData }: { fieldNames?: FieldNames; childrenAsData?: boolean } = {},
): FlattenOptionData<OptionType>[] {
const flattenList: FlattenOptionData<OptionType>[] = [];
const {
label: fieldLabel,
value: fieldValue,
options: fieldOptions,
} = fillFieldNames(fieldNames, false);
function dig(list: OptionType[], isGroupOption: boolean) {
list.forEach(data => {
const label = data[fieldLabel];
if (isGroupOption || !(fieldOptions in data)) {
const value = data[fieldValue];
// Option
flattenList.push({
key: getKey(data, flattenList.length),
groupOption: isGroupOption,
data,
label,
value,
});
} else {
let grpLabel = label;
if (grpLabel === undefined && childrenAsData) {
grpLabel = data.label;
}
// Option Group
flattenList.push({
key: getKey(data, flattenList.length),
group: true,
data,
label: grpLabel,
});
dig(data[fieldOptions], true);
}
});
}
dig(options, false);
return flattenList;
}
/**
* Inject `props` into `option` for legacy usage
*/
export function injectPropsWithOption<T>(option: T): T {
const newOption = { ...option };
if (!('props' in newOption)) {
Object.defineProperty(newOption, 'props', {
get() {
warning(
false,
'Return type is option instead of Option instance. Please read value directly instead of reading from `props`.',
);
return newOption;
},
});
}
return newOption;
}
export function getSeparatedContent(text: string, tokens: string[]): string[] {
if (!tokens || !tokens.length) {
return null;
}
let match = false;
function separate(str: string, [token, ...restTokens]: string[]) {
if (!token) {
return [str];
}
const list = str.split(token);
match = match || list.length > 1;
return list
.reduce((prevList, unitStr) => [...prevList, ...separate(unitStr, restTokens)], [])
.filter(unit => unit);
}
const list = separate(text, tokens);
return match ? list : null;
}

View File

@ -1,135 +0,0 @@
import warning, { noteOnce } from '../../vc-util/warning';
import { convertChildrenToData } from './legacyUtil';
import { toArray } from './commonUtil';
import { isValidElement } from '../../_util/props-util';
import type { VNode } from 'vue';
import type { RawValueType, LabelInValueType, SelectProps } from '../Select';
import { isMultiple } from '../BaseSelect';
function warningProps(props: SelectProps) {
const {
mode,
options,
children,
backfill,
allowClear,
placeholder,
getInputElement,
showSearch,
onSearch,
defaultOpen,
autofocus,
labelInValue,
value,
inputValue,
optionLabelProp,
} = props;
const multiple = isMultiple(mode);
const mergedShowSearch = showSearch !== undefined ? showSearch : multiple || mode === 'combobox';
const mergedOptions = options || convertChildrenToData(children);
// `tags` should not set option as disabled
warning(
mode !== 'tags' || mergedOptions.every((opt: { disabled?: boolean }) => !opt.disabled),
'Please avoid setting option to disabled in tags mode since user can always type text as tag.',
);
// `combobox` should not use `optionLabelProp`
warning(
mode !== 'combobox' || !optionLabelProp,
'`combobox` mode not support `optionLabelProp`. Please set `value` on Option directly.',
);
// Only `combobox` support `backfill`
warning(mode === 'combobox' || !backfill, '`backfill` only works with `combobox` mode.');
// Only `combobox` support `getInputElement`
warning(
mode === 'combobox' || !getInputElement,
'`getInputElement` only work with `combobox` mode.',
);
// Customize `getInputElement` should not use `allowClear` & `placeholder`
noteOnce(
mode !== 'combobox' || !getInputElement || !allowClear || !placeholder,
'Customize `getInputElement` should customize clear and placeholder logic instead of configuring `allowClear` and `placeholder`.',
);
// `onSearch` should use in `combobox` or `showSearch`
if (onSearch && !mergedShowSearch && mode !== 'combobox' && mode !== 'tags') {
warning(false, '`onSearch` should work with `showSearch` instead of use alone.');
}
noteOnce(
!defaultOpen || autofocus,
'`defaultOpen` makes Select open without focus which means it will not close by click outside. You can set `autofocus` if needed.',
);
if (value !== undefined && value !== null) {
const values = toArray<RawValueType | LabelInValueType>(value);
warning(
!labelInValue ||
values.every(val => typeof val === 'object' && ('key' in val || 'value' in val)),
'`value` should in shape of `{ value: string | number, label?: any }` when you set `labelInValue` to `true`',
);
warning(
!multiple || Array.isArray(value),
'`value` should be array when `mode` is `multiple` or `tags`',
);
}
// Syntactic sugar should use correct children type
if (children) {
let invalidateChildType = null;
children.some((node: any) => {
if (!isValidElement(node) || !node.type) {
return false;
}
const { type } = node as { type: { isSelectOption?: boolean; isSelectOptGroup?: boolean } };
if (type.isSelectOption) {
return false;
}
if (type.isSelectOptGroup) {
const childs = node.children?.default() || [];
const allChildrenValid = childs.every((subNode: VNode) => {
if (
!isValidElement(subNode) ||
!node.type ||
(subNode.type as { isSelectOption?: boolean }).isSelectOption
) {
return true;
}
invalidateChildType = subNode.type;
return false;
});
if (allChildrenValid) {
return false;
}
return true;
}
invalidateChildType = type;
return true;
});
if (invalidateChildType) {
warning(
false,
`\`children\` should be \`Select.Option\` or \`Select.OptGroup\` instead of \`${
invalidateChildType.displayName || invalidateChildType.name || invalidateChildType
}\`.`,
);
}
warning(
inputValue === undefined,
'`inputValue` is deprecated, please use `searchValue` instead.',
);
}
}
export default warningProps;

View File

@ -1,3 +1,4 @@
// base rc-tree-select@4.6.1
import TreeSelect from './TreeSelect'; import TreeSelect from './TreeSelect';
import TreeNode from './TreeNode'; import TreeNode from './TreeNode';
import { SHOW_ALL, SHOW_CHILD, SHOW_PARENT } from './utils/strategyUtil'; import { SHOW_ALL, SHOW_CHILD, SHOW_PARENT } from './utils/strategyUtil';