Skip to content

Commit

Permalink
feat(TagsInput): support multiple delimiters via RegExp (#1414)
Browse files Browse the repository at this point in the history
* feat(TagsInput): support multiple delimiters via RegExp (#1400)

* feat(TagsInput): use `replace` instead of `replaceAll` to avoid the need for RegExp global flag

* feat(TagsInput): update description for delimiter prop

* feat(TagsInput): update docs, add multiple delimiters example

* feat(TagsInput): add test cases for delimiter prop

* chore: fix test

---------

Co-authored-by: Raman Paulau <[email protected]>
  • Loading branch information
zernonia and romansp authored Nov 4, 2024
1 parent 920d402 commit 144999d
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 6 deletions.
25 changes: 24 additions & 1 deletion docs/content/docs/components/tags-input.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ You can compose Tags input together with [Combobox](../components/combobox.html)

You can automatically add tags on paste by passing the `add-on-paste` prop.

```vue line=6
```vue line=8
<script setup lang="ts">
import { TagsInputInput, TagsInputItem, TagsInputItemDelete, TagsInputItemText, TagsInputRoot } from 'reka-ui'
</script>
Expand All @@ -180,6 +180,29 @@ import { TagsInputInput, TagsInputItem, TagsInputItemDelete, TagsInputItemText,
</template>
```

### Multiple delimiters

You can pass `RegExp` as `delimiter` to allow multiple characters to trigger addition of a new tag. When `add-on-paste` is passed it will be also used to split tags for `@paste` event.

```vue line=4-5,11
<script setup lang="ts">
import { TagsInputInput, TagsInputItem, TagsInputItemDelete, TagsInputItemText, TagsInputRoot } from 'radix-vue'
// split by space, comma, semicolon, tab, or newline
const delimiter = /[ ,;\t\n\r]+/
</script>
<template>
<TagsInputRoot
v-model="modelValue"
:delimiter="delimiter"
add-on-paste
>
</TagsInputRoot>
</template>
```

## Accessibility

### Keyboard Interactions
Expand Down
71 changes: 71 additions & 0 deletions packages/core/src/TagsInput/TagsInput.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import TagsInputObject from './story/_TagsInputObject.vue'
import type { DOMWrapper, VueWrapper } from '@vue/test-utils'
import { mount } from '@vue/test-utils'
import { nextTick } from 'vue'
import userEvent from '@testing-library/user-event'

describe('given default TagsInput', () => {
// @ts-expect-error we return empty object
Expand Down Expand Up @@ -232,4 +233,74 @@ describe('given a TagsInput with objects', async () => {
expect(consoleWarnMockFunction).toHaveBeenCalledOnce()
expect(consoleWarnMockFunction).toHaveBeenLastCalledWith('You must provide a `convertValue` function when using objects as values.')
})

describe('given a TagsInput with delimiter', async () => {
const setupDelimiter = (delimiter: string | RegExp) => {
const wrapper = mount(TagsInput, {
props: {
delimiter,
addOnPaste: true,
},
attachTo: document.body,
})

const input = wrapper.find('input')

return {
wrapper,
input,
}
}

it('should add tag on typing single delimiter character', async () => {
const { wrapper, input } = setupDelimiter(',')
const user = userEvent.setup()

await user.type(input.element, 'tag1,')

const tags = wrapper.findAll('[data-reka-collection-item]')
expect(tags[1].text()).toBe('tag1')
})

it('should add tag on typing multiple delimiter characters', async () => {
const { wrapper, input } = setupDelimiter(/[ ,;]+/)
const user = userEvent.setup()

await user.type(input.element, 'tag1,')
await user.type(input.element, 'tag2 ')
await user.type(input.element, 'tag3;')

const tags = wrapper.findAll('[data-reka-collection-item]')
expect(tags[1].text()).toBe('tag1')
expect(tags[2].text()).toBe('tag2')
expect(tags[3].text()).toBe('tag3')
})

it('should add multiple tags on pasting text with single delimiter character', async () => {
const { wrapper, input } = setupDelimiter(',')
const user = userEvent.setup()

await user.click(input.element)
await user.paste('tag1,tag2,tag3')

const tags = wrapper.findAll('[data-reka-collection-item]')
expect(tags[1].text()).toBe('tag1')
expect(tags[2].text()).toBe('tag2')
expect(tags[3].text()).toBe('tag3')
})

it('should add multiple tags on pasting text with multiple delimiter characters', async () => {
const { wrapper, input } = setupDelimiter(/[ ,;]+/)
const user = userEvent.setup()

await user.click(input.element)
await user.paste('tag1, tag2;tag3 tag4')

const tags = wrapper.findAll('[data-reka-collection-item]')
expect(tags[1].text()).toBe('tag1')
expect(tags[2].text()).toBe('tag2')
expect(tags[3].text()).toBe('tag3')
expect(tags[4].text()).toBe('tag4')
})
})
})
8 changes: 6 additions & 2 deletions packages/core/src/TagsInput/TagsInputInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,14 @@ async function handleCustomKeydown(event: Event) {
function handleInput(event: InputEvent) {
context.isInvalidInput.value = false
if (event.data === null)
return
const delimiter = context.delimiter.value
if (delimiter === event.data) {
const matchesDelimiter = delimiter === event.data || (delimiter instanceof RegExp && delimiter.test(event.data))
if (matchesDelimiter) {
const target = event.target as HTMLInputElement
target.value = target.value.replaceAll(delimiter, '')
target.value = target.value.replace(delimiter, '')
const isAdded = context.onAddValue(target.value)
if (isAdded)
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/TagsInput/TagsInputRoot.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ export interface TagsInputRootProps<T = AcceptableInputValue> extends PrimitiveP
duplicate?: boolean
/** When `true`, prevents the user from interacting with the tags input. */
disabled?: boolean
/** The character to trigger the addition of a new tag. Also used to split tags for `@paste` event */
delimiter?: string
/** The character or regular expression to trigger the addition of a new tag. Also used to split tags for `@paste` event */
delimiter?: string | RegExp
/** The reading direction of the combobox when applicable. <br> If omitted, inherits globally from `ConfigProvider` or assumes LTR (left-to-right) reading mode. */
dir?: Direction
/** Maximum number of tags. */
Expand Down Expand Up @@ -52,7 +52,7 @@ export interface TagsInputRootContext<T = AcceptableInputValue> {
addOnTab: Ref<boolean>
addOnBlur: Ref<boolean>
disabled: Ref<boolean>
delimiter: Ref<string>
delimiter: Ref<string | RegExp>
dir: Ref<Direction>
max: Ref<number>
id: Ref<string | undefined> | undefined
Expand Down

0 comments on commit 144999d

Please sign in to comment.