diff --git a/packages/components/select-v2/__tests__/select.spec.ts b/packages/components/select-v2/__tests__/select.spec.ts index a74c4bfe80..17fcb98f8f 100644 --- a/packages/components/select-v2/__tests__/select.spec.ts +++ b/packages/components/select-v2/__tests__/select.spec.ts @@ -24,7 +24,7 @@ const clickClearButton = async wrapper => { const select = wrapper.findComponent(Select) const selectVm = select.vm as any selectVm.states.comboBoxHovering = true - await nextTick + await nextTick() const clearBtn = wrapper.find(`.${selectVm.clearIcon}`) expect(clearBtn.exists()).toBeTruthy() await clearBtn.trigger('click') @@ -38,6 +38,7 @@ interface SelectProps { clearable?: boolean multiple?: boolean filterable?: boolean + remote?: boolean multipleLimit?: number allowCreate?: boolean popperAppendToBody?: boolean @@ -51,6 +52,8 @@ interface SelectEvents { onRemoveTag?: (tag?: string) => void onFocus?: (event?: FocusEvent) => void onBlur?: (event?) => void + filterMethod?: (query: string) => void | undefined + remoteMethod?: (query: string) => void | undefined [key: string]: (...args) => any } @@ -76,6 +79,9 @@ const createSelect = (options: { :popper-append-to-body="popperAppendToBody" :placeholder="placeholder" :allow-create="allowCreate" + :remote="remote" + ${options.methods && options.methods.filterMethod ? `:filter-method="filterMethod"` : ''} + ${options.methods && options.methods.remoteMethod ? `:remote-method="remoteMethod"` : ''} @change="onChange" @visible-change="onVisibleChange" @remove-tah="onRemoveTag" @@ -95,6 +101,7 @@ const createSelect = (options: { disabled: false, clearable: false, multiple: false, + remote: false, filterable: false, multipleLimit: 0, popperAppendToBody: true, @@ -115,7 +122,7 @@ const createSelect = (options: { function getOptions(): HTMLElement[] { return Array.from(document.querySelectorAll( - `body > div:last-child .${OPTION_ITEM_CLASS_NAME}`, + `.${OPTION_ITEM_CLASS_NAME}`, )) } @@ -133,7 +140,7 @@ describe('Select', () => { it('create', async () => { const wrapper = createSelect() - await nextTick + await nextTick() expect(wrapper.classes()).toContain(CLASS_NAME) expect(wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`).text()).toContain(DEFAULT_PLACEHOLDER) const select = wrapper.findComponent(Select) @@ -143,7 +150,7 @@ describe('Select', () => { it('options rendered correctly', async() => { const wrapper = createSelect() - await nextTick + await nextTick() const vm = wrapper.vm as any const options = document.getElementsByClassName(OPTION_ITEM_CLASS_NAME) const result = [].every.call(options, (option, index) => { @@ -159,7 +166,7 @@ describe('Select', () => { popperClass: 'custom-dropdown', }), }) - await nextTick + await nextTick() expect(document.querySelector('.el-popper').classList).toContain('custom-dropdown') }) @@ -184,7 +191,7 @@ describe('Select', () => { }), }) const vm = wrapper.vm as any - await nextTick + await nextTick() expect(wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`).text()).toBe(vm.options[1].label) }) @@ -194,23 +201,23 @@ describe('Select', () => { const placeholder = wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`) expect(placeholder.text()).toBe(DEFAULT_PLACEHOLDER) vm.value = vm.options[2].value - await nextTick + await nextTick() expect(placeholder.text()).toBe(vm.options[2].label) vm.value = null - await nextTick + await nextTick() expect(placeholder.text()).toBe(DEFAULT_PLACEHOLDER) }) it('sync set value and options', async () => { const wrapper = createSelect() - await nextTick + await nextTick() const placeholder = wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`) const vm = wrapper.vm as any vm.value = vm.options[1].value - await nextTick + await nextTick() expect(placeholder.text()).toBe(vm.options[1].label) vm.options[1].label = 'option bb aa' - await nextTick + await nextTick() expect(placeholder.text()).toBe('option bb aa') }) @@ -227,18 +234,18 @@ describe('Select', () => { }, }, }) - await nextTick + await nextTick() const options = getOptions() const vm = wrapper.vm as any const placeholder = wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`) expect(vm.value).toBe('') expect(placeholder.text()).toBe(DEFAULT_PLACEHOLDER) options[2].click() - await nextTick + await nextTick() expect(vm.value).toBe(vm.options[2].value) expect(placeholder.text()).toBe(vm.options[2].label) options[4].click() - await nextTick + await nextTick() expect(vm.value).toBe(vm.options[4].value) expect(placeholder.text()).toBe(vm.options[4].label) expect(vm.count).toBe(2) @@ -268,22 +275,22 @@ describe('Select', () => { } }, }) - await nextTick + await nextTick() const vm = wrapper.vm as any const placeholder = wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`) const option = document.querySelector(`.el-select-dropdown__option-item.is-disabled`) expect(option.textContent).toBe(vm.options[1].label) option.click() - await nextTick + await nextTick() expect(vm.value).toBe('') expect(placeholder.text()).toBe(DEFAULT_PLACEHOLDER) vm.options[2].disabled = true - await nextTick + await nextTick() const options = document.querySelectorAll(`.el-select-dropdown__option-item.is-disabled`) expect(options.length).toBe(2) expect(options.item(1).textContent).toBe(vm.options[2].label) options.item(1).click() - await nextTick + await nextTick() expect(vm.value).toBe('') expect(placeholder.text()).toBe(DEFAULT_PLACEHOLDER) }) @@ -296,7 +303,7 @@ describe('Select', () => { } }, }) - await nextTick + await nextTick() expect(wrapper.find(`.${WRAPPER_CLASS_NAME}`).classes()).toContain('is-disabled') }) @@ -313,7 +320,7 @@ describe('Select', () => { }, }, }) - await nextTick + await nextTick() const vm = wrapper.vm as any await wrapper.trigger('click') expect(vm.visible).toBeTruthy() @@ -325,7 +332,7 @@ describe('Select', () => { }) const vm = wrapper.vm as any vm.value = vm.options[1].value - await nextTick + await nextTick() await clickClearButton(wrapper) expect(vm.value).toBe('') const placeholder = wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`) @@ -343,15 +350,15 @@ describe('Select', () => { } }, }) - await nextTick + await nextTick() const vm = wrapper.vm as any const options = getOptions() options[1].click() - await nextTick + await nextTick() expect(vm.value.length).toBe(1) expect(vm.value[0]).toBe(vm.options[1].value) options[3].click() - await nextTick + await nextTick() expect(vm.value.length).toBe(2) expect(vm.value[1]).toBe(vm.options[3].value) const tagIcon = wrapper.find('.el-tag__close') @@ -373,7 +380,7 @@ describe('Select', () => { }, }, }) - await nextTick + await nextTick() const vm = wrapper.vm as any const options = getOptions() options[0].click() @@ -400,16 +407,16 @@ describe('Select', () => { } }, }) - await nextTick + await nextTick() const vm = wrapper.vm as any const options = getOptions() options[1].click() - await nextTick + await nextTick() options[2].click() - await nextTick + await nextTick() expect(vm.value.length).toBe(2) options[3].click() - await nextTick + await nextTick() expect(vm.value.length).toBe(2) }) }) @@ -431,12 +438,12 @@ describe('Select', () => { const selectVm = select.vm as any // Simulate focus state to trigger menu multiple times selectVm.toggleMenu() - await nextTick + await nextTick() selectVm.toggleMenu() - await nextTick + await nextTick() // Simulate click the outside selectVm.handleClickOutside() - await nextTick + await nextTick() expect(onFocus).toHaveBeenCalledTimes(1) expect(onBlur).toHaveBeenCalled() }) @@ -463,20 +470,20 @@ describe('Select', () => { const selectVm = select.vm as any // Simulate focus state to trigger menu multiple times selectVm.toggleMenu() - await nextTick + await nextTick() selectVm.toggleMenu() - await nextTick + await nextTick() // Select multiple items in multiple mode without triggering focus const options = getOptions() options[1].click() - await nextTick + await nextTick() options[2].click() - await nextTick + await nextTick() expect(onFocus).toHaveBeenCalledTimes(1) // Simulate click the outside selectVm.handleClickOutside() - await nextTick - await nextTick + await nextTick() + await nextTick() expect(onBlur).toHaveBeenCalled() }) @@ -487,14 +494,14 @@ describe('Select', () => { onChange: handleChanged, }, }) - await nextTick + await nextTick() const vm = wrapper.vm as any vm.value = 'option_2' - await nextTick + await nextTick() expect(handleChanged).toHaveBeenCalledTimes(0) const options = getOptions() options[4].click() - await nextTick + await nextTick() expect(handleChanged).toHaveBeenCalled() }) }) @@ -505,6 +512,7 @@ describe('Select', () => { const wrapper = createSelect({ data: () => { return { + popperAppendToBody: false, allowCreate: true, filterable: true, clearable: true, @@ -525,23 +533,25 @@ describe('Select', () => { } }, }) - await nextTick - const vm = wrapper.vm as any - const input = wrapper.find('input') - await wrapper.trigger('click') - // create a new option - await input.trigger('compositionupdate', { - data: '1111', - }) - const options = getOptions() + await nextTick() const select = wrapper.findComponent(Select) const selectVm = select.vm as any + selectVm.expanded = true + await nextTick() + const vm = wrapper.vm as any + const input = wrapper.find('input') + // create a new option + input.element.value = '1111' + await input.trigger('input') + await nextTick() expect(selectVm.filteredOptions.length).toBe(1) // selected the new option - await options[0].click() + selectVm.onSelect(selectVm.filteredOptions[0]) expect(vm.value).toBe('1111') - // closed the menu - await wrapper.trigger('click') + selectVm.expanded = false + await nextTick() + selectVm.expanded = true + await nextTick() expect(selectVm.filteredOptions.length).toBe(4) selectVm.handleClear() expect(selectVm.filteredOptions.length).toBe(3) @@ -572,24 +582,24 @@ describe('Select', () => { } }, }) - await nextTick + await nextTick() const vm = wrapper.vm as any await wrapper.trigger('click') - await wrapper.find('input').trigger('compositionupdate', { - data: '1111', - }) - const options = getOptions() + const input = wrapper.find('input') + input.element.value = '1111' + await input.trigger('input') + await nextTick() const select = wrapper.findComponent(Select) const selectVm = select.vm as any expect(selectVm.filteredOptions.length).toBe(1) // selected the new option - await options[0].click() + selectVm.onSelect(selectVm.filteredOptions[0]) // closed the menu await wrapper.trigger('click') - await wrapper.find('input').trigger('compositionupdate', { - data: '2222', - }) - await getOptions()[0].click() + input.element.value = '2222' + await input.trigger('input') + await nextTick() + selectVm.onSelect(selectVm.filteredOptions[0]) expect(JSON.stringify(vm.value)).toBe(JSON.stringify(['1111', '2222'])) await wrapper.trigger('click') expect(selectVm.filteredOptions.length).toBe(5) @@ -618,7 +628,7 @@ describe('Select', () => { empty: '
EmptySlot
', }, }) - await nextTick + await nextTick() expect(wrapper.find('.empty-slot').exists()).toBeTruthy() }) @@ -645,11 +655,11 @@ describe('Select', () => { } }, }) - await nextTick + await nextTick() const vm = wrapper.vm as any const placeholder = wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`) vm.value = '2' - await nextTick + await nextTick() const select = wrapper.findComponent(Select) const selectVm = select.vm as any selectVm.toggleMenu() @@ -666,12 +676,12 @@ describe('Select', () => { } }, }) - await nextTick + await nextTick() const vm = wrapper.vm as any const placeholder = wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`) expect(placeholder.text()).toBe(DEFAULT_PLACEHOLDER) vm.value = undefined - await nextTick + await nextTick() expect(placeholder.text()).toBe(DEFAULT_PLACEHOLDER) }) @@ -683,7 +693,7 @@ describe('Select', () => { } }, }) - await nextTick + await nextTick() const vm = wrapper.vm as any const placeholder = wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`) expect(placeholder.text()).toBe(vm.value) @@ -707,7 +717,7 @@ describe('Select', () => { `, }, }) - await nextTick + await nextTick() expect(wrapper.findAll('.custom-renderer').length).toBeGreaterThan(0) }) @@ -719,24 +729,24 @@ describe('Select', () => { options: [ { value: 1, - lable: 'option 1', + label: 'option 1', disabled: true, }, { value: 2, - lable: 'option 2', + label: 'option 2', disabled: true, }, { value: 3, - lable: 'option 3', + label: 'option 3', }, ], value: [2, 3], } }, }) - await nextTick + await nextTick() expect(wrapper.findAll('.el-tag').length).toBe(2) const tagCloseIcons = wrapper.findAll('.el-tag__close') expect(tagCloseIcons.length).toBe(1) @@ -754,11 +764,11 @@ describe('Select', () => { } }, }) - await nextTick + await nextTick() expect(wrapper.findAll('.el-tag').length).toBe(3) const vm = wrapper.vm as any vm.value.splice(0, 1) - await nextTick + await nextTick() expect(wrapper.findAll('.el-tag').length).toBe(2) }) @@ -773,7 +783,7 @@ describe('Select', () => { } }, }) - await nextTick + await nextTick() expect(wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`).exists()).toBeFalsy() // When all tags are removed, the placeholder should be displayed const tagCloseIcon = wrapper.find('.el-tag__close') @@ -782,14 +792,109 @@ describe('Select', () => { // The placeholder should disappear after it is selected again const options = getOptions() options[0].click() - await nextTick + await nextTick() expect(wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`).exists()).toBeFalsy() // Simulate keyboard events const selectInput = wrapper.find('input') selectInput.trigger('keydown', { key: EVENT_CODE.backspace, }) - await nextTick + await nextTick() expect(wrapper.find(`.${PLACEHOLDER_CLASS_NAME}`).text()).toBe(DEFAULT_PLACEHOLDER) }) + + describe('filter method', () => { + async function testFilterMethod({ multiple = false }) { + const filterMethod = jest.fn() + const wrapper = createSelect({ + data() { + return { + filterable: true, + multiple, + } + }, + methods: { + filterMethod, + }, + }) + const input = wrapper.find('input') + input.element.value = 'query' + await input.trigger('input') + expect(filterMethod).toHaveBeenCalled() + } + it('should call filter method', async () => { + await testFilterMethod({ multiple: false }) + }) + + it('should call filter method in multiple mode', async () => { + await testFilterMethod({ multiple: true }) + }) + + it('should re-render', async () => { + const wrapper = createSelect({ + data() { + return { + multiple: true, + filterable: true, + } + }, + methods: { + filterMethod () { + this.options = [ + { + value: 1, + label: 'option 1', + }, + { + value: 2, + label: 'option 2', + }, + { + value: 3, + label: 'option 3', + }, + ] + }, + }, + }) + const input = wrapper.find('input') + input.element.value = 'query' + await input.trigger('input') + await nextTick() + input.element.value = '' + await input.trigger('input') + await nextTick() + const options = getOptions() + expect(options.length).toBe(3) + }) + }) + + describe('remote search', () => { + async function testRemoteSearch({ multiple = false }) { + const remoteMethod = jest.fn() + const wrapper = createSelect({ + data() { + return { + filterable: true, + remote: true, + multiple, + } + }, + methods: { + remoteMethod, + }, + }) + const input = wrapper.find('input') + input.element.value = 'query' + await input.trigger('input') + expect(remoteMethod).toHaveBeenCalled() + } + it('should call remote method', async () => { + await testRemoteSearch({ multiple: false }) + }) + + it('should call remote method in multiple mode', async () => { + await testRemoteSearch({ multiple: true }) + }) + }) }) diff --git a/packages/components/select-v2/src/option-item.vue b/packages/components/select-v2/src/option-item.vue index 4c3410b083..700dbd5abe 100644 --- a/packages/components/select-v2/src/option-item.vue +++ b/packages/components/select-v2/src/option-item.vue @@ -6,7 +6,7 @@ 'el-select-dropdown__option-item': true, 'is-selected': selected, 'is-disabled': disabled, - 'is-craeted': created, + 'is-created': created, 'hover': hovering }" @mouseenter="hoverItem" diff --git a/packages/components/select-v2/src/select.vue b/packages/components/select-v2/src/select.vue index d595457d7e..f9a98a5b96 100644 --- a/packages/components/select-v2/src/select.vue +++ b/packages/components/select-v2/src/select.vue @@ -10,7 +10,7 @@ > @@ -149,9 +149,9 @@ spellcheck="false" type="text" :unselectable="expanded ? 'on' : undefined" - @click.stop.prevent="handleInputBoxClick" - @compositionend="onInput" - @compositionupdate="onCompositionUpdate" + @compositionstart="handleCompositionStart" + @compositionupdate="handleCompositionUpdate" + @compositionend="handleCompositionEnd" @focus="handleFocus" @input="onInput" @keydown.esc.stop.prevent="handleEsc" @@ -203,7 +203,7 @@ diff --git a/packages/components/select-v2/src/useInput.ts b/packages/components/select-v2/src/useInput.ts new file mode 100644 index 0000000000..02aa9918d9 --- /dev/null +++ b/packages/components/select-v2/src/useInput.ts @@ -0,0 +1,32 @@ +import { ref } from 'vue' +import { isKorean } from '@element-plus/utils/isDef' +import { isFunction } from '@vue/shared' + +export function useInput(handleInput: (event: InputEvent) => void) { + const isComposing = ref(false) + + const handleCompositionStart = () => { + isComposing.value = true + } + + const handleCompositionUpdate = event => { + const text = event.target.value + const lastCharacter = text[text.length - 1] || '' + isComposing.value = !isKorean(lastCharacter) + } + + const handleCompositionEnd = event => { + if (isComposing.value) { + isComposing.value = false + if (isFunction(handleInput)) { + handleInput(event) + } + } + } + + return { + handleCompositionStart, + handleCompositionUpdate, + handleCompositionEnd, + } +} diff --git a/packages/components/select-v2/src/useSelect.ts b/packages/components/select-v2/src/useSelect.ts index 2d524bc788..82ef3350dc 100644 --- a/packages/components/select-v2/src/useSelect.ts +++ b/packages/components/select-v2/src/useSelect.ts @@ -33,9 +33,10 @@ import { flattenOptions } from './util' import type { ExtractPropTypes, CSSProperties } from 'vue' import type { ElFormContext, ElFormItemContext } from '@element-plus/tokens' import type { OptionType, Option } from './select.types' +import { useInput } from './useInput' const DEFAULT_INPUT_PLACEHOLDER = '' -const MINIMUM_INPUT_WIDTH = 4 +const MINIMUM_INPUT_WIDTH = 11 const useSelect = (props: ExtractPropTypes, emit) => { @@ -64,6 +65,7 @@ const useSelect = (props: ExtractPropTypes, emit) => { inputWidth: 240, initialInputHeight: 0, previousQuery: null, + previousValue: '', query: '', selectedLabel: '', softFocus: false, @@ -116,8 +118,8 @@ const useSelect = (props: ExtractPropTypes, emit) => { if (props.loading) { return props.loadingText || t('el.select.loading') } else { - if (props.remote && states.query === '' && options.length === 0) return false - if (props.filterable && states.query && options.length > 0) { + if (props.remote && states.inputValue === '' && options.length === 0) return false + if (props.filterable && states.inputValue && options.length > 0) { return props.noMatchText || t('el.select.noMatch') } if (options.length === 0) { @@ -135,7 +137,9 @@ const useSelect = (props: ExtractPropTypes, emit) => { const containsQueryString = query ? o.label.includes(query) : true return containsQueryString } - + if (props.loading) { + return [] + } return flattenOptions((props.options as OptionType[]).concat(states.createdOptions).map(v => { if (isArray(v.options)) { const filtered = v.options.filter(isValidOption) @@ -146,7 +150,7 @@ const useSelect = (props: ExtractPropTypes, emit) => { } } } else { - if (isValidOption(v as Option)) { + if (props.remote || isValidOption(v as Option)) { return v } } @@ -161,13 +165,11 @@ const useSelect = (props: ExtractPropTypes, emit) => { const calculatePopperSize = () => { popperSize.value = selectRef.value?.getBoundingClientRect?.()?.width || 200 } - // const readonly = computed(() => !props.filterable || props.multiple || (!isIE() && !isEdge() && !expanded.value)) const inputWrapperStyle = computed(() => { return { width: `${ - // 7 represents the margin-left value states.calculatedWidth === 0 ? MINIMUM_INPUT_WIDTH : Math.ceil(states.calculatedWidth) + MINIMUM_INPUT_WIDTH @@ -209,8 +211,11 @@ const useSelect = (props: ExtractPropTypes, emit) => { return -1 }) + const dropdownMenuVisible = computed(() => expanded.value && emptyText.value !== false) + // hooks const { createNewOption, removeNewOption, selectNewOption, clearAllNewOption } = useAllowCreate(props, states) + const { handleCompositionStart, handleCompositionUpdate, handleCompositionEnd } = useInput(e => onInput(e)) // methods const focusAndUpdatePopup = () => { @@ -221,78 +226,35 @@ const useSelect = (props: ExtractPropTypes, emit) => { const toggleMenu = () => { if (props.automaticDropdown) return if (!selectDisabled.value) { - // if (states.menuVisibleOnFocus) { - // states.menuVisibleOnFocus = false - // } else { - // if (expanded.value) { - // expanded.value = false - // } if (states.isComposing) states.softFocus = true expanded.value = !expanded.value inputRef.value?.focus?.() - // } } } - const handleQueryChange = (val: string) => { - if (states.previousQuery === val || states.isOnComposition) return - if ( - states.previousQuery === null && - (isFunction(props.filterMethod) || isFunction(props.remoteMethod)) - ) { - states.previousQuery = val - return - } - states.previousQuery = val - nextTick(() => { - if (expanded.value) popper.value?.update?.() - }) - states.hoveringIndex = -1 - if (props.multiple && props.filterable) { - nextTick(() => { - const length = inputRef.value.value.length * 15 + 20 - states.inputLength = props.collapseTags ? Math.min(50, length) : length - resetInputHeight() - }) - } - if (props.remote && isFunction(props.remoteMethod)) { - states.hoveringIndex = -1 - props.remoteMethod(val) - } else if (isFunction(props.filterMethod)) { - props.filterMethod(val) - // states.selectEmitter.emit('elOptionGroupQueryChange') - } else { - // states.selectEmitter.emit('elOptionQueryChange', val) - // states.selectEmitter.emit('elOptionGroupQueryChange') - } - if (props.defaultFirstOption && (props.filterable || props.remote)) { - // checkDefaultFirstOption() - } - } - - // const handleComposition = event => { - // const text = event.target.value - // if (event.type === 'compositionend') { - // states.isOnComposition = false - // nextTick(() => handleQueryChange(text)) - // } else { - // const lastCharacter = text[text.length - 1] || '' - // states.isOnComposition = !isKorean(lastCharacter) - // } - // } - const onInputChange = () => { if (props.filterable && states.inputValue !== states.selectedLabel) { states.query = states.selectedLabel - handleQueryChange(states.query) } + handleQueryChange(states.inputValue) + return nextTick(() => { + createNewOption(states.inputValue) + }) } const debouncedOnInputChange = lodashDebounce(onInputChange, debounce.value) - const debouncedQueryChange = lodashDebounce(e => { - handleQueryChange(e.target.value) - }, debounce.value) + const handleQueryChange = (val: string) => { + if (states.previousQuery === val) { + return + } + states.previousQuery = val + if (props.filterable && isFunction(props.filterMethod)) { + props.filterMethod(val) + } else if (props.filterable && props.remote && isFunction(props.remoteMethod)) { + props.remoteMethod(val) + } + } const emitChange = (val: any | any[]) => { if (!isEqual(props.modelValue, val)) { @@ -303,9 +265,9 @@ const useSelect = (props: ExtractPropTypes, emit) => { const update = (val: any) => { emit(UPDATE_MODEL_EVENT, val) emitChange(val) + states.previousValue = val.toString() } - // TODO 提取 const getValueIndex = (arr = [], value: unknown) => { if (!isObject(value)) return arr.indexOf(value) @@ -425,12 +387,6 @@ const useSelect = (props: ExtractPropTypes, emit) => { event.stopPropagation() } - const handleInputBoxClick = () => { - if (states.displayInputValue.length === 0 && expanded.value) { - expanded.value = false - } - } - const handleFocus = (event: FocusEvent) => { const focused = states.isComposing states.isComposing = true @@ -565,7 +521,9 @@ const useSelect = (props: ExtractPropTypes, emit) => { } } - const onInput = () => { + const onInput = event => { + const value = event.target.value + onUpdateInputValue(value) if (states.displayInputValue.length > 0 && !expanded.value) { expanded.value = true } @@ -574,13 +532,11 @@ const useSelect = (props: ExtractPropTypes, emit) => { if (props.multiple) { resetInputHeight() } - debouncedOnInputChange() - createNewOption(states.displayInputValue) - } - - const onCompositionUpdate = (e: CompositionEvent) => { - onUpdateInputValue(states.displayInputValue += e.data) - onInput() + if (props.remote) { + debouncedOnInputChange() + } else { + return onInputChange() + } } const handleClickOutside = () => { @@ -590,7 +546,7 @@ const useSelect = (props: ExtractPropTypes, emit) => { const handleMenuEnter = () => { states.inputValue = states.displayInputValue - nextTick(() => { + return nextTick(() => { if (~indexRef.value) { scrollToItem(indexRef.value) } @@ -642,8 +598,20 @@ const useSelect = (props: ExtractPropTypes, emit) => { } }) - watch([() => props.modelValue, () => props.options], () => { - initStates() + watch(() => props.modelValue, val => { + if (!val || val.toString() !== states.previousValue) { + initStates() + } + }, { + deep: true, + }) + + watch(() => props.options, () => { + const input = inputRef.value + // filter or remote-search scenarios are not initialized + if (!input || (input && document.activeElement !== input)) { + initStates() + } }, { deep: true, }) @@ -674,6 +642,7 @@ const useSelect = (props: ExtractPropTypes, emit) => { iconClass, inputWrapperStyle, popperSize, + dropdownMenuVisible, // readonly, shouldShowPlaceholder, selectDisabled, @@ -694,7 +663,6 @@ const useSelect = (props: ExtractPropTypes, emit) => { // methods exports debouncedOnInputChange, - debouncedQueryChange, deleteTag, getLabel, getValueKey, @@ -704,16 +672,17 @@ const useSelect = (props: ExtractPropTypes, emit) => { handleDel, handleEsc, handleFocus, - handleInputBoxClick, handleMenuEnter, toggleMenu, scrollTo: scrollToItem, - onCompositionUpdate, onInput, onKeyboardNavigate, onKeyboardSelect, onSelect, onUpdateInputValue, + handleCompositionStart, + handleCompositionEnd, + handleCompositionUpdate, } } diff --git a/packages/theme-chalk/src/select-v2.scss b/packages/theme-chalk/src/select-v2.scss index b53beacd85..86c1617ea0 100644 --- a/packages/theme-chalk/src/select-v2.scss +++ b/packages/theme-chalk/src/select-v2.scss @@ -226,7 +226,7 @@ $--input-inline-start: 15px !default; right: 5px; height: 40px; top: 50%; - margin-top: -20px; + transform: translateY(-50%); } @include e(caret) { diff --git a/website/docs/en-US/select-v2.md b/website/docs/en-US/select-v2.md index 49c83b4619..a11d6df4a2 100644 --- a/website/docs/en-US/select-v2.md +++ b/website/docs/en-US/select-v2.md @@ -365,7 +365,78 @@ Create and select new items that are not included in select options ### Remote search -WIP 👷‍♀️ +Enter keywords and search data from server. + +:::demo Set the value of `filterable` and `remote` with `true` to enable remote search, and you should pass the `remote-method`. `remote-method` is a `Function` that gets called when the input value changes, and its parameter is the current input value. +```html + + + +``` +::: ### Keyboard navigation diff --git a/website/docs/es/select-v2.md b/website/docs/es/select-v2.md index d3ca0b0b29..cc0d05a7e1 100644 --- a/website/docs/es/select-v2.md +++ b/website/docs/es/select-v2.md @@ -364,9 +364,80 @@ Crear y seleccionar nuevos items que no están incluidas en las opciones de sele ``` ::: -### Remote search +### Búsqueda remota -WIP 👷‍♀️ +Introduzca palabras y datos para buscar desde el servidor. + +:::demo Configure el valor de `filterable` y `remote` con `true` para habilitar la búsqueda remota, y debería pasar el método `remote-method`. `remote-method` es una función que se llama cuando el valor del input cambia, y su parámetro es el valor del input actual. +```html + + + +``` +::: ### Keyboard navigation @@ -391,6 +462,9 @@ Some APIs are still undergoing (comparing to the non-virtualized select), becaus | autocomplete | select input 的 autocomplete 属性 | string | — | off | | placeholder | the autocomplete attribute of select input | string | — | Please select | | filterable | is filterable | boolean | — | false | +| filter-method | método de filtrado personalizado | function | — | — | +| remote | si las opciones se traerán desde el servidor | boolean | — | false | +| remote-method | método de búsqueda remota personalizada | function | — | — | | allow-create | si esta permitido crear nuevos items. Para usar esto, `filterable` debe ser `true`. | boolean | — | false | | no-data-text | displayed text when there is no options, you can also use slot empty | string | — | No Data | | popper-class | custom class name for Select's dropdown | string | — | — | diff --git a/website/docs/fr-FR/select-v2.md b/website/docs/fr-FR/select-v2.md index 9556d652f9..635c940d5a 100644 --- a/website/docs/fr-FR/select-v2.md +++ b/website/docs/fr-FR/select-v2.md @@ -365,9 +365,80 @@ Vous pouvez entrer des choix dans le champ de sélection qui ne sont pas incluse ``` ::: -### Remote search +### Recherche à distance -WIP 👷‍♀️ +Vous pouvez aller chercher les options sur le serveur de manière dynamique. + +:::demo Ajoutez `filterable` et `remote` pour activer la recherche distante, ainsi que `remote-method`. Cette dernière est une `Function` qui est appelée lorsque la valeur change, avec pour paramètre la valeur courante. +```html + + + +``` +::: ### Keyboard navigation @@ -392,6 +463,9 @@ Some APIs are still undergoing (comparing to the non-virtualized select), becaus | autocomplete | select input 的 autocomplete 属性 | string | — | off | | placeholder | the autocomplete attribute of select input | string | — | Please select | | filterable | is filterable | boolean | — | false | +| filter-method | Méthode de filtrage personnalisée. | function | — | — | +| remote | Si les options sont chargées dynamiquement depuis le serveur. | boolean | — | false | +| remote-method | Méthode pour la recherche distante. | function | — | — | | allow-create | Si l'utilisateur peut créer des options. Dans ce cas `filterable` doit être activé. | boolean | — | false | | no-data-text | displayed text when there is no options, you can also use slot empty | string | — | No Data | | popper-class | custom class name for Select's dropdown | string | — | — | diff --git a/website/docs/jp/select-v2.md b/website/docs/jp/select-v2.md index 825054e7fe..6c7498c22f 100644 --- a/website/docs/jp/select-v2.md +++ b/website/docs/jp/select-v2.md @@ -363,9 +363,80 @@ We can clear all the selected options at once, also applicable for single select ``` ::: -### Remote search +### リモート検索 -WIP 👷‍♀️ +サーバーからキーワードや検索データを入力します。 + +:::demo リモート検索を有効にするには `filterable` と `remote` を `true` を設定し、`remote-method` を渡す必要がある。`remote-method`は入力値が変化したときに呼び出される `Function` であり、そのパラメータは現在の入力値である。 +```html + + + +``` +::: ### Keyboard navigation @@ -390,6 +461,9 @@ Some APIs are still undergoing (comparing to the non-virtualized select), becaus | autocomplete | select input 的 autocomplete 属性 | string | — | off | | placeholder | the autocomplete attribute of select input | string | — | Please select | | filterable | is filterable | boolean | — | false | +| filter-method | カスタムフィルタ方式 | function | — | — | +| remote | オプションがサーバから読み込まれているかどうか | boolean | — | false | +| remote-method | カスタムリモート検索法 | function | — | — | | allow-create | 新しいアイテムの作成を許可するかどうかを指定します。これを使うには、`filterable` がtrueでなければなりません。 | boolean | — | false | | no-data-text | displayed text when there is no options, you can also use slot empty | string | — | No Data | | popper-class | custom class name for Select's dropdown | string | — | — | diff --git a/website/docs/zh-CN/select-v2.md b/website/docs/zh-CN/select-v2.md index d21badc4c0..dd1ed085d7 100644 --- a/website/docs/zh-CN/select-v2.md +++ b/website/docs/zh-CN/select-v2.md @@ -366,7 +366,77 @@ ### 远程搜索 -WIP (该功能还在施工中👷‍♀️) +从服务器搜索数据,输入关键字进行查找 +:::demo 为了启用远程搜索,需要将`filterable`和`remote`设置为`true`,同时传入一个`remote-method`。`remote-method`为一个`Function`,它会在输入值发生变化时调用,参数为当前输入值。 +```html + + + +``` +::: ### 键盘操作 @@ -391,6 +461,9 @@ WIP (该功能还在施工中👷‍♀️) | autocomplete | select input 的 autocomplete 属性 | string | — | off | | placeholder | 占位符 | string | — | 请选择 | | filterable | 是否可搜索 | boolean | — | false | +| filter-method | 自定义搜索方法 | function | — | — | +| remote | 是否为远程搜索 | boolean | — | false | +| remote-method | 远程搜索方法 | function | — | — | | allow-create | 是否允许用户创建新条目,需配合 `filterable` 使用 | boolean | — | false | | no-data-text | 选项为空时显示的文字,也可以使用`#empty`设置 | string | — | 无数据 | | popper-class | Select 下拉框的类名 | string | — | — |