From 8a243036fe2ed038422deca55e278ccd9dc9db68 Mon Sep 17 00:00:00 2001 From: Noblet Ouways <91417411+Dsaquel@users.noreply.github.com> Date: Tue, 8 Jul 2025 11:02:13 +0200 Subject: [PATCH] fix(components): [input-tag] paste multiple delimiters (#21256) * fix(components): [input-tag] paste multiple delimiters * chore: refact * refactor: easier to read & import types * chore: remove useless convert regex Co-authored-by: btea <2356281422@qq.com> * chore: fix * chore: fix * chore: same logic on paste & better tests * chore: fix edge case * chore: fix * chore: add --------- Co-authored-by: btea <2356281422@qq.com> --- docs/en-US/component/input-tag.md | 18 ++++---- .../input-tag/__tests__/input-tag.test.tsx | 39 ++++++++++++++-- .../src/composables/use-input-tag.ts | 46 ++++++++++++------- .../components/input-tag/src/input-tag.ts | 2 +- 4 files changed, 74 insertions(+), 31 deletions(-) diff --git a/docs/en-US/component/input-tag.md b/docs/en-US/component/input-tag.md index 223a5e6fc5..9b0a2f696c 100644 --- a/docs/en-US/component/input-tag.md +++ b/docs/en-US/component/input-tag.md @@ -137,15 +137,15 @@ input-tag/prefix-suffix ### Events -| Name | Description | Type | -| ---------- | --------------------------------------- | ---------------------------------------- | -| change | triggers when the modelValue change | ^[Function]`(value: string[]) => void` | -| input | triggers when the input value change | ^[Function]`(value: string) => void` | -| add-tag | triggers when a tag is added | ^[Function]`(value: string) => void` | -| remove-tag | triggers when a tag is removed | ^[Function]`(value: string) => void` | -| focus | triggers when InputTag focuses | ^[Function]`(event: FocusEvent) => void` | -| blur | triggers when InputTag blurs | ^[Function]`(event: FocusEvent) => void` | -| clear | triggers when the clear icon is clicked | ^[Function]`() => void` | +| Name | Description | Type | +| ---------- | --------------------------------------- | ------------------------------------------------- | +| change | triggers when the modelValue change | ^[Function]`(value: string[]) => void` | +| input | triggers when the input value change | ^[Function]`(value: string) => void` | +| add-tag | triggers when a tag is added | ^[Function]`(value: string \| string []) => void` | +| remove-tag | triggers when a tag is removed | ^[Function]`(value: string) => void` | +| focus | triggers when InputTag focuses | ^[Function]`(event: FocusEvent) => void` | +| blur | triggers when InputTag blurs | ^[Function]`(event: FocusEvent) => void` | +| clear | triggers when the clear icon is clicked | ^[Function]`() => void` | ### Slots diff --git a/packages/components/input-tag/__tests__/input-tag.test.tsx b/packages/components/input-tag/__tests__/input-tag.test.tsx index ca8e602c61..3f529e1785 100644 --- a/packages/components/input-tag/__tests__/input-tag.test.tsx +++ b/packages/components/input-tag/__tests__/input-tag.test.tsx @@ -233,26 +233,57 @@ describe('InputTag.vue', () => { describe('delimiter', () => { test('with string', async () => { const inputValue = ref() + const addTag = vi.fn() const wrapper = mount(() => ( - + )) - await wrapper.find('input').setValue(`${AXIOM},`) + + expect(addTag).toBeCalledWith(AXIOM) expect(wrapper.findAll('.el-tag').length).toBe(1) expect(wrapper.find('.el-tag').text()).toBe(AXIOM) expect(inputValue.value).toEqual([AXIOM]) }) test('with RegExp', async () => { const inputValue = ref() + const addTag = vi.fn() const wrapper = mount(() => ( - + )) - await wrapper.find('input').setValue(`${AXIOM}.`) + + expect(addTag).toBeCalledWith(AXIOM) expect(wrapper.findAll('.el-tag').length).toBe(1) expect(wrapper.find('.el-tag').text()).toBe(AXIOM) expect(inputValue.value).toEqual([AXIOM]) }) + test('paste multiple delimiter', async () => { + const inputValue = ref() + const addTag = vi.fn() + const wrapper = mount(() => ( + + )) + + await wrapper + .find('input') + .setValue(`${AXIOM}.${AXIOM}.${AXIOM}.${AXIOM}.`) + + const result = [AXIOM, AXIOM, AXIOM, AXIOM] + expect(wrapper.findAll('.el-tag').length).toBe(4) + expect(addTag).toBeCalledWith(result) + wrapper + .findAll('.el-tag') + .forEach((tag) => expect(tag.text()).toBe(AXIOM)) + expect(inputValue.value).toEqual(result) + }) }) describe('events', () => { diff --git a/packages/components/input-tag/src/composables/use-input-tag.ts b/packages/components/input-tag/src/composables/use-input-tag.ts index fe342ff303..ec20d4d1be 100644 --- a/packages/components/input-tag/src/composables/use-input-tag.ts +++ b/packages/components/input-tag/src/composables/use-input-tag.ts @@ -5,14 +5,12 @@ import { INPUT_EVENT, UPDATE_MODEL_EVENT, } from '@element-plus/constants' -import { type EmitFn, debugWarn, isUndefined } from '@element-plus/utils' +import { debugWarn, ensureArray, isUndefined } from '@element-plus/utils' import { useComposition, useFocusController } from '@element-plus/hooks' -import { - type FormItemContext, - useFormDisabled, - useFormSize, -} from '@element-plus/components/form' +import { useFormDisabled, useFormSize } from '@element-plus/components/form' +import type { EmitFn } from '@element-plus/utils' +import type { FormItemContext } from '@element-plus/components/form' import type { InputTagEmits, InputTagProps } from '../input-tag' interface UseInputTagOptions { @@ -41,6 +39,26 @@ export function useInputTag({ props, emit, formItem }: UseInputTagOptions) { : (props.modelValue?.length ?? 0) >= props.max }) + const addTagsEmit = (value: string | string[]) => { + const list = [...(props.modelValue ?? []), ...ensureArray(value)] + + emit(UPDATE_MODEL_EVENT, list) + emit(CHANGE_EVENT, list) + emit('add-tag', value) + inputValue.value = undefined + } + + const getDelimitedTags = (input: string) => { + const tags = input + .split(props.delimiter) + .filter((val) => val && val !== input) + if (props.max) { + const maxInsert = props.max - (props.modelValue?.length ?? 0) + tags.splice(maxInsert) + } + return tags.length === 1 ? tags[0] : tags + } + const handleInput = (event: Event) => { if (inputLimit.value) { inputValue.value = undefined @@ -48,11 +66,10 @@ export function useInputTag({ props, emit, formItem }: UseInputTagOptions) { } if (isComposing.value) return - if (props.delimiter) { - const replacement = inputValue.value?.replace(props.delimiter, '') - if (replacement?.length !== inputValue.value?.length) { - inputValue.value = replacement - handleAddTag() + if (props.delimiter && inputValue.value) { + const tags = getDelimitedTags(inputValue.value) + if (tags.length) { + addTagsEmit(tags) } } emit(INPUT_EVENT, (event.target as HTMLInputElement).value) @@ -86,12 +103,7 @@ export function useInputTag({ props, emit, formItem }: UseInputTagOptions) { const handleAddTag = () => { const value = inputValue.value?.trim() if (!value || inputLimit.value) return - const list = [...(props.modelValue ?? []), value] - - emit(UPDATE_MODEL_EVENT, list) - emit(CHANGE_EVENT, list) - emit('add-tag', value) - inputValue.value = undefined + addTagsEmit(value) } const handleRemoveTag = (index: number) => { diff --git a/packages/components/input-tag/src/input-tag.ts b/packages/components/input-tag/src/input-tag.ts index bf5f41017e..f510a8ad28 100644 --- a/packages/components/input-tag/src/input-tag.ts +++ b/packages/components/input-tag/src/input-tag.ts @@ -140,7 +140,7 @@ export const inputTagEmits = { isArray(value) || isUndefined(value), [CHANGE_EVENT]: (value?: string[]) => isArray(value) || isUndefined(value), [INPUT_EVENT]: (value: string) => isString(value), - 'add-tag': (value: string) => isString(value), + 'add-tag': (value: string | string[]) => isString(value) || isArray(value), 'remove-tag': (value: string) => isString(value), focus: (evt: FocusEvent) => evt instanceof FocusEvent, blur: (evt: FocusEvent) => evt instanceof FocusEvent,