58 lines
1.6 KiB
TypeScript
58 lines
1.6 KiB
TypeScript
"use client";
|
||
|
||
import { useId, useState } from "react";
|
||
import { normalizeTags } from "@/lib/validate";
|
||
|
||
type Props = { value: string[]; onChange: (tags: string[]) => void; suggestions: string[] };
|
||
|
||
/** Chip-style tag entry: Enter or comma adds, Backspace on empty removes the last. */
|
||
export default function TagInput({ value, onChange, suggestions }: Props) {
|
||
const [draft, setDraft] = useState("");
|
||
const listId = useId();
|
||
|
||
function commit(text: string) {
|
||
if (!text.trim()) return;
|
||
onChange(normalizeTags([...value, ...text.split(",")]));
|
||
setDraft("");
|
||
}
|
||
|
||
return (
|
||
<div className="tag-input">
|
||
{value.map((tag) => (
|
||
<span key={tag} className="chip">
|
||
{tag}
|
||
<button type="button" aria-label={`remove ${tag}`} onClick={() => onChange(value.filter((t) => t !== tag))}>
|
||
×
|
||
</button>
|
||
</span>
|
||
))}
|
||
<input
|
||
list={listId}
|
||
value={draft}
|
||
placeholder={value.length ? "" : "add tags…"}
|
||
onChange={(e) => {
|
||
const v = e.target.value;
|
||
if (v.endsWith(",")) commit(v);
|
||
else setDraft(v);
|
||
}}
|
||
onKeyDown={(e) => {
|
||
if (e.key === "Enter") {
|
||
e.preventDefault();
|
||
commit(draft);
|
||
} else if (e.key === "Backspace" && !draft && value.length) {
|
||
onChange(value.slice(0, -1));
|
||
}
|
||
}}
|
||
onBlur={() => commit(draft)}
|
||
/>
|
||
<datalist id={listId}>
|
||
{suggestions
|
||
.filter((s) => !value.includes(s))
|
||
.map((s) => (
|
||
<option key={s} value={s} />
|
||
))}
|
||
</datalist>
|
||
</div>
|
||
);
|
||
}
|