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 { 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> {
/** Legacy for check if is a Option Group */

View File

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

View File

@ -1,8 +1,8 @@
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> {
/** Legacy for check if is a Option Group */

View File

@ -1,8 +1,8 @@
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 */
[prop: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
}

View File

@ -1,22 +1,12 @@
import TransBtn from './TransBtn';
import PropTypes from '../_util/vue-types';
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 type { PropType } from 'vue';
import { computed, defineComponent, nextTick, reactive, watch } from 'vue';
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 { isPlatformMac } from './utils/platformUtil';
@ -28,78 +18,28 @@ export interface RefOptionListProps {
import type { EventHandler } from '../_util/EventInterface';
import omit from '../_util/omit';
export interface OptionListProps<OptionType extends object> {
prefixCls: string;
id: string;
options: OptionType[];
fieldNames?: FieldNames;
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,
};
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<OptionListProps<SelectOptionsType[number]>, { state?: any }>({
const OptionList = defineComponent({
name: 'OptionList',
inheritAttrs: false,
slots: ['option'],
setup(props) {
const itemPrefixCls = computed(() => `${props.prefixCls}-item`);
setup(_, { expose, slots }) {
const baseProps = useBaseProps();
const props = useSelectProps();
const itemPrefixCls = computed(() => `${baseProps.prefixCls}-item`);
const memoFlattenOptions = useMemo(
() => props.flattenOptions,
[() => props.open, () => props.flattenOptions],
[() => baseProps.open, () => props.flattenOptions],
next => next[0],
);
@ -124,7 +64,7 @@ const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, {
const current = (index + i * offset + len) % len;
const { group, data } = memoFlattenOptions.value[current];
if (!group && !(data as OptionData).disabled) {
if (!group && !data.disabled) {
return current;
}
}
@ -152,7 +92,7 @@ const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, {
// Auto active first item when list length or searchValue changed
watch(
[() => memoFlattenOptions.value.length, () => props.searchValue],
[() => memoFlattenOptions.value.length, () => baseProps.searchValue],
() => {
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
watch(
[() => props.open, () => props.searchValue],
[() => baseProps.open, () => baseProps.searchValue],
() => {
if (!props.multiple && props.open && props.values.size === 1) {
const value = Array.from(props.values)[0];
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);
@ -174,7 +114,7 @@ const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, {
}
}
// Force trigger scrollbar visible when open
if (props.open) {
if (baseProps.open) {
nextTick(() => {
listRef.current?.scrollTo(undefined);
});
@ -186,262 +126,253 @@ const OptionList = defineComponent<OptionListProps<SelectOptionsType[number]>, {
// ========================== Values ==========================
const onSelectValue = (value?: RawValueType) => {
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
if (!props.multiple) {
props.onToggleOpen(false);
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 || {}) as OptionData;
const { value, label, children } = itemData;
const itemData = item.data || {};
const { value } = itemData;
const { group } = item;
const attrs = pickAttrs(itemData, true);
const mergedLabel = props.childrenAsData ? children : label;
const mergedLabel = getLabel(item);
return item ? (
<div
aria-label={typeof mergedLabel === 'string' ? mergedLabel : undefined}
aria-label={typeof mergedLabel === 'string' && !group ? mergedLabel : null}
{...attrs}
key={index}
role="option"
id={`${props.id}_list_${index}`}
aria-selected={props.values.has(value)}
role={group ? 'presentation' : 'option'}
id={`${baseProps.id}_list_${index}`}
aria-selected={props.rawValues.has(value)}
>
{value}
</div>
) : null;
}
return {
memoFlattenOptions,
renderItem,
listRef,
state,
onListMouseDown,
itemPrefixCls,
setActive,
onSelectValue,
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) {
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 (isPlatformMac() && ctrlKey) {
if (which === KeyCode.N) {
offset = 1;
} else if (which === KeyCode.P) {
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 (props.open) {
event.preventDefault();
}
break;
if (offset !== 0) {
const nextActiveIndex = getEnabledActiveIndex(state.activeIndex + offset, offset);
scrollIntoView(nextActiveIndex);
setActive(nextActiveIndex, true);
}
// >>> Close
case KeyCode.ESC: {
props.onToggleOpen(false);
if (props.open) {
event.stopPropagation();
}
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();
}
}
},
onKeyup: () => {},
scrollTo: (index: number) => {
scrollIntoView(index);
},
}
};
},
render() {
const {
renderItem,
listRef,
onListMouseDown,
itemPrefixCls,
setActive,
onSelectValue,
memoFlattenOptions,
$slots,
} = this as any;
const {
id,
childrenAsData,
values,
height,
itemHeight,
menuItemSelectedIcon,
notFoundContent,
virtual,
fieldNames,
onScroll,
onMouseenter,
} = this.$props;
const renderOption = $slots.option;
const { activeIndex } = this.state;
const omitFieldNameList = Object.values(fillFieldNames(fieldNames));
// ========================== Render ==========================
if (memoFlattenOptions.length === 0) {
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`}
class={`${itemPrefixCls}-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}
height={height}
itemHeight={itemHeight}
fullHeight={false}
onMousedown={onListMouseDown}
onScroll={onScroll}
virtual={virtual}
onMouseenter={onMouseenter}
v-slots={{
default: ({ group, groupOption, data, label, value }, itemIndex) => {
const { key } = data;
// Group
if (group) {
<>
<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 class={classNames(itemPrefixCls, `${itemPrefixCls}-group`)}>
{renderOption ? renderOption(data) : label !== undefined ? label : key}
<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>
);
}
const {
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>
</>
);
},
}}
></List>
</>
);
};
},
});
OptionList.props = OptionListProps;
export default OptionList;

View File

@ -1,6 +1,6 @@
/**
* 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.
*
* ref:
@ -29,76 +29,614 @@
* - `combobox` mode not support `optionLabelProp`
*/
import type { OptionsType as SelectOptionsType } from './interface';
import SelectOptionList from './OptionList';
import Option from './Option';
import OptGroup from './OptGroup';
import { convertChildrenToData as convertSelectChildrenToData } from './utils/legacyUtil';
import {
getLabeledValue as getSelectLabeledValue,
filterOptions as selectDefaultFilterOptions,
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 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 { 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]>({
prefixCls: 'rc-select',
components: {
optionList: SelectOptionList as any,
},
convertChildrenToData: convertSelectChildrenToData,
flattenOptions,
getLabeledValue: getSelectLabeledValue,
filterOptions: selectDefaultFilterOptions,
isValueDisabled: isSelectValueDisabled,
findValueOption: findSelectValueOption,
warningProps,
fillOptionsWithMissingValue,
});
const OMIT_DOM_PROPS = ['inputValue'];
export type ExportedSelectProps<T extends DefaultValueType = DefaultValueType> = SelectProps<
SelectOptionsType[number],
T
>;
export type OnActiveValue = (
active: RawValueType,
index: number,
info?: { source?: 'keyboard' | 'mouse' },
) => void;
export function selectProps<T>() {
return selectBaseProps<SelectOptionsType[number], T>();
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;
}
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',
inheritAttrs: false,
Option,
OptGroup,
props: RefSelect.props,
setup(props, { attrs, expose, slots }) {
const selectRef = ref();
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: () => {
focus() {
selectRef.value?.focus();
},
blur: () => {
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 (
<RefSelect
ref={selectRef}
{...(props as any)}
<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}
children={slots.default?.() || []}
/>
);
};
},
});
export default Select;

View File

@ -1,19 +1,12 @@
import Trigger from '../vc-trigger';
import PropTypes from '../_util/vue-types';
import { getSlot } from '../_util/props-util';
import classNames from '../_util/classNames';
import createRef from '../_util/createRef';
import type { CSSProperties } from 'vue';
import { defineComponent } from 'vue';
import type { RenderDOMFunc } from './interface';
import type { DropdownRender } from './interface/generator';
import type { Placement } from './generate';
import { computed, ref, defineComponent } from 'vue';
import type { VueNode } from '../_util/type';
import type { DropdownRender, Placement, RenderDOMFunc } from './BaseSelect';
const getBuiltInPlacements = (dropdownMatchSelectWidth: number | boolean) => {
// Enable horizontal overflow auto-adjustment when a custom dropdown width is provided
const adjustX = typeof dropdownMatchSelectWidth !== 'number' ? 0 : 1;
const getBuiltInPlacements = (adjustX: number) => {
return {
bottomLeft: {
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 {
prefixCls: string;
disabled: boolean;
@ -66,103 +72,122 @@ export interface SelectTriggerProps {
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,
created() {
this.popupRef = createRef();
},
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`;
methods: {
getPopupElement() {
return this.popupRef.current;
},
},
let popupNode = popupElement;
if (dropdownRender) {
popupNode = dropdownRender({ menuNode: popupElement, props });
}
render() {
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`;
const mergedTransitionName = animation ? `${dropdownPrefixCls}-${animation}` : transitionName;
let popupNode = popupElement;
if (dropdownRender) {
popupNode = dropdownRender({ menuNode: popupElement, props });
}
const popupStyle = { minWidth: `${containerWidth}px`, ...dropdownStyle };
const builtInPlacements = getBuiltInPlacements(dropdownMatchSelectWidth);
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}
prefixCls={dropdownPrefixCls}
popupTransitionName={mergedTransitionName}
popup={<div ref={this.popupRef}>{popupNode}</div>}
popupAlign={dropdownAlign}
popupVisible={visible}
getPopupContainer={getPopupContainer}
popupClassName={classNames(dropdownClassName, {
[`${dropdownPrefixCls}-empty`]: empty,
})}
popupStyle={popupStyle}
getTriggerDOMNode={getTriggerDOMNode}
>
{getSlot(this)[0]}
</Trigger>
);
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>
);
};
},
});
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;

View File

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

View File

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

View File

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

View File

@ -11,8 +11,8 @@
import KeyCode from '../../_util/KeyCode';
import MultipleSelector from './MultipleSelector';
import SingleSelector from './SingleSelector';
import type { LabelValueType, RawValueType, CustomTagProps } from '../interface/generator';
import type { RenderNode, Mode } from '../interface';
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';
@ -20,22 +20,22 @@ 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;
/** Display in the Selector value, it's not same as `value` prop */
values: LabelValueType[];
multiple: boolean;
values: DisplayValueType[];
multiple?: boolean;
mode: Mode;
searchValue: string;
activeValue: string;
inputElement: VueNode;
autofocus?: boolean;
accessibilityIndex: number;
activeDescendantId?: string;
tabindex?: number | string;
disabled?: boolean;
placeholder?: VueNode;
@ -44,7 +44,7 @@ export interface SelectorProps {
// Tags
maxTagCount?: number | 'responsive';
maxTagTextLength?: number;
maxTagPlaceholder?: VueNode | ((omittedValues: LabelValueType[]) => VueNode);
maxTagPlaceholder?: VueNode | ((omittedValues: DisplayValueType[]) => VueNode);
tagRender?: (props: CustomTagProps) => VueNode;
/** 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: (searchText: string, fromTyping: boolean, isCompositing: boolean) => boolean;
onSearchSubmit: (searchText: string) => void;
onSelect: (value: RawValueType, option: { selected: boolean }) => void;
onRemove: (value: DisplayValueType) => void;
onInputKeyDown?: (e: KeyboardEvent) => void;
/**
@ -66,6 +66,11 @@ export interface SelectorProps {
*/
domRef: () => HTMLDivElement;
}
export interface RefSelectorProps {
focus: () => void;
blur: () => void;
scrollTo?: ScrollTo;
}
const Selector = defineComponent<SelectorProps>({
name: 'Selector',
@ -84,7 +89,7 @@ const Selector = defineComponent<SelectorProps>({
inputElement: PropTypes.any,
autofocus: PropTypes.looseBool,
accessibilityIndex: PropTypes.number,
activeDescendantId: PropTypes.string,
tabindex: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
disabled: PropTypes.looseBool,
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: PropTypes.func,
onSearchSubmit: PropTypes.func,
onSelect: PropTypes.func,
onRemove: PropTypes.func,
onInputKeyDown: { type: Function as PropType<EventHandler> },
/**
@ -115,7 +120,7 @@ const Selector = defineComponent<SelectorProps>({
*/
domRef: PropTypes.func,
} as any,
setup(props) {
setup(props, { expose }) {
const inputRef = createRef();
let compositionStatus = false;
@ -139,7 +144,7 @@ const Selector = defineComponent<SelectorProps>({
props.onSearchSubmit((event.target as HTMLInputElement).value);
}
if (![KeyCode.SHIFT, KeyCode.TAB, KeyCode.BACKSPACE, KeyCode.ESC].includes(which)) {
if (isValidateOpenKey(which)) {
props.onToggleOpen(true);
}
};
@ -227,58 +232,44 @@ const Selector = defineComponent<SelectorProps>({
props.onToggleOpen();
}
};
return {
expose({
focus: () => {
inputRef.current.focus();
},
blur: () => {
inputRef.current.blur();
},
onMousedown,
onClick,
onInputPaste,
inputRef,
onInternalInputKeyDown,
onInternalInputMouseDown,
onInputChange,
onInputCompositionEnd,
onInputCompositionStart,
});
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>
);
};
},
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;

View File

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

View File

@ -1,10 +1,11 @@
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: VueNode | ((props?: any) => VueNode);
customizeIcon: RenderNode;
customizeIconProps?: any;
onMousedown?: (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 Option from './Option';
import OptGroup from './OptGroup';
import { selectBaseProps } from './generate';
import type { ExtractPropTypes } from 'vue';
import BaseSelect from './BaseSelect';
import type { BaseSelectProps, BaseSelectRef, BaseSelectPropsWithoutPrivate } from './BaseSelect';
import useBaseProps from './hooks/useBaseProps';
export type SelectProps<T = any> = Partial<ExtractPropTypes<ExportedSelectProps<T>>>;
export { Option, OptGroup, selectBaseProps, selectProps };
export { Option, OptGroup, selectProps, BaseSelect, useBaseProps };
export type { BaseSelectProps, BaseSelectRef, BaseSelectPropsWithoutPrivate, SelectProps };
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[] {
if (Array.isArray(value)) {
return value;
@ -13,116 +5,8 @@ export function toArray<T>(value: T | T[]): T[] {
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 =
typeof window !== 'undefined' && window.document && window.document.documentElement;
/** Is client side and not jsdom */
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 type { VNode } from 'vue';
import type { OptionData, OptionGroupData, OptionsType } from '../interface';
import type { BaseOptionType, DefaultOptionType } from '../Select';
import type { VueNode } from '../../_util/type';
function convertNodeToOption(node: VNode): OptionData {
function convertNodeToOption<OptionType extends BaseOptionType = DefaultOptionType>(
node: VNode,
): OptionType {
const {
key,
children,
@ -18,13 +20,16 @@ function convertNodeToOption(node: VNode): OptionData {
value: value !== undefined ? value : key,
children: child,
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 [])
.map((node: VNode, index: number): OptionData | OptionGroupData | null => {
.map((node: VNode, index: number): OptionType | null => {
if (!isValidElement(node) || !node.type) {
return null;
}

View File

@ -1,24 +1,8 @@
import type { BaseOptionType, DefaultOptionType, RawValueType, FieldNames } from '../Select';
import { warning } from '../../vc-util/warning';
import { cloneVNode, isVNode } from 'vue';
import type {
OptionsType as SelectOptionsType,
OptionData,
OptionGroupData,
FlattenOptionData,
FieldNames,
} from '../interface';
import type {
LabelValueType,
FilterFunc,
RawValueType,
GetLabeledValue,
DefaultValueType,
} from '../interface/generator';
import type { FlattenOptionData } from '../interface';
import { toArray } from './commonUtil';
import type { VueNode } from '../../_util/type';
function getKey(data: OptionData | OptionGroupData, index: number) {
function getKey(data: BaseOptionType, index: number) {
const { key } = data;
let value: RawValueType;
@ -35,11 +19,11 @@ function getKey(data: OptionData | OptionGroupData, index: number) {
return `rc-index-key-${index}`;
}
export function fillFieldNames(fieldNames?: FieldNames) {
export function fillFieldNames(fieldNames: FieldNames | undefined, childrenAsData: boolean) {
const { label, value, options } = fieldNames || {};
return {
label: label || 'label',
label: label || (childrenAsData ? 'children' : 'label'),
value: value || 'value',
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.
* Here is simply set `key` to the index if not provided.
*/
export function flattenOptions(
options: SelectOptionsType,
{ fieldNames }: { fieldNames?: FieldNames } = {},
): FlattenOptionData[] {
const flattenList: FlattenOptionData[] = [];
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);
} = fillFieldNames(fieldNames, false);
function dig(list: SelectOptionsType, isGroupOption: boolean) {
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: data[fieldValue],
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,
label: grpLabel,
});
dig(data[fieldOptions], true);
@ -97,7 +86,7 @@ export function flattenOptions(
/**
* Inject `props` into `option` for legacy usage
*/
function injectPropsWithOption<T>(option: T): T {
export function injectPropsWithOption<T>(option: T): T {
const newOption = { ...option };
if (!('props' in newOption)) {
Object.defineProperty(newOption, 'props', {
@ -114,154 +103,6 @@ function injectPropsWithOption<T>(option: T): T {
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[] {
if (!tokens || !tokens.length) {
return null;
@ -285,53 +126,3 @@ export function getSeparatedContent(text: string, tokens: string[]): string[] {
const list = separate(text, tokens);
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 type { SelectProps } from '..';
import { convertChildrenToData } from './legacyUtil';
import { toArray } from './commonUtil';
import type { RawValueType, LabelValueType } from '../interface/generator';
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 {
@ -25,13 +25,13 @@ function warningProps(props: SelectProps) {
optionLabelProp,
} = props;
const multiple = mode === 'multiple' || mode === 'tags';
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: 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.',
);
@ -67,7 +67,7 @@ function warningProps(props: SelectProps) {
);
if (value !== undefined && value !== null) {
const values = toArray<RawValueType | LabelValueType>(value);
const values = toArray<RawValueType | LabelInValueType>(value);
warning(
!labelInValue ||
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 TreeNode from './TreeNode';
import { SHOW_ALL, SHOW_CHILD, SHOW_PARENT } from './utils/strategyUtil';