feat: add select responsive

refactor-list
tangjinzhou 2021-06-22 10:47:33 +08:00
parent 501204c543
commit 656d14fc4e
21 changed files with 371 additions and 293 deletions

View File

@ -20,7 +20,7 @@ export interface LabeledValue {
label: VNodeChild;
}
export type SizeType = 'small' | 'middle' | 'large' | undefined;
export type SelectValue = RawValue | RawValue[] | LabeledValue | LabeledValue[];
export type SelectValue = RawValue | RawValue[] | LabeledValue | LabeledValue[] | undefined;
export interface InternalSelectProps<VT> extends Omit<RcSelectProps<VT>, 'mode'> {
suffixIcon?: VNodeChild;

View File

@ -37,6 +37,10 @@
background: @input-disabled-bg;
cursor: not-allowed;
.@{select-prefix-cls}-multiple& {
background: @select-multiple-disabled-background;
}
input {
cursor: not-allowed;
}
@ -66,7 +70,12 @@
display: inline-block;
cursor: pointer;
&:not(.@{select-prefix-cls}-disabled):hover &-selector {
&:not(&-customize-input) &-selector {
.select-selector();
.select-search-input-without-border();
}
&:not(&-disabled):hover &-selector {
.hover();
}
@ -93,6 +102,7 @@
color: @input-placeholder-color;
white-space: nowrap;
text-overflow: ellipsis;
pointer-events: none;
// IE11 css hack. `*::-ms-backdrop,` is a must have
@media all and (-ms-high-contrast: none) {
@ -189,21 +199,21 @@
outline: none;
box-shadow: @box-shadow-base;
&.slide-up-enter.slide-up-enter-active&-placement-bottomLeft,
&.slide-up-appear.slide-up-appear-active&-placement-bottomLeft {
&.@{ant-prefix}-slide-up-enter.@{ant-prefix}-slide-up-enter-active&-placement-bottomLeft,
&.@{ant-prefix}-slide-up-appear.@{ant-prefix}-slide-up-appear-active&-placement-bottomLeft {
animation-name: antSlideUpIn;
}
&.slide-up-enter.slide-up-enter-active&-placement-topLeft,
&.slide-up-appear.slide-up-appear-active&-placement-topLeft {
&.@{ant-prefix}-slide-up-enter.@{ant-prefix}-slide-up-enter-active&-placement-topLeft,
&.@{ant-prefix}-slide-up-appear.@{ant-prefix}-slide-up-appear-active&-placement-topLeft {
animation-name: antSlideDownIn;
}
&.slide-up-leave.slide-up-leave-active&-placement-bottomLeft {
&.@{ant-prefix}-slide-up-leave.@{ant-prefix}-slide-up-leave-active&-placement-bottomLeft {
animation-name: antSlideUpOut;
}
&.slide-up-leave.slide-up-leave-active&-placement-topLeft {
&.@{ant-prefix}-slide-up-leave.@{ant-prefix}-slide-up-leave-active&-placement-topLeft {
animation-name: antSlideDownOut;
}

View File

@ -1,5 +1,6 @@
@import './index';
@select-overflow-prefix-cls: ~'@{select-prefix-cls}-selection-overflow';
@select-multiple-item-border-width: 1px;
@select-multiple-padding: max(
@ -13,13 +14,25 @@
* since chrome may update to redesign with its align logic.
*/
// =========================== Overflow ===========================
.@{select-overflow-prefix-cls} {
position: relative;
display: flex;
flex: auto;
flex-wrap: wrap;
max-width: 100%;
&-item {
flex: none;
align-self: center;
max-width: 100%;
}
}
.@{select-prefix-cls} {
&-multiple {
// ========================= Selector =========================
.@{select-prefix-cls}-selector {
.select-selector();
.select-search-input-without-border();
display: flex;
flex-wrap: wrap;
align-items: center;
@ -59,9 +72,7 @@
height: @select-multiple-item-height;
margin-top: @select-multiple-item-spacing-half;
margin-right: @input-padding-vertical-base;
margin-bottom: @select-multiple-item-spacing-half;
padding: 0 (@padding-xs / 2) 0 @padding-xs;
line-height: @select-multiple-item-height - @select-multiple-item-border-width * 2;
background: @select-selection-item-bg;
border: 1px solid @select-selection-item-border-color;
@ -69,6 +80,9 @@
cursor: default;
transition: font-size 0.3s, line-height 0.3s, height 0.3s;
user-select: none;
margin-inline-end: @input-padding-vertical-base;
padding-inline-start: @padding-xs;
padding-inline-end: (@padding-xs / 2);
.@{select-prefix-cls}-disabled& {
color: @select-multiple-item-disabled-color;
@ -81,7 +95,7 @@
display: inline-block;
margin-right: (@padding-xs / 2);
overflow: hidden;
white-space: nowrap;
white-space: pre; // fix whitespace wrapping. custom tags display all whitespace within.
text-overflow: ellipsis;
}
@ -90,10 +104,9 @@
display: inline-block;
color: @text-color-secondary;
font-weight: bold;
font-size: @font-size-sm;
font-size: 10px;
line-height: inherit;
cursor: pointer;
.iconfont-size-under-12px(10px);
> .@{iconfont-css-prefix} {
vertical-align: -0.2em;
@ -106,14 +119,24 @@
}
// ========================== Input ==========================
.@{select-overflow-prefix-cls}-item + .@{select-overflow-prefix-cls}-item {
.@{select-prefix-cls}-selection-search {
margin-inline-start: 0;
}
}
.@{select-prefix-cls}-selection-search {
position: relative;
margin-left: (@select-multiple-padding / 2);
max-width: 100%;
margin-top: @select-multiple-item-spacing-half;
margin-bottom: @select-multiple-item-spacing-half;
margin-inline-start: @input-padding-horizontal-base - @input-padding-vertical-base;
&-input,
&-mirror {
height: @select-multiple-item-height;
font-family: @font-family;
line-height: @line-height-base;
line-height: @select-multiple-item-height;
transition: all 0.3s;
}
@ -127,14 +150,9 @@
top: 0;
left: 0;
z-index: 999;
white-space: nowrap;
white-space: pre; // fix whitespace wrapping caused width calculation bug
visibility: hidden;
}
// https://github.com/ant-design/ant-design/issues/22906
&:first-child .@{select-prefix-cls}-selection-search-input {
margin-left: 6.5px;
}
}
// ======================= Placeholder =======================
@ -166,8 +184,8 @@
}
.@{select-prefix-cls}-selection-search {
height: @select-selection-height + @select-multiple-padding;
line-height: @select-selection-height + @select-multiple-padding;
height: @select-selection-height;
line-height: @select-selection-height;
&-input,
&-mirror {
@ -186,10 +204,9 @@
.@{select-prefix-cls}-selection-placeholder {
left: @input-padding-horizontal-sm;
}
// https://github.com/ant-design/ant-design/issues/22906
.@{select-prefix-cls}-selection-search:first-child
.@{select-prefix-cls}-selection-search-input {
margin-left: 3px;
// https://github.com/ant-design/ant-design/issues/29559
.@{select-prefix-cls}-selection-search {
margin-inline-start: 3px;
}
}
&.@{select-prefix-cls}-lg {

View File

@ -66,9 +66,6 @@
// ======================== Selections ========================
.@{select-prefix-cls}-selection-item {
.@{select-prefix-cls}-rtl& {
margin-right: 0;
margin-left: @input-padding-vertical-base;
padding: 0 @padding-xs 0 (@padding-xs / 2);
text-align: right;
}
// It's ok not to do this, but 24px makes bottom narrow in view should adjust
@ -83,11 +80,6 @@
// ========================== Input ==========================
.@{select-prefix-cls}-selection-search {
.@{select-prefix-cls}-rtl& {
margin-right: (@select-multiple-padding / 2);
margin-left: @input-padding-vertical-base;
}
&-mirror {
.@{select-prefix-cls}-rtl& {
right: 0;
@ -119,7 +111,7 @@
}
// single
@selection-item-padding: ceil((@font-size-base * 1.25));
@selection-item-padding: ceil(@font-size-base * 1.25);
.@{select-prefix-cls}-single {
// ========================= Selector =========================
@ -150,18 +142,6 @@
}
}
// ========================== Input ==========================
// We only change the style of non-customize input which is only support by `combobox` mode.
// Not customize
&:not(.@{select-prefix-cls}-customize-input) {
.@{select-prefix-cls}-selector {
.@{select-prefix-cls}-rtl& {
padding: 0 @input-padding-horizontal-base;
}
}
}
// ============================================================
// == Size ==
// ============================================================
@ -172,7 +152,7 @@
// With arrow should provides `padding-right` to show the arrow
&.@{select-prefix-cls}-show-arrow .@{select-prefix-cls}-selection-search {
.@{select-prefix-cls}-rtl& {
right: 0;
right: @input-padding-horizontal-sm - 1px;
}
}

View File

@ -1,6 +1,6 @@
@import './index';
@selection-item-padding: ceil((@font-size-base * 1.25));
@selection-item-padding: ceil(@font-size-base * 1.25);
.@{select-prefix-cls}-single {
// ========================= Selector =========================
@ -76,10 +76,7 @@
// Not customize
&:not(.@{select-prefix-cls}-customize-input) {
.@{select-prefix-cls}-selector {
.select-selector();
.select-search-input-without-border();
width: 100%;
height: @input-height-base;
padding: 0 @input-padding-horizontal-base;

View File

@ -90,19 +90,17 @@ export default defineComponent({
</Component>
);
if (responsive) {
itemNode = (
<ResizeObserver
onResize={({ offsetWidth }) => {
internalRegisterSize(offsetWidth);
}}
>
{itemNode}
</ResizeObserver>
);
}
return itemNode;
// 使 disabled rerender
return (
<ResizeObserver
disabled={!responsive}
onResize={({ offsetWidth }) => {
internalRegisterSize(offsetWidth);
}}
>
{itemNode}
</ResizeObserver>
);
};
},
});

View File

@ -370,12 +370,12 @@ const Overflow = defineComponent({
)}
</Component>
);
if (isResponsive.value) {
overflowNode = <ResizeObserver onResize={onOverflowResize}>{overflowNode}</ResizeObserver>;
}
return overflowNode;
// 使 disabled rerender
return (
<ResizeObserver disabled={!isResponsive.value} onResize={onOverflowResize}>
{overflowNode}
</ResizeObserver>
);
};
},
});

View File

@ -237,6 +237,9 @@ const OptionList = defineComponent<OptionListProps, { state?: any }>({
// >>> Close
case KeyCode.ESC: {
props.onToggleOpen(false);
if (props.open) {
event.stopPropagation();
}
}
}
},

View File

@ -46,7 +46,6 @@ 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>({
@ -69,21 +68,27 @@ export type ExportedSelectProps<
> = SelectProps<SelectOptionsType, ValueType>;
const Select = defineComponent<Omit<ExportedSelectProps, 'children'>>({
setup() {
setup(props, { attrs, expose, slots }) {
const selectRef = ref(null);
return {
selectRef,
expose({
focus: () => {
selectRef.value?.focus();
},
blur: () => {
selectRef.value?.blur();
},
});
return () => {
return (
<RefSelect
ref={selectRef}
{...(props as any)}
{...attrs}
children={slots.default?.() || []}
/>
);
};
},
render() {
return <RefSelect ref="selectRef" {...this.$props} {...this.$attrs} children={getSlot(this)} />;
},
});
Select.inheritAttrs = false;
Select.props = omit(RefSelect.props, ['children']);

View File

@ -11,6 +11,7 @@ import {
import PropTypes from '../../_util/vue-types';
import { RefObject } from '../../_util/createRef';
import antInput from '../../_util/antInputDirective';
import classNames from 'ant-design-vue/es/_util/classNames';
interface InputProps {
prefixCls: string;
@ -33,6 +34,8 @@ interface InputProps {
onPaste: EventHandlerNonNull;
onCompositionstart: EventHandlerNonNull;
onCompositionend: EventHandlerNonNull;
onFocus: EventHandlerNonNull;
onBlur: EventHandlerNonNull;
}
const Input = defineComponent<InputProps, { VCSelectContainerEvent: any; blurTimeout: any }>({
@ -72,6 +75,8 @@ const Input = defineComponent<InputProps, { VCSelectContainerEvent: any; blurTim
onPaste,
onCompositionstart,
onCompositionend,
onFocus,
onBlur,
open,
inputRef,
attrs,
@ -82,6 +87,8 @@ const Input = defineComponent<InputProps, { VCSelectContainerEvent: any; blurTim
const {
onKeydown: onOriginKeyDown,
onInput: onOriginInput,
onFocus: onOriginFocus,
onBlur: onOriginBlur,
onMousedown: onOriginMouseDown,
onCompositionstart: onOriginCompositionStart,
onCompositionend: onOriginCompositionEnd,
@ -97,7 +104,7 @@ const Input = defineComponent<InputProps, { VCSelectContainerEvent: any; blurTim
tabindex,
autocomplete: autocomplete || 'off',
autofocus,
class: `${prefixCls}-selection-search-input`,
class: classNames(`${prefixCls}-selection-search-input`, inputNode?.props?.className),
style: { ...style, opacity: editable ? null : 0 },
role: 'combobox',
'aria-expanded': open,
@ -143,10 +150,14 @@ const Input = defineComponent<InputProps, { VCSelectContainerEvent: any; blurTim
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);
},
@ -181,6 +192,8 @@ Input.props = {
onPaste: PropTypes.func,
onCompositionstart: PropTypes.func,
onCompositionend: PropTypes.func,
onFocus: PropTypes.func,
onBlur: PropTypes.func,
};
export default Input;

View File

@ -1,35 +1,32 @@
import TransBtn from '../TransBtn';
import { LabelValueType, RawValueType, CustomTagProps } from '../interface/generator';
import {
LabelValueType,
RawValueType,
CustomTagProps,
DefaultValueType,
DisplayLabelValueType,
} from '../interface/generator';
import { RenderNode } from '../interface';
import { InnerSelectorProps } from '.';
import Input from './Input';
import {
computed,
defineComponent,
onMounted,
ref,
VNodeChild,
watch,
watchEffect,
Ref,
} from 'vue';
import { computed, defineComponent, onMounted, ref, VNodeChild, watch, Ref } from 'vue';
import classNames from '../../_util/classNames';
import pickAttrs from '../../_util/pickAttrs';
import PropTypes from '../../_util/vue-types';
import { getTransitionGroupProps, TransitionGroup } from '../../_util/transition';
const REST_TAG_KEY = '__RC_SELECT_MAX_REST_COUNT__';
import { VueNode } from 'ant-design-vue/es/_util/type';
import Overflow from '../../vc-overflow';
interface SelectorProps extends InnerSelectorProps {
// Icon
removeIcon?: RenderNode;
// Tags
maxTagCount?: number;
maxTagCount?: number | 'responsive';
maxTagTextLength?: number;
maxTagPlaceholder?: VNodeChild;
tokenSeparators?: string[];
tagRender?: (props: CustomTagProps) => VNodeChild;
onToggleOpen: (open?: boolean) => void;
// Motion
choiceTransitionName?: string;
@ -57,7 +54,7 @@ const props = {
removeIcon: PropTypes.VNodeChild,
choiceTransitionName: PropTypes.string,
maxTagCount: PropTypes.number,
maxTagCount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
maxTagTextLength: PropTypes.number,
maxTagPlaceholder: PropTypes.any.def(() => (omittedValues: LabelValueType[]) =>
`+ ${omittedValues.length} ...`,
@ -73,24 +70,27 @@ const props = {
onInputCompositionEnd: PropTypes.func,
};
const onPreventMouseDown = (event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
};
const SelectSelector = defineComponent<SelectorProps>({
name: 'MultipleSelectSelector',
setup(props) {
let motionAppear = false; // not need use ref, because not need trigger watchEffect
const measureRef = ref();
const inputWidth = ref(0);
const focused = ref(false);
// ===================== Motion ======================
onMounted(() => {
motionAppear = true;
});
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.open && props.showSearch) as boolean),
() =>
props.mode === 'tags' || ((props.showSearch && (props.open || focused.value)) as boolean),
);
// We measure width and set to the input immediately
@ -100,122 +100,99 @@ const SelectSelector = defineComponent<SelectorProps>({
() => {
inputWidth.value = measureRef.value.scrollWidth;
},
{ flush: 'post' },
{ flush: 'post', immediate: true },
);
});
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 = getTransitionGroupProps(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={classNames(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>
);
},
// ===================== Render ======================
// >>> Render Selector Node. Includes Item & Rest
function defaultRenderSelector(
content: VueNode,
itemDisabled: boolean,
closable?: boolean,
onClose?: (e: MouseEvent) => void,
) {
return (
<span
class={classNames(`${selectionPrefixCls.value}-item`, {
[`${selectionPrefixCls.value}-item-disabled`]: itemDisabled,
})}
>
<span class={`${selectionPrefixCls.value}-item-content`}>{content}</span>
{closable && (
<TransBtn
class={`${selectionPrefixCls.value}-item-remove`}
onMousedown={onPreventMouseDown}
onClick={onClose}
customizeIcon={props.removeIcon}
>
×
</TransBtn>
)}
</TransitionGroup>
</span>
);
});
}
function customizeRenderSelector(
value: DefaultValueType,
content: VueNode,
itemDisabled: boolean,
closable: boolean,
onClose: (e: MouseEvent) => void,
) {
const onMouseDown = (e: MouseEvent) => {
onPreventMouseDown(e);
props.onToggleOpen(!open);
};
return (
<span onMousedown={onMouseDown}>
{props.tagRender({
label: content,
value,
disabled: itemDisabled,
closable,
onClose,
})}
</span>
);
}
function renderItem({ disabled: itemDisabled, label, value }: DisplayLabelValueType) {
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.onSelect(value, { selected: false });
};
return typeof props.tagRender === 'function'
? customizeRenderSelector(value, displayLabel, itemDisabled, closable, onClose)
: defaultRenderSelector(displayLabel, itemDisabled, closable, onClose);
}
function renderRest(omittedValues: DisplayLabelValueType[]) {
const {
maxTagPlaceholder = (omittedValues: LabelValueType[]) => `+ ${omittedValues.length} ...`,
} = props;
const content =
typeof maxTagPlaceholder === 'function'
? maxTagPlaceholder(omittedValues)
: maxTagPlaceholder;
return defaultRenderSelector(content, false);
}
return () => {
const {
@ -237,40 +214,63 @@ const SelectSelector = defineComponent<SelectorProps>({
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}
accessibilityIndex={accessibilityIndex}
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.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>
{selectionNode}
{!values.length && !inputValue.value && (
<span class={`${prefixCls}-selection-placeholder`}>{placeholder}</span>
<span class={`${selectionPrefixCls.value}-placeholder`}>{placeholder}</span>
)}
</>
);

View File

@ -63,7 +63,7 @@ export interface SelectorProps {
removeIcon?: RenderNode;
// Tags
maxTagCount?: number;
maxTagCount?: number | 'responsive';
maxTagTextLength?: number;
maxTagPlaceholder?: VNodeChild;
tagRender?: (props: CustomTagProps) => VNodeChild;
@ -140,8 +140,12 @@ const Selector = defineComponent<SelectorProps>({
compositionStatus = true;
};
const onInputCompositionEnd = () => {
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 } }) => {
@ -152,7 +156,10 @@ const Selector = defineComponent<SelectorProps>({
// 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, ' ');
const replacedText = pastedText
.replace(/[\r\n]+$/, '')
.replace(/\r\n/g, ' ')
.replace(/[\r\n]/g, ' ');
value = value.replace(replacedText, pastedText);
}
@ -271,7 +278,7 @@ Selector.props = {
removeIcon: PropTypes.any,
// Tags
maxTagCount: PropTypes.number,
maxTagCount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
maxTagTextLength: PropTypes.number,
maxTagPlaceholder: PropTypes.any,
tagRender: PropTypes.func,

View File

@ -56,6 +56,7 @@ import createRef from '../_util/createRef';
import PropTypes, { withUndefined } from '../_util/vue-types';
import initDefaultProps from '../_util/props-util/initDefaultProps';
import warning from '../_util/warning';
import isMobile from '../vc-util/isMobile';
const DEFAULT_OMIT_PROPS = [
'children',
@ -67,6 +68,7 @@ const DEFAULT_OMIT_PROPS = [
'maxTagPlaceholder',
'choiceTransitionName',
'onInputKeyDown',
'tabindex',
];
export const BaseProps = () => ({
@ -94,6 +96,7 @@ export const BaseProps = () => ({
* It's by design.
*/
filterOption: PropTypes.any,
filterSort: PropTypes.func,
showSearch: PropTypes.looseBool,
autoClearSearchValue: PropTypes.looseBool,
onSearch: PropTypes.func,
@ -134,7 +137,7 @@ export const BaseProps = () => ({
getInputElement: PropTypes.func,
optionLabelProp: PropTypes.string,
maxTagTextLength: PropTypes.number,
maxTagCount: PropTypes.number,
maxTagCount: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
maxTagPlaceholder: PropTypes.any,
tokenSeparators: PropTypes.array,
tagRender: PropTypes.func,
@ -195,6 +198,7 @@ export interface SelectProps<OptionsType extends object[], ValueType> {
* It's by design.
*/
filterOption?: boolean | FilterFunc<OptionsType[number]>;
filterSort?: (optionA: OptionsType[number], optionB: OptionsType[number]) => number;
showSearch?: boolean;
autoClearSearchValue?: boolean;
onSearch?: (value: string) => void;
@ -235,7 +239,7 @@ export interface SelectProps<OptionsType extends object[], ValueType> {
getInputElement?: () => VNodeChild | JSX.Element;
optionLabelProp?: string;
maxTagTextLength?: number;
maxTagCount?: number;
maxTagCount?: number | 'responsive';
maxTagPlaceholder?: VNodeChild | ((omittedValues: LabelValueType[]) => VNodeChild);
tokenSeparators?: string[];
tagRender?: (props: CustomTagProps) => VNodeChild;
@ -385,6 +389,11 @@ export default function generateSelector<
: isMultiple.value || props.mode === 'combobox',
);
const mobile = ref(false);
onMounted(() => {
mobile.value = isMobile();
});
// ============================== Ref ===============================
const selectorDomRef = createRef();
@ -399,12 +408,14 @@ export default function generateSelector<
// ============================= Value ==============================
/** Unique raw values */
const mergedRawValue = computed(() =>
const mergedRawValueArr = computed(() =>
toInnerValue(mergedValue.value, {
labelInValue: mergedLabelInValue.value,
combobox: props.mode === 'combobox',
}),
);
const mergedRawValue = computed(() => mergedRawValueArr.value[0]);
const mergedValueMap = computed(() => mergedRawValueArr.value[1]);
/** We cache a set of raw values to speed up check */
const rawValues = computed(() => new Set(mergedRawValue.value));
@ -457,7 +468,7 @@ export default function generateSelector<
const mergedFlattenOptions = computed(() => flattenOptions(mergedOptions.value, props));
const getValueOption = useCacheOptions(mergedRawValue.value, mergedFlattenOptions);
const getValueOption = useCacheOptions(mergedFlattenOptions);
// Display options for OptionList
const displayOptions = computed<OptionsType>(() => {
@ -484,6 +495,9 @@ export default function generateSelector<
key: '__RC_SELECT_TAG_PLACEHOLDER__',
});
}
if (props.filterSort && Array.isArray(filteredOptions)) {
return ([...filteredOptions] as OptionsType).sort(props.filterSort);
}
return filteredOptions;
});
@ -507,7 +521,7 @@ export default function generateSelector<
const valueOptions = getValueOption([val]);
const displayValue = getLabeledValue(val, {
options: valueOptions,
prevValue: mergedValue.value,
prevValueMap: mergedValueMap.value,
labelInValue: mergedLabelInValue.value,
optionLabelProp: mergedOptionLabelProp.value,
});
@ -542,7 +556,7 @@ export default function generateSelector<
const selectValue = (mergedLabelInValue.value
? getLabeledValue(newValue, {
options: newValueOption,
prevValue: mergedValue.value,
prevValueMap: mergedValueMap.value,
labelInValue: mergedLabelInValue.value,
optionLabelProp: mergedOptionLabelProp.value,
})
@ -583,7 +597,7 @@ export default function generateSelector<
labelInValue: mergedLabelInValue.value,
options: newRawValuesOptions,
getLabeledValue,
prevValue: mergedValue.value,
prevValueMap: mergedValueMap.value,
optionLabelProp: mergedOptionLabelProp.value,
});
@ -770,6 +784,10 @@ export default function generateSelector<
// If menu is open, OptionList will take charge
// If mode isn't tags, press enter is not meaningful when you can't see any option
const onSearchSubmit = (searchText: string) => {
// prevent empty tags from appearing when you click the Enter button
if (!searchText || !searchText.trim()) {
return;
}
const newRawValues = Array.from(
new Set<RawValueType>([...mergedRawValue.value, searchText]),
);
@ -815,9 +833,17 @@ export default function generateSelector<
const onInternalKeyDown = (event: KeyboardEvent) => {
const clearLock = getClearLock();
const { which } = event;
// We only manage open state here, close logic should handle by list component
if (!mergedOpen.value && which === KeyCode.ENTER) {
onToggleOpen(true);
if (which === KeyCode.ENTER) {
// Do not submit form when type in the input
if (props.mode !== 'combobox') {
event.preventDefault();
}
// We only manage open state here, close logic should handle by list component
if (!mergedOpen.value) {
onToggleOpen(true);
}
}
setClearLock(!!mergedSearchValue.value);
@ -922,7 +948,6 @@ export default function generateSelector<
const onInternalMouseDown = (event: MouseEvent) => {
const { target } = event;
const popupElement: HTMLDivElement = triggerRef.value && triggerRef.value.getPopupElement();
// We should give focus back to selector if clicked item is not focusable
if (popupElement && popupElement.contains(target as HTMLElement)) {
const timeoutId = window.setTimeout(() => {
@ -933,7 +958,7 @@ export default function generateSelector<
cancelSetMockFocused();
if (!popupElement.contains(document.activeElement)) {
if (!mobile.value && !popupElement.contains(document.activeElement)) {
selectorRef.value.focus();
}
});
@ -993,6 +1018,7 @@ export default function generateSelector<
return {
focus,
blur,
scrollTo: listRef.value?.scrollTo,
tokenWithEnter,
mockFocused,
mergedId,

View File

@ -17,7 +17,7 @@ export default function useCacheDisplayValue(
const resultValues = values.value.map(item => {
const cacheLabel = valueLabels.get(item.value);
if (item.value === item.label && cacheLabel) {
if (item.isCacheable && cacheLabel) {
return {
...item,
label: cacheLabel,

View File

@ -8,7 +8,7 @@ export default function useCacheOptions<
key?: Key;
disabled?: boolean;
}[]
>(_values: RawValueType[], options: Ref) {
>(options: Ref) {
const optionMap = computed(() => {
const map: Map<RawValueType, FlattenOptionsType<OptionsType>[number]> = new Map();
options.value.forEach((item: any) => {

View File

@ -6,7 +6,11 @@ export default function useSelectTriggerControl(
triggerOpen: (open: boolean) => void,
) {
function onGlobalMouseDown(event: MouseEvent) {
const target = event.target as HTMLElement;
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 &&

View File

@ -1,5 +1,4 @@
import { VueNode } from '../../_util/type';
import { VNodeChild } from 'vue';
export type SelectSource = 'option' | 'selection' | 'input';
@ -13,7 +12,8 @@ export type RawValueType = string | number | null;
export interface LabelValueType extends Record<string, any> {
key?: Key;
value?: RawValueType;
label?: VNodeChild;
label?: VueNode;
isCacheable?: boolean;
}
export type DefaultValueType = RawValueType | RawValueType[] | LabelValueType | LabelValueType[];
@ -23,10 +23,10 @@ export interface DisplayLabelValueType extends LabelValueType {
export type SingleType<MixType> = MixType extends (infer Single)[] ? Single : MixType;
export type OnClear = () => void;
export type OnClear = () => any;
export type CustomTagProps = {
label: DefaultValueType;
label: VueNode;
value: DefaultValueType;
disabled: boolean;
onClose: (event?: MouseEvent) => void;
@ -38,7 +38,7 @@ export type GetLabeledValue<FOT extends FlattenOptionsType> = (
value: RawValueType,
config: {
options: FOT;
prevValue: DefaultValueType;
prevValueMap: Map<RawValueType, LabelValueType>;
labelInValue: boolean;
optionLabelProp: string;
},

View File

@ -19,20 +19,28 @@ export function toArray<T>(value: T | T[]): T[] {
export function toInnerValue(
value: DefaultValueType,
{ labelInValue, combobox }: { labelInValue: boolean; combobox: boolean },
): RawValueType[] {
): [RawValueType[], Map<RawValueType, LabelValueType>] {
const valueMap = new Map<RawValueType, LabelValueType>();
if (value === undefined || (value === '' && combobox)) {
return [];
return [[], valueMap];
}
const values = Array.isArray(value) ? value : [value];
let rawValues = values as RawValueType[];
if (labelInValue) {
return (values as LabelValueType[]).map(({ key, value: val }: LabelValueType) =>
val !== undefined ? val : key,
);
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 values as RawValueType[];
return [rawValues, valueMap];
}
/**
@ -43,7 +51,7 @@ export function toOuterValues<FOT extends FlattenOptionsType>(
{
optionLabelProp,
labelInValue,
prevValue,
prevValueMap,
options,
getLabeledValue,
}: {
@ -51,7 +59,7 @@ export function toOuterValues<FOT extends FlattenOptionsType>(
labelInValue: boolean;
getLabeledValue: GetLabeledValue<FOT>;
options: FOT;
prevValue: DefaultValueType;
prevValueMap: Map<RawValueType, LabelValueType>;
},
): RawValueType[] | LabelValueType[] | DefaultValueType {
let values: DefaultValueType = valueList;
@ -60,7 +68,7 @@ export function toOuterValues<FOT extends FlattenOptionsType>(
values = values.map(val =>
getLabeledValue(val, {
options,
prevValue,
prevValueMap,
labelInValue,
optionLabelProp,
}),

View File

@ -120,24 +120,14 @@ export function findValueOption(
export const getLabeledValue: GetLabeledValue<FlattenOptionData[]> = (
value,
{ options, prevValue, labelInValue, optionLabelProp },
{ options, prevValueMap, labelInValue, optionLabelProp },
) => {
const item = findValueOption([value], options)[0];
const result: LabelValueType = {
value,
};
let prevValItem: LabelValueType;
const prevValues = toArray<LabelValueType>(prevValue as LabelValueType);
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;
}
const prevValItem: LabelValueType = labelInValue ? prevValueMap.get(value) : undefined;
if (prevValItem && typeof prevValItem === 'object' && 'label' in prevValItem) {
result.label = prevValItem.label;
@ -160,6 +150,7 @@ export const getLabeledValue: GetLabeledValue<FlattenOptionData[]> = (
}
} else {
result.label = value;
result.isCacheable = true;
}
// Used for motion control
@ -211,7 +202,7 @@ export function filterOptions(
let filterFunc: FilterFunc<SelectOptionsType[number]>;
if (filterOption === false) {
return options;
return [...options];
}
if (typeof filterOption === 'function') {
filterFunc = filterOption;

View File

@ -0,0 +1,18 @@
export default () => {
if (typeof navigator === 'undefined' || typeof window === 'undefined') {
return false;
}
const agent = navigator.userAgent || navigator.vendor || (window as any).opera;
if (
/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(
agent,
) ||
/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i.test(
agent?.substr(0, 4),
)
) {
return true;
}
return false;
};

View File

@ -37,5 +37,6 @@ declare module 'vue' {
onKeydown?: EventHandler;
onKeyup?: EventHandler;
onDeselect?: EventHandler;
onClear?: EventHandler;
}
}