initial commit
This commit is contained in:
57
components/TagInput.tsx
Normal file
57
components/TagInput.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user