From b917fafcccd596a05c8cd36c53b0fd40a276d76b Mon Sep 17 00:00:00 2001 From: Rainbow <1256734885@qq.com> Date: Mon, 20 Oct 2025 10:46:24 +0800 Subject: [PATCH] feat(components): [select/select-v2] support debounce prop (#22471) * feat(components): [select/select-v2] support debounce prop * feat: update * feat: update * feat: update * docs: update version number * feat: update * feat: update * feat: update * feat: update * feat: update * feat: update --- docs/en-US/component/select-v2.md | 1 + docs/en-US/component/select.md | 1 + .../select-v2/__tests__/select.test.ts | 10 ++++++++ packages/components/select-v2/src/defaults.ts | 7 ++++++ .../components/select-v2/src/useSelect.ts | 25 +++++++++++-------- .../select/__tests__/select.test.ts | 10 ++++++++ packages/components/select/src/select.ts | 7 ++++++ packages/components/select/src/useSelect.ts | 25 +++++++++++-------- 8 files changed, 65 insertions(+), 21 deletions(-) diff --git a/docs/en-US/component/select-v2.md b/docs/en-US/component/select-v2.md index ca40ee3476..f7e0c9d8d2 100644 --- a/docs/en-US/component/select-v2.md +++ b/docs/en-US/component/select-v2.md @@ -275,6 +275,7 @@ select-v2/custom-width | item-height | The height of the dropdown item | ^[number] | 34 | | scrollbar-always-on | Controls whether the scrollbar is always displayed | ^[boolean] | false | | remote | whether search data from server | ^[boolean] | false | +| debounce ^(2.11.6) | debounce delay during remote search, in milliseconds | ^[number] | 300 | | remote-method | function that gets called when the input value changes. Its parameter is the current input value. To use this, `filterable` must be true | ^[Function]`(query: string) => void` | — | | validate-event | whether to trigger form validation | ^[boolean] | true | | offset ^(2.8.8) | offset of the dropdown | ^[number] | 12 | diff --git a/docs/en-US/component/select.md b/docs/en-US/component/select.md index 92427bf022..602cda9ae6 100644 --- a/docs/en-US/component/select.md +++ b/docs/en-US/component/select.md @@ -223,6 +223,7 @@ select/custom-label | allow-create | whether creating new items is allowed. To use this, `filterable` must be true | ^[boolean] | false | | filter-method | custom filter method, the first parameter is the current input value. To use this, `filterable` must be true | ^[Function]`(query: string) => void` | — | | remote | whether options are loaded from server | ^[boolean] | false | +| debounce ^(2.11.6) | debounce delay during remote search, in milliseconds | ^[number] | 300 | | remote-method | function that gets called when the input value changes. Its parameter is the current input value. To use this, `filterable` must be true | ^[Function]`(query: string) => void` | — | | remote-show-suffix | in remote search method show suffix icon | ^[boolean] | false | | loading | whether Select is loading data from server | ^[boolean] | false | diff --git a/packages/components/select-v2/__tests__/select.test.ts b/packages/components/select-v2/__tests__/select.test.ts index 5381d8d1ec..8c772c111d 100644 --- a/packages/components/select-v2/__tests__/select.test.ts +++ b/packages/components/select-v2/__tests__/select.test.ts @@ -23,6 +23,15 @@ vi.mock('lodash-unified', async () => { } }) +vi.mock('@vueuse/core', async () => { + return { + ...((await vi.importActual('@vueuse/core')) as Record), + useDebounceFn: vi.fn((fn) => { + return fn + }), + } +}) + const _mount = makeMountFunc({ components: { 'el-select': Select, @@ -66,6 +75,7 @@ interface SelectProps { allowCreate?: boolean popperAppendToBody?: boolean placeholder?: string + debounce?: number [key: string]: any } diff --git a/packages/components/select-v2/src/defaults.ts b/packages/components/select-v2/src/defaults.ts index 96e155c58b..81f5be7bbe 100644 --- a/packages/components/select-v2/src/defaults.ts +++ b/packages/components/select-v2/src/defaults.ts @@ -216,6 +216,13 @@ export const selectV2Props = buildProps({ * @description whether search data from server */ remote: Boolean, + /** + * @description debounce delay during remote search, in milliseconds + */ + debounce: { + type: Number, + default: 300, + }, /** * @description size of component */ diff --git a/packages/components/select-v2/src/useSelect.ts b/packages/components/select-v2/src/useSelect.ts index ac50095a7f..fb58c69265 100644 --- a/packages/components/select-v2/src/useSelect.ts +++ b/packages/components/select-v2/src/useSelect.ts @@ -7,19 +7,15 @@ import { watch, watchEffect, } from 'vue' -import { - findLastIndex, - get, - isEqual, - debounce as lodashDebounce, -} from 'lodash-unified' -import { useResizeObserver } from '@vueuse/core' +import { findLastIndex, get, isEqual } from 'lodash-unified' +import { useDebounceFn, useResizeObserver } from '@vueuse/core' import { ValidateComponentsMap, debugWarn, escapeStringRegexp, getEventCode, isArray, + isEmpty, isFunction, isNumber, isObject, @@ -82,6 +78,7 @@ const useSelect = (props: SelectV2Props, emit: SelectV2EmitFn) => { // data refs const popperSize = ref(-1) + const debouncing = ref(false) // DOM & Component refs const selectRef = ref() @@ -179,7 +176,7 @@ const useSelect = (props: SelectV2Props, emit: SelectV2EmitFn) => { return ValidateComponentsMap[validateState.value] }) - const debounce = computed(() => (props.remote ? 300 : 0)) + const debounce = computed(() => (props.remote ? props.debounce : 0)) const isRemoteSearchEmpty = computed( () => props.remote && !states.inputValue && !hasOptions.value @@ -390,7 +387,11 @@ const useSelect = (props: SelectV2Props, emit: SelectV2EmitFn) => { const dropdownMenuVisible = computed({ get() { - return expanded.value && (props.loading || !isRemoteSearchEmpty.value) + return ( + expanded.value && + (props.loading || !isRemoteSearchEmpty.value) && + (!debouncing.value || !isEmpty(states.previousQuery)) + ) }, set(val: boolean) { expanded.value = val @@ -445,7 +446,10 @@ const useSelect = (props: SelectV2Props, emit: SelectV2EmitFn) => { }) } - const debouncedOnInputChange = lodashDebounce(onInputChange, debounce.value) + const debouncedOnInputChange = useDebounceFn(() => { + onInputChange() + debouncing.value = false + }, debounce) const handleQueryChange = (val: string) => { if (states.previousQuery === val || isComposing.value) { @@ -768,6 +772,7 @@ const useSelect = (props: SelectV2Props, emit: SelectV2EmitFn) => { const onInput = (event: Event) => { states.inputValue = (event.target as HTMLInputElement).value if (props.remote) { + debouncing.value = true debouncedOnInputChange() } else { return onInputChange() diff --git a/packages/components/select/__tests__/select.test.ts b/packages/components/select/__tests__/select.test.ts index 76bc864adf..cdaef942d1 100644 --- a/packages/components/select/__tests__/select.test.ts +++ b/packages/components/select/__tests__/select.test.ts @@ -22,6 +22,15 @@ vi.mock('lodash-unified', async () => { } }) +vi.mock('@vueuse/core', async () => { + return { + ...((await vi.importActual('@vueuse/core')) as Record), + useDebounceFn: vi.fn((fn) => { + return fn + }), + } +}) + interface SelectProps { filterMethod?: any remoteMethod?: any @@ -38,6 +47,7 @@ interface SelectProps { defaultFirstOption?: boolean fitInputWidth?: boolean size?: 'small' | 'default' | 'large' + debounce?: number } const _mount = (template: string, data: any = () => ({}), otherObj?) => diff --git a/packages/components/select/src/select.ts b/packages/components/select/src/select.ts index d6b615eb2b..2a407f4fc8 100644 --- a/packages/components/select/src/select.ts +++ b/packages/components/select/src/select.ts @@ -115,6 +115,13 @@ export const selectProps = buildProps({ * @description whether options are loaded from server */ remote: Boolean, + /** + * @description debounce delay during remote search, in milliseconds + */ + debounce: { + type: Number, + default: 300, + }, /** * @description displayed text while loading data from server, default is 'Loading' */ diff --git a/packages/components/select/src/useSelect.ts b/packages/components/select/src/useSelect.ts index c60767e251..cfd04ebb89 100644 --- a/packages/components/select/src/useSelect.ts +++ b/packages/components/select/src/useSelect.ts @@ -7,13 +7,8 @@ import { watch, watchEffect, } from 'vue' -import { - findLastIndex, - get, - isEqual, - debounce as lodashDebounce, -} from 'lodash-unified' -import { useResizeObserver } from '@vueuse/core' +import { findLastIndex, get, isEqual } from 'lodash-unified' +import { useDebounceFn, useResizeObserver } from '@vueuse/core' import { ValidateComponentsMap, debugWarn, @@ -21,6 +16,7 @@ import { getEventCode, isArray, isClient, + isEmpty, isFunction, isIOS, isNumber, @@ -97,6 +93,7 @@ export const useSelect = (props: SelectProps, emit: SelectEmits) => { // the controller of the expanded popup const expanded = ref(false) const hoverOption = ref() + const debouncing = ref(false) const { form, formItem } = useFormItem() const { inputId } = useFormItemInputId(props, { @@ -170,7 +167,7 @@ export const useSelect = (props: SelectProps, emit: SelectEmits) => { (ValidateComponentsMap[validateState.value] as Component) ) - const debounce = computed(() => (props.remote ? 300 : 0)) + const debounce = computed(() => (props.remote ? props.debounce : 0)) const isRemoteSearchEmpty = computed( () => props.remote && !states.inputValue && states.options.size === 0 @@ -248,7 +245,11 @@ export const useSelect = (props: SelectProps, emit: SelectEmits) => { const dropdownMenuVisible = computed({ get() { - return expanded.value && (props.loading || !isRemoteSearchEmpty.value) + return ( + expanded.value && + (props.loading || !isRemoteSearchEmpty.value) && + (!debouncing.value || !isEmpty(states.previousQuery)) + ) }, set(val: boolean) { expanded.value = val @@ -490,15 +491,17 @@ export const useSelect = (props: SelectProps, emit: SelectEmits) => { const onInput = (event: Event) => { states.inputValue = (event.target as HTMLInputElement).value if (props.remote) { + debouncing.value = true debouncedOnInputChange() } else { return onInputChange() } } - const debouncedOnInputChange = lodashDebounce(() => { + const debouncedOnInputChange = useDebounceFn(() => { onInputChange() - }, debounce.value) + debouncing.value = false + }, debounce) const emitChange = (val: OptionValue | OptionValue[]) => { if (!isEqual(props.modelValue, val)) {