feat: refactor vc-select

pull/2950/head
tangjinzhou 2020-10-07 22:49:01 +08:00
parent 72b1c0b539
commit b786f6bba6
47 changed files with 3811 additions and 135 deletions

View File

@ -2,7 +2,7 @@
module.exports = function(modules) {
const plugins = [
require.resolve('@vue/babel-plugin-jsx'),
[require.resolve('@vue/babel-plugin-jsx'), { mergeProps: false }],
require.resolve('@babel/plugin-proposal-optional-chaining'),
require.resolve('@babel/plugin-transform-object-assign'),
require.resolve('@babel/plugin-proposal-object-rest-spread'),

View File

@ -83,7 +83,7 @@ function getWebpackConfig(modules) {
options: {
presets: [require.resolve('@babel/preset-env')],
plugins: [
require.resolve('@vue/babel-plugin-jsx'),
[require.resolve('@vue/babel-plugin-jsx'), { mergeProps: false }],
require.resolve('@babel/plugin-proposal-object-rest-spread'),
],
},
@ -231,7 +231,6 @@ All rights reserved.
return config;
}
getWebpackConfig.webpack = webpack;
getWebpackConfig.svgRegex = svgRegex;
getWebpackConfig.svgOptions = svgOptions;
getWebpackConfig.imageOptions = imageOptions;

View File

@ -3,7 +3,7 @@ module.exports = {
test: {
presets: [['@babel/preset-env', { targets: { node: true } }]],
plugins: [
'@vue/babel-plugin-jsx',
['@vue/babel-plugin-jsx', { mergeProps: false }],
'@babel/plugin-proposal-optional-chaining',
'@babel/plugin-transform-object-assign',
'@babel/plugin-proposal-object-rest-spread',

View File

@ -1,4 +1,4 @@
interface RefObject extends Function {
export interface RefObject extends Function {
current?: any;
}
@ -9,4 +9,23 @@ function createRef(): RefObject {
return func;
}
export function fillRef<T>(ref, node: T) {
if (typeof ref === 'function') {
ref(node);
} else if (typeof ref === 'object' && ref && 'current' in ref) {
(ref as any).current = node;
}
}
/**
* Merge refs into one ref function to support ref passing.
*/
export function composeRef<T>(...refs: any[]) {
return (node: T) => {
refs.forEach(ref => {
fillRef(ref, node);
});
};
}
export default createRef;

View File

@ -1,23 +1,23 @@
const attributes = `accept acceptCharset accessKey action allowFullScreen allowTransparency
alt async autoComplete autoFocus autoPlay capture cellPadding cellSpacing challenge
charSet checked classID className colSpan cols content contentEditable contextMenu
controls coords crossOrigin data dateTime default defer dir disabled download draggable
encType form formAction formEncType formMethod formNoValidate formTarget frameBorder
headers height hidden high href hrefLang htmlFor httpEquiv icon id inputMode integrity
is keyParams keyType kind label lang list loop low manifest marginHeight marginWidth max maxLength media
mediaGroup method min minLength multiple muted name noValidate nonce open
optimum pattern placeholder poster preload radioGroup readOnly rel required
reversed role rowSpan rows sandbox scope scoped scrolling seamless selected
shape size sizes span spellCheck src srcDoc srcLang srcSet start step style
summary tabIndex target title type useMap value width wmode wrap`;
const attributes = `accept acceptcharset accesskey action allowfullscreen allowtransparency
alt async autocomplete autofocus autoplay capture cellpadding cellspacing challenge
charset checked classid classname colspan cols content contenteditable contextmenu
controls coords crossorigin data datetime default defer dir disabled download draggable
enctype form formaction formenctype formmethod formnovalidate formtarget frameborder
headers height hidden high href hreflang htmlfor httpequiv icon id inputmode integrity
is keyparams keytype kind label lang list loop low manifest marginheight marginwidth max maxlength media
mediagroup method min minlength multiple muted name novalidate nonce open
optimum pattern placeholder poster preload radiogroup readonly rel required
reversed role rowspan rows sandbox scope scoped scrolling seamless selected
shape size sizes span spellcheck src srcdoc srclang srcset start step style
summary tabindex target title type usemap value width wmode wrap`;
const eventsName = `onCopy onCut onPaste onCompositionEnd onCompositionStart onCompositionUpdate onKeyDown
onKeyPress onKeyUp onFocus onBlur onChange onInput onSubmit onClick onContextMenu onDoubleClick
onDrag onDragEnd onDragEnter onDragExit onDragLeave onDragOver onDragStart onDrop onMouseDown
onMouseEnter onMouseLeave onMouseMove onMouseOut onMouseOver onMouseUp onSelect onTouchCancel
onTouchEnd onTouchMove onTouchStart onScroll onWheel onAbort onCanPlay onCanPlayThrough
onDurationChange onEmptied onEncrypted onEnded onError onLoadedData onLoadedMetadata
onLoadStart onPause onPlay onPlaying onProgress onRateChange onSeeked onSeeking onStalled onSuspend onTimeUpdate onVolumeChange onWaiting onLoad onError`;
const eventsName = `onCopy onCut onPaste onCompositionend onCompositionstart onCompositionupdate onKeydown
onKeypress onKeyup onFocus onBlur onChange onInput onSubmit onClick onContextmenu onDoubleclick onDblclick
onDrag onDragend onDragenter onDragexit onDragleave onDragover onDragstart onDrop onMousedown
onMouseenter onMouseleave onMousemove onMouseout onMouseover onMouseup onSelect onTouchcancel
onTouchend onTouchmove onTouchstart onScroll onWheel onAbort onCanplay onCanplaythrough
onDurationchange onEmptied onEncrypted onEnded onError onLoadeddata onLoadedmetadata
onLoadstart onPause onPlay onPlaying onProgress onRatechange onSeeked onSeeking onStalled onSuspend onTimeupdate onVolumechange onWaiting onLoad onError`;
const propList = `${attributes} ${eventsName}`.split(/[\s\n]+/);
@ -60,7 +60,7 @@ export default function pickAttrs(props, ariaOnly = false) {
// Data
(mergedConfig.data && match(key, dataPrefix)) ||
// Attr
(mergedConfig.attr && propList.includes(key))
(mergedConfig.attr && (propList.includes(key) || propList.includes(key.toLowerCase())))
) {
attrs[key] = props[key];
}

View File

@ -19,6 +19,8 @@ export type LiteralUnion<T extends U, U> = T | (U & {});
export type Data = Record<string, unknown>;
export type Key = string | number;
type DefaultFactory<T> = (props: Data) => T | null | undefined;
export interface PropOptions<T = any, D = T> {

View File

@ -2,7 +2,7 @@ import { filterEmpty } from './props-util';
import { cloneVNode } from 'vue';
import warning from './warning';
export function cloneElement(vnode, nodeProps = {}, override = true) {
export function cloneElement(vnode, nodeProps = {}, override = true, mergeRef = false) {
let ele = vnode;
if (Array.isArray(vnode)) {
ele = filterEmpty(vnode)[0];
@ -10,7 +10,7 @@ export function cloneElement(vnode, nodeProps = {}, override = true) {
if (!ele) {
return null;
}
const node = cloneVNode(ele, nodeProps);
const node = cloneVNode(ele, nodeProps, mergeRef);
// cloneVNode内部是合并属性这里改成覆盖属性
node.props = override ? { ...node.props, ...nodeProps } : node.props;

View File

@ -1,11 +1,14 @@
import PropTypes from '../_util/vue-types';
export default {
props: {
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
},
isSelectOptGroup: true,
render() {
return null;
},
};
import { FunctionalComponent } from 'vue';
import { OptionGroupData } from './interface';
export interface OptGroupProps extends Omit<OptionGroupData, 'options'> {}
export interface OptionGroupFC extends FunctionalComponent<OptGroupProps> {
/** Legacy for check if is a Option Group */
isSelectOptGroup: boolean;
}
const OptGroup: OptionGroupFC = () => null;
OptGroup.isSelectOptGroup = true;
export default OptGroup;

View File

@ -1,14 +1,17 @@
import PropTypes from '../_util/vue-types';
import { FunctionalComponent } from 'vue';
export default {
props: {
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
disabled: PropTypes.bool,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
},
isSelectOption: true,
render() {
return null;
},
};
import { OptionCoreData } from './interface';
export interface OptionProps extends Omit<OptionCoreData, '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;
export default Option;

View File

@ -5,8 +5,41 @@ import classNames from '../_util/classNames';
import pickAttrs from '../_util/pickAttrs';
import { isValidElement } from '../_util/props-util';
import createRef from '../_util/createRef';
import { computed, defineComponent, reactive, watch } from 'vue';
import { computed, defineComponent, reactive, VNodeChild, watch } from 'vue';
import List from '../vc-virtual-list/List';
import {
OptionsType as SelectOptionsType,
OptionData,
RenderNode,
OnActiveValue,
} from './interface';
import { RawValueType, FlattenOptionsType } from './interface/generator';
export interface OptionListProps {
prefixCls: string;
id: string;
options: SelectOptionsType;
flattenOptions: FlattenOptionsType<SelectOptionsType>;
height: number;
itemHeight: number;
values: Set<RawValueType>;
multiple: boolean;
open: boolean;
defaultActiveFirstOption?: boolean;
notFoundContent?: VNodeChild;
menuItemSelectedIcon?: RenderNode;
childrenAsData: boolean;
searchValue: string;
virtual: boolean;
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: EventHandlerNonNull;
/** Tell Select that mouse enter the popup to force re-render */
onMouseenter?: EventHandlerNonNull;
}
const OptionListProps = {
prefixCls: PropTypes.string,
@ -39,8 +72,7 @@ const OptionListProps = {
* Using virtual list of option display.
* Will fallback to dom if use customize render.
*/
const OptionList = defineComponent({
props: OptionListProps,
const OptionList = defineComponent<OptionListProps>({
name: 'OptionList',
inheritAttrs: false,
setup(props) {
@ -49,25 +81,25 @@ const OptionList = defineComponent({
// =========================== List ===========================
const listRef = createRef();
const onListMouseDown = event => {
const onListMouseDown: EventHandlerNonNull = event => {
event.preventDefault();
};
const scrollIntoView = index => {
const scrollIntoView = (index: number) => {
if (listRef.current) {
listRef.current.scrollTo({ index });
}
};
// ========================== Active ==========================
const getEnabledActiveIndex = (index, offset = 1) => {
const getEnabledActiveIndex = (index: number, offset = 1) => {
const len = props.flattenOptions.length;
for (let i = 0; i < len; i += 1) {
const current = (index + i * offset + len) % len;
const { group, data } = props.flattenOptions[current];
if (!group && !data.disabled) {
if (!group && !(data as OptionData).disabled) {
return current;
}
}
@ -78,9 +110,9 @@ const OptionList = defineComponent({
activeIndex: getEnabledActiveIndex(0),
});
const setActive = (index, fromKeyboard = false) => {
const setActive = (index: number, fromKeyboard = false) => {
state.activeIndex = index;
const info = { source: fromKeyboard ? 'keyboard' : 'mouse' };
const info = { source: fromKeyboard ? ('keyboard' as const) : ('mouse' as const) };
// Trigger active event
const flattenItem = props.flattenOptions[index];
@ -94,30 +126,38 @@ const OptionList = defineComponent({
// Auto active first item when list length or searchValue changed
watch([props.flattenOptions.length, props.searchValue], () => {
setActive(props.defaultActiveFirstOption !== false ? getEnabledActiveIndex(0) : -1);
});
watch(
computed(() => [props.flattenOptions.length, props.searchValue]),
() => {
setActive(props.defaultActiveFirstOption !== false ? getEnabledActiveIndex(0) : -1);
},
);
// Auto scroll to item position in single mode
watch(props.open, () => {
/**
* React will skip `onChange` when component update.
* `setActive` function will call root accessibility state update which makes re-render.
* So we need to delay to let Input component trigger onChange first.
*/
const timeoutId = setTimeout(() => {
if (!props.multiple && props.open && props.values.size === 1) {
const value = Array.from(props.values)[0];
const index = props.flattenOptions.findIndex(({ data }) => data.value === value);
setActive(index);
scrollIntoView(index);
}
});
return () => clearTimeout(timeoutId);
});
let timeoutId: number;
watch(
computed(() => props.open),
() => {
/**
* React will skip `onChange` when component update.
* `setActive` function will call root accessibility state update which makes re-render.
* So we need to delay to let Input component trigger onChange first.
*/
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
if (!props.multiple && props.open && props.values.size === 1) {
const value = Array.from(props.values)[0];
const index = props.flattenOptions.findIndex(({ data }) => data.value === value);
setActive(index);
scrollIntoView(index);
}
});
},
{ immediate: true, flush: 'post' },
);
// ========================== Values ==========================
const onSelectValue = value => {
const onSelectValue = (value?: RawValueType) => {
if (value !== undefined) {
props.onSelect(value, { selected: !props.values.has(value) });
}
@ -128,11 +168,11 @@ const OptionList = defineComponent({
}
};
function renderItem(index) {
function renderItem(index: number) {
const item = props.flattenOptions[index];
if (!item) return null;
const itemData = item.data || {};
const itemData = (item.data || {}) as OptionData;
const { value, label, children } = itemData;
const attrs = pickAttrs(itemData, true);
const mergedLabel = props.childrenAsData ? children : label;
@ -157,7 +197,7 @@ const OptionList = defineComponent({
itemPrefixCls,
setActive,
onSelectValue,
onKeydown: event => {
onKeydown: (event: KeyboardEvent) => {
const { which } = event;
switch (which) {
// >>> Arrow keys
@ -204,7 +244,7 @@ const OptionList = defineComponent({
},
onKeyup: () => {},
scrollTo: index => {
scrollTo: (index: number) => {
scrollIntoView(index);
},
};
@ -256,8 +296,7 @@ const OptionList = defineComponent({
onScroll={onScroll}
virtual={virtual}
onMouseenter={onMouseenter}
>
{({ group, groupOption, data }, itemIndex) => {
children={({ group, groupOption, data }, itemIndex) => {
const { label, key } = data;
// Group
@ -339,10 +378,12 @@ const OptionList = defineComponent({
</div>
);
}}
</List>
></List>
</>
);
},
});
OptionList.props = OptionListProps;
export default OptionList;

View File

@ -0,0 +1,92 @@
/**
* 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 { 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 generateSelector, { SelectProps } from './generate';
import { DefaultValueType } from './interface/generator';
import warningProps from './utils/warningPropsUtil';
import { defineComponent, ref } from 'vue';
import { getSlot } from '../_util/props-util';
import omit from 'lodash-es/omit';
const RefSelect = generateSelector<SelectOptionsType>({
prefixCls: 'rc-select',
components: {
optionList: SelectOptionList,
},
convertChildrenToData: convertSelectChildrenToData,
flattenOptions,
getLabeledValue: getSelectLabeledValue,
filterOptions: selectDefaultFilterOptions,
isValueDisabled: isSelectValueDisabled,
findValueOption: findSelectValueOption,
warningProps,
fillOptionsWithMissingValue,
});
export type ExportedSelectProps<
ValueType extends DefaultValueType = DefaultValueType
> = SelectProps<SelectOptionsType, ValueType>;
const Select = defineComponent<Omit<ExportedSelectProps, 'children'>>({
setup() {
const selectRef = ref(null);
return {
selectRef,
focus: () => {
selectRef.value?.focus();
},
blur: () => {
selectRef.value?.blur();
},
};
},
render() {
return <RefSelect ref="selectRef" {...this.$props} {...this.$attrs} children={getSlot(this)} />;
},
});
Select.inheritAttrs = false;
Select.props = omit(RefSelect.props, ['children']);
Select.Option = Option;
Select.OptGroup = OptGroup;
export default Select;

View File

@ -49,7 +49,7 @@ export interface SelectTriggerProps {
prefixCls: string;
disabled: boolean;
visible: boolean;
popupElement: VNodeChild;
popupElement: VNodeChild | JSX.Element;
animation?: string;
transitionName?: string;
containerWidth: number;
@ -61,7 +61,7 @@ export interface SelectTriggerProps {
getPopupContainer?: RenderDOMFunc;
dropdownAlign: object;
empty: boolean;
getTriggerDOMNode: () => HTMLElement;
getTriggerDOMNode: () => any;
}
const SelectTrigger = defineComponent<SelectTriggerProps>({
name: 'SelectTrigger',

View File

@ -0,0 +1,158 @@
import { cloneElement } from '../../_util/vnode';
import { defineComponent, inject, VNode, VNodeChild, withDirectives } from 'vue';
import PropTypes from '../../_util/vue-types';
import { RefObject } from '../../_util/createRef';
import antInput from '../../_util/antInputDirective';
interface InputProps {
prefixCls: string;
id: string;
inputElement: VNodeChild;
disabled: boolean;
autofocus: boolean;
autocomplete: string;
editable: boolean;
accessibilityIndex: number;
value: string;
open: boolean;
tabindex: number;
/** Pass accessibility props to input */
attrs: object;
inputRef: RefObject;
onKeydown: EventHandlerNonNull;
onMousedown: EventHandlerNonNull;
onChange: EventHandlerNonNull;
onPaste: EventHandlerNonNull;
onCompositionstart: EventHandlerNonNull;
onCompositionend: EventHandlerNonNull;
}
const Input = defineComponent<InputProps>({
name: 'Input',
inheritAttrs: false,
setup() {
return {
VCSelectContainerEvent: inject('VCSelectContainerEvent'),
};
},
render() {
const {
prefixCls,
id,
inputElement,
disabled,
tabindex,
autofocus,
autocomplete,
editable,
accessibilityIndex,
value,
onKeydown,
onMousedown,
onChange,
onPaste,
onCompositionstart,
onCompositionend,
open,
inputRef,
attrs,
} = this.$props as InputProps;
let inputNode: any = withDirectives((inputElement || <input />) as VNode, [[antInput]]);
const inputProps = inputNode.props || {};
const {
onKeydown: onOriginKeyDown,
onInput: onOriginInput,
onMousedown: onOriginMouseDown,
onCompositionstart: onOriginCompositionStart,
onCompositionend: onOriginCompositionEnd,
style,
} = inputProps;
inputNode = cloneElement(inputNode, {
id,
ref: inputRef,
disabled,
tabindex,
autocomplete: autocomplete || 'off',
type: 'search',
autofocus,
class: `${prefixCls}-selection-search-input`,
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': `${id}_list_${accessibilityIndex}`,
...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[]) => {
this.VCSelectContainerEvent?.focus(args[0]);
},
onBlur: (...args: any[]) => {
this.VCSelectContainerEvent?.blur(args[0]);
},
}) as VNode;
return inputNode;
},
});
Input.props = {
inputRef: PropTypes.any,
prefixCls: PropTypes.string,
id: PropTypes.string,
inputElement: PropTypes.any,
disabled: PropTypes.bool,
autofocus: PropTypes.bool,
autocomplete: PropTypes.string,
editable: PropTypes.bool,
accessibilityIndex: PropTypes.number,
value: PropTypes.string,
open: PropTypes.bool,
tabindex: PropTypes.number,
/** 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,
};
export default Input;

View File

@ -0,0 +1,281 @@
import TransBtn from '../TransBtn';
import { LabelValueType, RawValueType, CustomTagProps } from '../interface/generator';
import { RenderNode } from '../interface';
import { InnerSelectorProps } from '.';
import Input from './Input';
import {
computed,
defineComponent,
onMounted,
ref,
TransitionGroup,
VNodeChild,
watch,
watchEffect,
Ref,
} from 'vue';
import classNames from '../../_util/classNames';
import pickAttrs from '../../_util/pickAttrs';
import PropTypes from '../../_util/vue-types';
import getTransitionProps from '../../_util/getTransitionProps';
const REST_TAG_KEY = '__RC_SELECT_MAX_REST_COUNT__';
interface SelectorProps extends InnerSelectorProps {
// Icon
removeIcon?: RenderNode;
// Tags
maxTagCount?: number;
maxTagTextLength?: number;
maxTagPlaceholder?: VNodeChild;
tokenSeparators?: string[];
tagRender?: (props: CustomTagProps) => VNodeChild;
// Motion
choiceTransitionName?: string;
// Event
onSelect: (value: RawValueType, option: { selected: boolean }) => void;
}
const props = {
id: PropTypes.string,
prefixCls: PropTypes.string,
values: PropTypes.array,
open: PropTypes.bool,
searchValue: PropTypes.string,
inputRef: PropTypes.any,
placeholder: PropTypes.any,
disabled: PropTypes.bool,
mode: PropTypes.string,
showSearch: PropTypes.bool,
autofocus: PropTypes.bool,
autocomplete: PropTypes.string,
accessibilityIndex: PropTypes.number,
tabindex: PropTypes.number,
removeIcon: PropTypes.bool,
choiceTransitionName: PropTypes.string,
maxTagCount: PropTypes.number,
maxTagTextLength: PropTypes.number,
maxTagPlaceholder: PropTypes.any.def(
(omittedValues: LabelValueType[]) => `+ ${omittedValues.length} ...`,
),
tagRender: PropTypes.func,
onSelect: PropTypes.func,
onInputChange: PropTypes.func,
onInputPaste: PropTypes.func,
onInputKeyDown: PropTypes.func,
onInputMouseDown: PropTypes.func,
onInputCompositionStart: PropTypes.func,
onInputCompositionEnd: PropTypes.func,
};
const SelectSelector = defineComponent<SelectorProps>({
name: 'SelectSelector',
setup(props) {
let motionAppear = false; // not need use ref, because not need trigger watchEffect
const measureRef = ref();
const inputWidth = ref(0);
// ===================== Motion ======================
onMounted(() => {
motionAppear = true;
});
// ===================== Search ======================
const inputValue = computed(() =>
props.open || props.mode === 'tags' ? props.searchValue : '',
);
const inputEditable: Ref<boolean> = computed(
() => props.mode === 'tags' || ((props.open && props.showSearch) as boolean),
);
// We measure width and set to the input immediately
watch(
inputValue,
() => {
inputWidth.value = measureRef.value.scrollWidth;
},
{ flush: 'pre' },
);
const selectionNode = ref();
watchEffect(() => {
const {
values,
prefixCls,
removeIcon,
choiceTransitionName,
maxTagCount,
maxTagTextLength,
maxTagPlaceholder = (omittedValues: LabelValueType[]) => `+ ${omittedValues.length} ...`,
tagRender,
onSelect,
} = props;
// ==================== Selection ====================
let displayValues: LabelValueType[] = values;
// Cut by `maxTagCount`
let restCount: number;
if (typeof maxTagCount === 'number') {
restCount = values.length - maxTagCount;
displayValues = values.slice(0, maxTagCount);
}
// Update by `maxTagTextLength`
if (typeof maxTagTextLength === 'number') {
displayValues = displayValues.map(({ label, ...rest }) => {
let displayLabel = label;
if (typeof label === 'string' || typeof label === 'number') {
const strLabel = String(displayLabel);
if (strLabel.length > maxTagTextLength) {
displayLabel = `${strLabel.slice(0, maxTagTextLength)}...`;
}
}
return {
...rest,
label: displayLabel,
};
});
}
// Fill rest
if (restCount > 0) {
displayValues.push({
key: REST_TAG_KEY,
label:
typeof maxTagPlaceholder === 'function'
? maxTagPlaceholder(values.slice(maxTagCount))
: maxTagPlaceholder,
});
}
const transitionProps = getTransitionProps(choiceTransitionName, {
appear: motionAppear,
});
selectionNode.value = (
<TransitionGroup {...transitionProps}>
{displayValues.map(
({ key, label, value, disabled: itemDisabled, class: className, style }) => {
const mergedKey = key || value;
const closable = key !== REST_TAG_KEY && !itemDisabled;
const onMousedown = (event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
};
const onClose = (event?: MouseEvent) => {
if (event) event.stopPropagation();
onSelect(value as RawValueType, { selected: false });
};
return typeof tagRender === 'function' ? (
<span
key={mergedKey as string}
onMousedown={onMousedown}
class={className}
style={style}
>
{tagRender({
label,
value,
disabled: itemDisabled,
closable,
onClose,
} as CustomTagProps)}
</span>
) : (
<span
key={mergedKey as string}
class={classNames(className, `${prefixCls}-selection-item`, {
[`${prefixCls}-selection-item-disabled`]: itemDisabled,
})}
style={style}
>
<span class={`${prefixCls}-selection-item-content`}>{label}</span>
{closable && (
<TransBtn
class={`${prefixCls}-selection-item-remove`}
onMousedown={onMousedown}
onClick={onClose}
customizeIcon={removeIcon}
>
×
</TransBtn>
)}
</span>
);
},
)}
{}
</TransitionGroup>
);
});
return () => {
const {
id,
prefixCls,
values,
open,
inputRef,
placeholder,
disabled,
autofocus,
autocomplete,
accessibilityIndex,
tabindex,
onInputChange,
onInputPaste,
onInputKeyDown,
onInputMouseDown,
onInputCompositionStart,
onInputCompositionEnd,
} = props;
return (
<>
{selectionNode.value}
<span class={`${prefixCls}-selection-search`} style={{ width: inputWidth.value + 'px' }}>
<Input
inputRef={inputRef}
open={open}
prefixCls={prefixCls}
id={id}
inputElement={null}
disabled={disabled}
autofocus={autofocus}
autocomplete={autocomplete}
editable={inputEditable.value as boolean}
accessibilityIndex={accessibilityIndex}
value={inputValue.value}
onKeydown={onInputKeyDown}
onMousedown={onInputMouseDown}
onChange={onInputChange}
onPaste={onInputPaste}
onCompositionstart={onInputCompositionStart}
onCompositionend={onInputCompositionEnd}
tabindex={tabindex}
attrs={pickAttrs(props, true)}
/>
{/* Measure Node */}
<span ref={measureRef} class={`${prefixCls}-selection-search-mirror`} aria-hidden>
{inputValue.value}&nbsp;
</span>
</span>
{!values.length && !inputValue.value && (
<span class={`${prefixCls}-selection-placeholder`}>{placeholder}</span>
)}
</>
);
};
},
});
SelectSelector.inheritAttrs = false;
SelectSelector.props = props;
export default SelectSelector;

View File

@ -0,0 +1,143 @@
import pickAttrs from '../../_util/pickAttrs';
import Input from './Input';
import { InnerSelectorProps } from '.';
import { computed, defineComponent, ref, VNodeChild, watch } from 'vue';
import PropTypes from '../../_util/vue-types';
interface SelectorProps extends InnerSelectorProps {
inputElement: VNodeChild;
activeValue: string;
backfill?: boolean;
}
const props = {
inputElement: PropTypes.any,
id: PropTypes.string,
prefixCls: PropTypes.string,
values: PropTypes.array,
open: PropTypes.bool,
searchValue: PropTypes.string,
inputRef: PropTypes.any,
placeholder: PropTypes.any,
disabled: PropTypes.bool,
mode: PropTypes.string,
showSearch: PropTypes.bool,
autofocus: PropTypes.bool,
autocomplete: PropTypes.string,
accessibilityIndex: PropTypes.number,
tabindex: PropTypes.number,
activeValue: PropTypes.string,
backfill: PropTypes.bool,
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;
});
watch(
computed(() => [combobox.value, props.activeValue]),
() => {
if (combobox.value) {
inputChanged.value = false;
}
},
);
// Not show text when closed expect combobox mode
const hasTextInput = computed(() =>
props.mode !== 'combobox' && !props.open ? 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;
});
return () => {
const {
inputElement,
prefixCls,
id,
values,
inputRef,
disabled,
autofocus,
autocomplete,
accessibilityIndex,
open,
placeholder,
tabindex,
onInputKeyDown,
onInputMouseDown,
onInputChange,
onInputPaste,
onInputCompositionStart,
onInputCompositionEnd,
} = props;
const item = values[0];
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}
accessibilityIndex={accessibilityIndex}
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}>
{Array.isArray(item.label) ? item.label.map(la => la) : item.label}
</span>
)}
{/* Display placeholder */}
{!item && !hasTextInput.value && (
<span class={`${prefixCls}-selection-placeholder`}>{placeholder}</span>
)}
</>
);
};
},
});
SingleSelector.props = props;
SingleSelector.inheritAttrs = false;
export default SingleSelector;

View File

@ -0,0 +1,298 @@
/**
* 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 { LabelValueType, RawValueType, CustomTagProps } from '../interface/generator';
import { RenderNode, Mode } from '../interface';
import useLock from '../hooks/useLock';
import { defineComponent, VNode, VNodeChild } from 'vue';
import createRef, { RefObject } from '../../_util/createRef';
import PropTypes from '../../_util/vue-types copy';
export interface InnerSelectorProps {
prefixCls: string;
id: string;
mode: Mode;
inputRef: RefObject;
placeholder?: VNodeChild;
disabled?: boolean;
autofocus?: boolean;
autocomplete?: string;
values: LabelValueType[];
showSearch?: boolean;
searchValue: string;
accessibilityIndex: number;
open: boolean;
tabindex?: number;
onInputKeyDown: EventHandlerNonNull;
onInputMouseDown: EventHandlerNonNull;
onInputChange: EventHandlerNonNull;
onInputPaste: EventHandlerNonNull;
onInputCompositionStart: EventHandlerNonNull;
onInputCompositionEnd: EventHandlerNonNull;
}
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;
mode: Mode;
searchValue: string;
activeValue: string;
inputElement: JSX.Element;
autofocus?: boolean;
accessibilityIndex: number;
tabindex?: number;
disabled?: boolean;
placeholder?: VNodeChild;
removeIcon?: RenderNode;
// Tags
maxTagCount?: number;
maxTagTextLength?: number;
maxTagPlaceholder?: VNodeChild;
tagRender?: (props: CustomTagProps) => VNode;
/** Check if `tokenSeparators` contains `\n` or `\r\n` */
tokenWithEnter?: boolean;
// Motion
choiceTransitionName?: string;
onToggleOpen: (open?: boolean) => void;
/** `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;
onInputKeyDown?: EventHandlerNonNull;
/**
* @private get real dom for trigger align.
* This may be removed after React provides replacement of `findDOMNode`
*/
domRef: () => HTMLDivElement;
}
const Selector = defineComponent<SelectorProps>({
name: 'Selector',
setup(props) {
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 (![KeyCode.SHIFT, KeyCode.TAB, KeyCode.BACKSPACE, KeyCode.ESC].includes(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 = () => {
compositionStatus = false;
};
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/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();
}
};
return {
focus: () => {
inputRef.current.focus();
},
blur: () => {
inputRef.current.blur();
},
onMousedown,
onClick,
onInputPaste,
inputRef,
onInternalInputKeyDown,
onInternalInputMouseDown,
onInputChange,
onInputCompositionEnd,
onInputCompositionStart,
};
},
render() {
const { prefixCls, domRef, multiple } = this.$props as SelectorProps;
const {
onMousedown,
onClick,
inputRef,
onInputPaste,
onInternalInputKeyDown,
onInternalInputMouseDown,
onInputChange,
onInputCompositionStart,
onInputCompositionEnd,
} = this;
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>
);
},
});
Selector.inheritAttrs = false;
Selector.props = {
id: PropTypes.string,
prefixCls: PropTypes.string,
showSearch: PropTypes.bool,
open: PropTypes.bool,
/** Display in the Selector value, it's not same as `value` prop */
values: PropTypes.array,
multiple: PropTypes.bool,
mode: PropTypes.string,
searchValue: PropTypes.string,
activeValue: PropTypes.string,
inputElement: PropTypes.any,
autofocus: PropTypes.bool,
accessibilityIndex: PropTypes.number,
tabindex: PropTypes.number,
disabled: PropTypes.bool,
placeholder: PropTypes.any,
removeIcon: PropTypes.any,
// Tags
maxTagCount: PropTypes.number,
maxTagTextLength: PropTypes.number,
maxTagPlaceholder: PropTypes.any,
tagRender: PropTypes.func,
/** Check if `tokenSeparators` contains `\n` or `\r\n` */
tokenWithEnter: PropTypes.bool,
// Motion
choiceTransitionName: PropTypes.string,
onToggleOpen: PropTypes.func,
/** `onSearch` returns go next step boolean to check if need do toggle open */
onSearch: PropTypes.func,
onSearchSubmit: PropTypes.func,
onSelect: PropTypes.func,
onInputKeyDown: PropTypes.func,
/**
* @private get real dom for trigger align.
* This may be removed after React provides replacement of `findDOMNode`
*/
domRef: PropTypes.func,
};
export default Selector;

View File

@ -0,0 +1,345 @@
@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);
}
}

View File

@ -0,0 +1,10 @@
// input {
// // height: 24px;
// // line-height: 24px;
// border: 1px solid #333;
// border-radius: 4px;
// }
// button {
// border: 1px solid #333;
// }

View File

@ -0,0 +1,35 @@
import jsonp from 'jsonp';
import querystring from 'querystring';
let timeout;
let currentValue;
export function fetch(value, callback) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
currentValue = value;
function fake() {
const str = querystring.encode({
code: 'utf-8',
q: value,
});
jsonp(`http://suggest.taobao.com/sug?${str}`, (err, d) => {
if (currentValue === value) {
const { result } = d;
const data = [];
result.forEach(r => {
data.push({
value: r[0],
text: r[0],
});
});
callback(data);
}
});
}
timeout = setTimeout(fake, 300);
}

View File

@ -0,0 +1,3 @@
.test-option {
font-weight: bolder;
}

View File

@ -0,0 +1,154 @@
import { defineComponent } from 'vue';
/* eslint-disable no-console */
import Select, { Option } from '..';
import '../assets/index.less';
import './single.less';
const Test = defineComponent({
data() {
return {
destroy: false,
value: '9',
};
},
methods: {
onChange(e) {
let value;
if (e && e.target) {
({ value } = e.target);
} else {
value = e;
}
console.log('onChange', value);
this.value = value;
},
onDestroy() {
this.destroy = 1;
},
onBlur(v) {
console.log('onBlur', v);
},
onFocus() {
console.log('onFocus');
},
onSearch(val) {
console.log('Search:', val);
},
},
render() {
const { value, destroy } = this;
if (destroy) {
return null;
}
return (
<div style={{ margin: '20px' }}>
<div
style={{ height: '150px', background: 'rgba(0, 255, 0, 0.1)' }}
onMousedown={e => {
e.preventDefault();
}}
>
Prevent Default
</div>
<h2>Single Select</h2>
<div style={{ width: '300px' }}>
<Select
autofocus
id="my-select"
value={value}
placeholder="placeholder"
showSearch
style={{ width: '500px' }}
onBlur={this.onBlur}
onFocus={this.onFocus}
onSearch={this.onSearch}
allowClear
optionFilterProp="text"
onChange={this.onChange}
onPopupScroll={() => {
console.log('Scroll!');
}}
>
<Option value={null}></Option>,
<Option value="01" text="jack" title="jack">
<b
style={{
color: 'red',
}}
>
jack
</b>
</Option>
,
<Option value="11" text="lucy">
<span>lucy</span>
</Option>
,
<Option value="21" disabled text="disabled">
disabled
</Option>
,
<Option value="31" text="yiminghe" class="test-option" style={{ background: 'yellow' }}>
yiminghe
</Option>
,
{[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(i => (
<Option key={i} value={String(i)} text={String(i)}>
{i}-text
</Option>
))}
</Select>
</div>
<h2>native select</h2>
<select value={value} style={{ width: '500px' }} onChange={this.onChange}>
<option value="01">jack</option>
<option value="11">lucy</option>
<option value="21" disabled>
disabled
</option>
<option value="31">yiminghe</option>
{[1, 2, 3, 4, 5, 6, 7, 8, 9].map(i => (
<option value={i} key={i}>
{i}
</option>
))}
</select>
<h2>RTL Select</h2>
<div style={{ width: '300px' }}>
<Select
id="my-select-rtl"
placeholder="rtl"
direction="rtl"
dropdownMatchSelectWidth={300}
dropdownStyle={{ minWidth: '300px' }}
style={{ width: '500px' }}
>
<Option value="1">1</Option>
<Option value="2">2</Option>
</Select>
</div>
<p>
<button type="button" onClick={this.onDestroy}>
destroy
</button>
</p>
</div>
);
},
});
export default Test;
/* eslint-enable */

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,35 @@
import { computed, ComputedRef, Ref } from 'vue';
import { 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.value === item.label && cacheLabel) {
return {
...item,
label: cacheLabel,
};
}
return item;
});
prevValues = resultValues;
return resultValues;
});
return mergedValues;
}

View File

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

View File

@ -0,0 +1,32 @@
import { onBeforeUpdate, Ref, 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: number;
const cancelLatest = () => {
window.clearTimeout(delay);
};
onBeforeUpdate(() => {
cancelLatest();
});
const delaySetBool = (value: boolean, callback: () => void) => {
cancelLatest();
delay = window.setTimeout(() => {
bool.value = value;
if (callback) {
callback();
}
}, timeout);
};
return [bool, delaySetBool, cancelLatest];
}

View File

@ -0,0 +1,29 @@
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: number;
onBeforeUpdate(() => {
window.clearTimeout(timeout);
});
function doLock(locked: boolean) {
if (locked || lock === null) {
lock = locked;
}
window.clearTimeout(timeout);
timeout = window.setTimeout(() => {
lock = null;
}, duration);
}
return [() => lock, doLock];
}

View File

@ -0,0 +1,26 @@
import { onBeforeUnmount, onMounted, Ref } from 'vue';
export default function useSelectTriggerControl(
elements: (HTMLElement | undefined)[],
open: Ref<boolean>,
triggerOpen: (open: boolean) => void,
) {
function onGlobalMouseDown(event: MouseEvent) {
const target = event.target as HTMLElement;
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,9 +0,0 @@
// based on vc-select 9.2.2
import Select from './Select';
import Option from './Option';
import { SelectPropTypes } from './PropTypes';
import OptGroup from './OptGroup';
Select.Option = Option;
Select.OptGroup = OptGroup;
export { Select, Option, OptGroup, SelectPropTypes };
export default Select;

View File

@ -0,0 +1,7 @@
import Select, { ExportedSelectProps } from './Select';
import Option from './Option';
import OptGroup from './OptGroup';
type SelectProps = ExportedSelectProps;
export { Option, OptGroup, SelectProps };
export default Select;

View File

@ -1,5 +1,4 @@
import * as Vue from 'vue';
import { SelectProps, RefSelectProps } from '../generate';
export type SelectSource = 'option' | 'selection' | 'input';
@ -8,9 +7,9 @@ export const INTERNAL_PROPS_MARK = 'RC_SELECT_INTERNAL_PROPS_MARK';
// =================================== Shared Type ===================================
export type Key = string | number;
export type RawValueType = string | number;
export type RawValueType = string | number | null;
export interface LabelValueType {
export interface LabelValueType extends Record<string, any> {
key?: Key;
value?: RawValueType;
label?: Vue.VNodeChild;

View File

@ -1,4 +1,5 @@
import * as Vue from 'vue';
import { VNode } from 'vue';
import { Key, RawValueType } from './generator';
export type RenderDOMFunc = (props: any) => HTMLElement;
@ -19,12 +20,11 @@ export interface OptionCoreData {
disabled?: boolean;
value: Key;
title?: string;
className?: string;
class?: string;
style?: Vue.CSSProperties;
label?: Vue.VNodeChild;
/** @deprecated Only works when use `children` as option data */
children?: Vue.VNodeChild;
children?: VNode[] | JSX.Element[];
}
export interface OptionData extends OptionCoreData {
@ -36,7 +36,6 @@ export interface OptionGroupData {
key?: Key;
label?: Vue.VNodeChild;
options: OptionData[];
className?: string;
class?: string;
style?: Vue.CSSProperties;

View File

@ -0,0 +1,120 @@
import {
RawValueType,
GetLabeledValue,
LabelValueType,
DefaultValueType,
FlattenOptionsType,
} from '../interface/generator';
export function toArray<T>(value: T | T[]): T[] {
if (Array.isArray(value)) {
return value;
}
return value !== undefined ? [value] : [];
}
/**
* Convert outer props value into internal value
*/
export function toInnerValue(
value: DefaultValueType,
{ labelInValue, combobox }: { labelInValue: boolean; combobox: boolean },
): RawValueType[] {
if (value === undefined || (value === '' && combobox)) {
return [];
}
const values = Array.isArray(value) ? value : [value];
if (labelInValue) {
return (values as LabelValueType[]).map(({ key, value: val }: LabelValueType) =>
val !== undefined ? val : key,
);
}
return values as RawValueType[];
}
/**
* Convert internal value into out event value
*/
export function toOuterValues<FOT extends FlattenOptionsType>(
valueList: RawValueType[],
{
optionLabelProp,
labelInValue,
prevValue,
options,
getLabeledValue,
}: {
optionLabelProp: string;
labelInValue: boolean;
getLabeledValue: GetLabeledValue<FOT>;
options: FOT;
prevValue: DefaultValueType;
},
): RawValueType[] | LabelValueType[] | DefaultValueType {
let values: DefaultValueType = valueList;
if (labelInValue) {
values = values.map(val =>
getLabeledValue(val, {
options,
prevValue,
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

@ -0,0 +1,47 @@
import { flattenChildren, isValidElement } from '../../_util/props-util';
import { VNode } from 'vue';
import { OptionData, OptionGroupData, OptionsType } from '../interface';
function convertNodeToOption(node: VNode): OptionData {
const {
key,
children,
props: { value, ...restProps },
} = node as VNode & {
children: { default?: () => any };
};
const child = children.default ? children.default() : undefined;
return { key, value: value !== undefined ? value : key, children: child, ...restProps };
}
export function convertChildrenToData(nodes: any[], optionOnly = false): OptionsType {
const dd = flattenChildren(nodes)
.map((node: VNode, index: number): OptionData | OptionGroupData | null => {
if (!isValidElement(node) || !node.type) {
return null;
}
const {
type: { isSelectOptGroup },
key,
children,
props,
} = node as VNode & {
type: { isSelectOptGroup?: boolean };
children: { default?: () => any };
};
if (optionOnly || !isSelectOptGroup) {
return convertNodeToOption(node);
}
const child = children.default ? children.default() : undefined;
return {
key: `__RC_SELECT_GRP__${key === null ? index : key}__`,
label: key,
...props,
options: convertChildrenToData(child || []),
} as any;
})
.filter(data => data);
return dd;
}

View File

@ -0,0 +1,312 @@
import { warning } from '../../vc-util/warning';
import { VNodeChild } from 'vue';
import {
OptionsType as SelectOptionsType,
OptionData,
OptionGroupData,
FlattenOptionData,
} from '../interface';
import {
LabelValueType,
FilterFunc,
RawValueType,
GetLabeledValue,
DefaultValueType,
} from '../interface/generator';
import { toArray } from './commonUtil';
function getKey(data: OptionData | OptionGroupData, 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}`;
}
/**
* 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(options: SelectOptionsType): FlattenOptionData[] {
const flattenList: FlattenOptionData[] = [];
function dig(list: SelectOptionsType, isGroupOption: boolean) {
list.forEach(data => {
if (isGroupOption || !('options' in data)) {
// Option
flattenList.push({
key: getKey(data, flattenList.length),
groupOption: isGroupOption,
data,
});
} else {
// Option Group
flattenList.push({
key: getKey(data, flattenList.length),
group: true,
data,
});
dig(data.options, true);
}
});
}
dig(options, false);
return flattenList;
}
/**
* Inject `props` into `option` for legacy usage
*/
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 findValueOption(
values: RawValueType[],
options: FlattenOptionData[],
{ prevValueOptions = [] }: { prevValueOptions?: OptionData[] } = {},
): OptionData[] {
const optionMap: Map<RawValueType, OptionData> = new Map();
options.forEach(flattenItem => {
if (!flattenItem.group) {
const data = flattenItem.data as OptionData;
// Check if match
optionMap.set(data.value, data);
}
});
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, prevValue, labelInValue, optionLabelProp },
) => {
const item = findValueOption([value], options)[0];
const result: LabelValueType = {
value,
};
let prevValItem: LabelValueType;
const prevValues = toArray<RawValueType | LabelValueType>(prevValue);
if (labelInValue) {
prevValItem = prevValues.find((prevItem: LabelValueType) => {
if (typeof prevItem === 'object' && 'value' in prevItem) {
return prevItem.value === value;
}
// [Legacy] Support `key` as `value`
return prevItem.key === value;
}) as LabelValueType;
}
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) {
result.label = item[optionLabelProp];
} else {
result.label = value;
}
// Used for motion control
result.key = result.value;
return result;
};
function toRawString(content: VNodeChild): string {
return toArray(content).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;
}
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;
}
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

@ -0,0 +1,160 @@
import warning, { noteOnce } from '../../vc-util/warning';
import { SelectProps } from '..';
import { convertChildrenToData } from './legacyUtil';
import { OptionData, OptionGroupData } from '../interface';
import { toArray } from './commonUtil';
import { RawValueType, LabelValueType } from '../interface/generator';
import { isValidElement } from '../../_util/props-util';
import { VNode } from 'vue';
function warningProps(props: SelectProps) {
const {
mode,
options,
children,
backfill,
allowClear,
placeholder,
getInputElement,
showSearch,
onSearch,
defaultOpen,
autofocus,
labelInValue,
value,
inputValue,
optionLabelProp,
} = props;
const multiple = mode === 'multiple' || mode === 'tags';
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 } & OptionGroupData) => !opt.disabled),
'Please avoid setting option to disabled in tags mode since user can always type text as tag.',
);
// `combobox` & `tags` should option be `string` type
if (mode === 'tags' || mode === 'combobox') {
const hasNumberValue = mergedOptions.some(item => {
if (item.options) {
return item.options.some(
(opt: OptionData) => typeof ('value' in opt ? opt.value : opt.key) === 'number',
);
}
return typeof ('value' in item ? item.value : item.key) === 'number';
});
warning(
!hasNumberValue,
'`value` of Option should not use number type when `mode` is `tags` or `combobox`.',
);
}
// `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 | LabelValueType>(value);
warning(
!labelInValue ||
values.every(val => typeof val === 'object' && ('key' in val || 'value' in val)),
'`value` should in shape of `{ value: string | number, label?: ReactNode }` 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: VNode & {
children: { default?: () => 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

@ -401,7 +401,7 @@ export default defineComponent({
prefixCls,
destroyPopupOnHide,
visible: sPopupVisible,
point: alignPoint && point,
point: alignPoint ? point : null,
action,
align,
animation: popupAnimation,

View File

@ -1,5 +1,12 @@
import addDOMEventListener from 'add-dom-event-listener';
export default function addEventListenerWrap(target, eventType, cb, option) {
return addDOMEventListener(target, eventType, cb, option);
if (target.addEventListener) {
target.addEventListener(eventType, cb, option);
}
return {
remove: () => {
if (target.removeEventListener) {
target.removeEventListener(eventType, cb);
}
},
};
}

View File

@ -1,11 +0,0 @@
export default function contains(root, n) {
let node = n;
while (node) {
if (node === root) {
return true;
}
node = node.parentNode;
}
return false;
}

View File

@ -0,0 +1,7 @@
export default function contains(root: Node | null | undefined, n?: Node) {
if (!root) {
return false;
}
return root.contains(n);
}

View File

@ -1,14 +1,14 @@
/* eslint-disable no-console */
let warned = {};
let warned: Record<string, boolean> = {};
export function warning(valid, message) {
export function warning(valid: boolean, message: string) {
// Support uglify
if (process.env.NODE_ENV !== 'production' && !valid && console !== undefined) {
console.error(`Warning: ${message}`);
}
}
export function note(valid, message) {
export function note(valid: boolean, message: string) {
// Support uglify
if (process.env.NODE_ENV !== 'production' && !valid && console !== undefined) {
console.warn(`Note: ${message}`);
@ -19,18 +19,22 @@ export function resetWarned() {
warned = {};
}
export function call(method, valid, message) {
export function call(
method: (valid: boolean, message: string) => void,
valid: boolean,
message: string,
) {
if (!valid && !warned[message]) {
method(false, message);
warned[message] = true;
}
}
export function warningOnce(valid, message) {
export function warningOnce(valid: boolean, message: string) {
call(warning, valid, message);
}
export function noteOnce(valid, message) {
export function noteOnce(valid: boolean, message: string) {
call(note, valid, message);
}

View File

@ -1,6 +1,6 @@
import classNames from '../_util/classNames';
import ResizeObserver from '../vc-resize-observer';
import { CSSProperties, FunctionalComponent } from 'vue';
import { CSSProperties, FunctionalComponent, PropType } from 'vue';
interface FillerProps {
prefixCls?: string;
@ -59,5 +59,13 @@ const Filter: FunctionalComponent<FillerProps> = (
Filter.displayName = 'Filter';
Filter.inheritAttrs = false;
Filter.props = {
prefixCls: String,
/** Virtual filler height. Should be `count * itemMinHeight` */
height: Number,
/** Set offset of visible items. Should be the top of start item position */
offset: Number,
onInnerResize: Function as PropType<() => void>,
};
export default Filter;

View File

@ -1,4 +1,4 @@
import { cloneVNode, FunctionalComponent } from 'vue';
import { cloneVNode, FunctionalComponent, PropType } from 'vue';
export interface ItemProps {
setRef: (element: HTMLElement) => void;
@ -13,5 +13,10 @@ const Item: FunctionalComponent<ItemProps> = ({ setRef }, { slots }) => {
})
: children;
};
Item.props = {
setRef: {
type: Function as PropType<(element: HTMLElement) => void>,
default: () => {},
},
};
export default Item;

View File

@ -79,6 +79,8 @@ const List = defineComponent({
virtual: PropTypes.bool,
children: PropTypes.func,
onScroll: PropTypes.func,
onMousedown: PropTypes.func,
onMouseenter: PropTypes.func,
},
setup(props) {
// ================================= MISC =================================
@ -255,7 +257,7 @@ const List = defineComponent({
const removeEventListener = () => {
if (componentRef.value) {
componentRef.value.removeEventListener('wheel', onRawWheel);
componentRef.value.removeEventListener('DOMMouseScroll', onFireFoxScroll);
componentRef.value.removeEventListener('DOMMouseScroll' as any, onFireFoxScroll);
componentRef.value.removeEventListener('MozMousePixelScroll', onMozMousePixelScroll);
}
};
@ -264,7 +266,7 @@ const List = defineComponent({
if (componentRef.value) {
removeEventListener();
componentRef.value.addEventListener('wheel', onRawWheel);
componentRef.value.addEventListener('DOMMouseScroll', onFireFoxScroll);
componentRef.value.addEventListener('DOMMouseScroll' as any, onFireFoxScroll);
componentRef.value.addEventListener('MozMousePixelScroll', onMozMousePixelScroll);
}
});
@ -331,7 +333,7 @@ const List = defineComponent({
style,
class: className,
...restProps
} = { ...this.$props, ...this.$attrs };
} = { ...this.$props, ...this.$attrs } as any;
const mergedClassName = classNames(prefixCls, className);
const { scrollTop, mergedData } = this.state;
const { scrollHeight, offset, start, end } = this.calRes;

View File

@ -15,7 +15,7 @@ export default function useScrollTo(
) {
let scroll: number | null = null;
return arg => {
return (arg: any) => {
raf.cancel(scroll!);
const data = state.mergedData;
const itemHeight = props.itemHeight;

View File

@ -86,7 +86,7 @@
"@types/raf": "^3.4.0",
"@typescript-eslint/eslint-plugin": "^4.1.0",
"@typescript-eslint/parser": "^4.1.0",
"@vue/babel-plugin-jsx": "^1.0.0-rc.2",
"@vue/babel-plugin-jsx": "^1.0.0-rc.3",
"@vue/cli-plugin-eslint": "^4.0.0",
"@vue/compiler-sfc": "^3.0.0",
"@vue/eslint-config-prettier": "^6.0.0",

View File

@ -5,7 +5,7 @@
"ant-design-vue": ["components/index.tsx"],
"ant-design-vue/es/*": ["components/*"]
},
"strictNullChecks": true,
"strictNullChecks": false,
"moduleResolution": "node",
"esModuleInterop": true,
"experimentalDecorators": true,

View File

@ -33,7 +33,7 @@ const babelConfig = {
style: true,
},
],
['@vue/babel-plugin-jsx'],
['@vue/babel-plugin-jsx', { mergeProps: false }],
'@babel/plugin-proposal-optional-chaining',
'@babel/plugin-transform-object-assign',
'@babel/plugin-proposal-object-rest-spread',