Tags Input

Editeur d'etiquettes convivial au clavier pour ajouter et supprimer des valeurs de chaine. Installez Tags Input depuis le registre VLLNT UI avec la CLI shadcn.

Signaler un bug

Editeur d'etiquettes convivial au clavier pour ajouter et supprimer des valeurs de chaine. Fait partie de la famille form dans VLLNT UI, il est publie comme entree de registre lisible par machine — copiez la source directement dans votre application avec la CLI shadcn et possedez-la, sans dependance d'execution a une bibliotheque de composants.

Apercu

Basculez entre clair et sombre pour inspecter l'apercu Storybook integre.

Installation

Ajoutez Tags Input a votre projet avec la CLI shadcn. La source arrive dans votre code, prete a etre adaptee :

pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/tags-input.json

Source

"use client"; import * as React from "react"; import { X } from "lucide-react"; import { cn } from "../../lib/utils"; function normalizeTag(tag: string) { return tag.trim(); } function getNormalizedTags(tags: string[]) { return tags.reduce<{ seen: Set<string>; tags: string[] }>( (state, tag) => { const normalizedTag = normalizeTag(tag); if (normalizedTag.length > 0 && !state.seen.has(normalizedTag)) { state.seen.add(normalizedTag); state.tags.push(normalizedTag); } return state; }, { seen: new Set(), tags: [] }, ).tags; } function shouldAddTagFromKey(key: string) { return key === "Enter" || key === ","; } type TagsInputStateOptions = { defaultValue: string[]; onValueChange?: (value: string[]) => void; value?: string[]; }; type TagsInputHandlersOptions = { disabled: boolean; inputValue: string; onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>; setInputValue: React.Dispatch<React.SetStateAction<string>>; tags: string[]; updateTags: (nextTags: string[]) => void; }; type TagListProps = { disabled: boolean; onRemove: (tag: string) => void; tags: string[]; }; function useTagsInputState({ defaultValue, onValueChange, value, }: TagsInputStateOptions) { const [uncontrolledValue, setUncontrolledValue] = React.useState(() => getNormalizedTags(defaultValue), ); const isControlled = value !== undefined; const tags = React.useMemo( () => getNormalizedTags(value ?? uncontrolledValue), [uncontrolledValue, value], ); const updateTags = React.useCallback( (nextTags: string[]) => { const normalizedTags = getNormalizedTags(nextTags); if (!isControlled) { setUncontrolledValue(normalizedTags); } onValueChange?.(normalizedTags); }, [isControlled, onValueChange], ); return { tags, updateTags }; } function useTagsInputHandlers({ disabled, inputValue, onKeyDown, setInputValue, tags, updateTags, }: TagsInputHandlersOptions) { const removeTag = React.useCallback( (tagToRemove: string) => { updateTags(tags.filter((tag) => tag !== tagToRemove)); }, [tags, updateTags], ); const commitTag = React.useCallback(() => { const nextTag = normalizeTag(inputValue); if (nextTag.length === 0 || tags.includes(nextTag)) { setInputValue(""); return; } updateTags([...tags, nextTag]); setInputValue(""); }, [inputValue, setInputValue, tags, updateTags]); const handleKeyDown = React.useCallback( (event: React.KeyboardEvent<HTMLInputElement>) => { onKeyDown?.(event); if (event.defaultPrevented || disabled) { return; } if (shouldAddTagFromKey(event.key)) { event.preventDefault(); commitTag(); return; } if ( (event.key === "Backspace" || event.key === "Delete") && inputValue.length === 0 ) { const lastTag = tags.at(-1); if (lastTag) { event.preventDefault(); removeTag(lastTag); } } }, [commitTag, disabled, inputValue.length, onKeyDown, removeTag, tags], ); return { handleKeyDown, removeTag }; } function TagList({ disabled, onRemove, tags }: TagListProps) { return ( <ul className="flex flex-wrap items-center gap-2"> {tags.map((tag) => ( <li className="flex items-center gap-1 rounded-md border bg-muted px-2 py-1 text-sm text-foreground" key={tag} > <span>{tag}</span> <button aria-label={`Remove ${tag}`} className="rounded-sm text-muted-foreground outline-none ring-offset-background transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2" disabled={disabled} onClick={(event) => { event.stopPropagation(); onRemove(tag); }} type="button" > <X className="size-3.5" /> </button> </li> ))} </ul> ); } export type TagsInputProps = Omit< React.ComponentPropsWithoutRef<"input">, "defaultValue" | "onChange" | "value" > & { defaultValue?: string[]; onValueChange?: (value: string[]) => void; value?: string[]; }; const TagsInput = ({ className, defaultValue = [], disabled = false, onBlur, onKeyDown, onValueChange, placeholder = "Add a tag", ref, value, ...props }: TagsInputProps & { ref?: React.Ref<HTMLInputElement> }) => { const [inputValue, setInputValue] = React.useState(""); const { tags, updateTags } = useTagsInputState({ defaultValue, onValueChange, value, }); const { handleKeyDown, removeTag } = useTagsInputHandlers({ disabled, inputValue, onKeyDown, setInputValue, tags, updateTags, }); return ( <div aria-disabled={disabled || undefined} className={cn( "flex min-h-10 w-full flex-wrap items-center gap-2 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background transition-colors focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2", disabled && "cursor-not-allowed opacity-50", className, )} data-disabled={disabled ? "true" : undefined} role="group" > <TagList disabled={disabled} onRemove={removeTag} tags={tags} /> <input {...props} className="min-w-[8rem] flex-1 border-0 bg-transparent p-0 text-sm placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed" disabled={disabled} onBlur={onBlur} onChange={(event) => { setInputValue(event.target.value); }} onKeyDown={handleKeyDown} placeholder={placeholder} ref={ref} type="text" value={inputValue} /> </div> ); }; TagsInput.displayName = "TagsInput"; export { TagsInput };

Stories

Explorez chaque variante et etat dans le Storybook interactif :

Apercu

Basculez entre clair et sombre pour inspecter l'apercu Storybook integre.

Dependances

  • @vllnt/ui@^0.3.0