feat(web-core): add VtsTagsInput component

This commit is contained in:
Olivier Floch
2026-10-21 16:21:24 +02:00
parent 8d25cb6b94
commit 75e990343a
4 changed files with 120 additions and 0 deletions

View File

@@ -527,6 +527,13 @@ declare module 'vue-router/auto-routes' {
Record<never, never>,
| never
>,
'/story/web-core/tags-input/vts-tags-input': RouteRecordInfo<
'/story/web-core/tags-input/vts-tags-input',
'/story/web-core/tags-input/vts-tags-input',
Record<never, never>,
Record<never, never>,
| never
>,
'/story/web-core/tree/vts-tree-item': RouteRecordInfo<
'/story/web-core/tree/vts-tree-item',
'/story/web-core/tree/vts-tree-item',
@@ -1572,6 +1579,12 @@ declare module 'vue-router/auto-routes' {
views:
| never
}
'src/stories/web-core/tags-input/vts-tags-input.story.vue': {
routes:
| '/story/web-core/tags-input/vts-tags-input'
views:
| never
}
'src/stories/web-core/tree/vts-tree-item.story.vue': {
routes:
| '/story/web-core/tree/vts-tree-item'

View File

@@ -0,0 +1,14 @@
# Usage
Enter or comma adds the typed value: it is trimmed, and an empty value or a value already in the list is ignored. A
pasted comma-separated draft adds each of its values. Enter while an IME composition is in progress adds nothing.
Removing a chip updates the model with the new array.
The label comes from the surrounding `VtsInputWrapper`.
```vue-template
<VtsInputWrapper label="Values">
<VtsTagsInput v-model="values" accent="brand" placeholder="Add a value" />
</VtsInputWrapper>
```

View File

@@ -0,0 +1,21 @@
<template>
<ComponentStory
v-slot="{ properties }"
:params="[
model().type('string[]').preset(['alpha', 'beta']).help('Enter or comma adds the typed value'),
prop('accent').enum('brand', 'warning', 'danger').required().preset('brand').widget(),
prop('placeholder').str().preset('Add a value').widget(),
]"
>
<VtsInputWrapper label="Values">
<VtsTagsInput v-bind="properties" />
</VtsInputWrapper>
</ComponentStory>
</template>
<script lang="ts" setup>
import ComponentStory from '@/components/component-story/ComponentStory.vue'
import { model, prop } from '@/libs/story/story-param.ts'
import VtsInputWrapper from '@core/components/input-wrapper/VtsInputWrapper.vue'
import VtsTagsInput from '@core/components/tags-input/VtsTagsInput.vue'
</script>

View File

@@ -0,0 +1,72 @@
<template>
<div class="vts-tags-input">
<UiInput v-model="draft" :accent :placeholder @keydown="(event: KeyboardEvent) => handleKeydown(event)" />
<div v-if="tags.length > 0" class="chips">
<UiChip v-for="(tag, index) of tags" :key="index" accent="info" @remove="removeTag(index)">
{{ tag }}
</UiChip>
</div>
</div>
</template>
<script lang="ts" setup>
import UiChip from '@core/components/ui/chip/UiChip.vue'
import UiInput, { type InputAccent } from '@core/components/ui/input/UiInput.vue'
import { ref } from 'vue'
const { accent, placeholder } = defineProps<{
accent: InputAccent
placeholder?: string
}>()
const tags = defineModel<string[]>({ required: true })
const draft = ref('')
const TAG_SEPARATOR_KEYS = ['Enter', ',']
// A pasted draft may hold several comma-separated tags
function addDraftTags() {
const draftTags = new Set(draft.value.split(',').map(tag => tag.trim()))
const newTags = [...draftTags].filter(tag => tag !== '' && !tags.value.includes(tag))
draft.value = ''
if (newTags.length === 0) {
return
}
tags.value = [...tags.value, ...newTags]
}
function handleKeydown(event: KeyboardEvent) {
// Enter also validates an IME composition, which must not add a half-typed tag
const isSeparator = !event.isComposing && TAG_SEPARATOR_KEYS.includes(event.key)
if (!isSeparator) {
return
}
// Prevents a form submit on Enter and keeps the comma out of the draft
event.preventDefault()
addDraftTags()
}
function removeTag(index: number) {
tags.value = tags.value.filter((_tag, tagIndex) => tagIndex !== index)
}
</script>
<style lang="postcss" scoped>
.vts-tags-input {
display: flex;
flex-direction: column;
gap: 0.8rem;
.chips {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
}
}
</style>