feat: tag autocomplete functionality in ItemRow component

This commit is contained in:
Felix Zett 2025-09-17 21:21:07 +02:00
parent 005a1c08fa
commit c16fa32af2

View file

@ -25,6 +25,7 @@ export default function ItemRow({
const [hover, setHover] = useState(false);
const [addingTag, setAddingTag] = useState(false);
const [newTagInput, setNewTagInput] = useState("");
const [autocompleteIndex, setAutocompleteIndex] = useState(0);
const filteredSuggestions = allTags.filter(
(t) =>
@ -132,32 +133,103 @@ export default function ItemRow({
className="border rounded px-1 py-0.5"
placeholder="Tag..."
value={newTagInput}
onChange={(e) => setNewTagInput(e.target.value)}
onChange={e => {
setNewTagInput(e.target.value);
setAutocompleteIndex(0);
}}
autoFocus
onKeyDown={(e) => {
onKeyDown={e => {
// Autocomplete navigation
const match = newTagInput.match(/^(\w*)$/);
const suggestions = match
? allTags.filter(
tag =>
tag.name.toLowerCase().startsWith(match[1].toLowerCase()) &&
!item.tags.some((it) => it.id === tag.id)
)
: [];
const showDropdown = suggestions.length > 0 && match && match[1].length >= 0;
if (showDropdown) {
if (e.key === "ArrowDown") {
e.preventDefault();
setAutocompleteIndex(i => (i + 1) % suggestions.length);
return;
}
if (e.key === "ArrowUp") {
e.preventDefault();
setAutocompleteIndex(i => (i - 1 + suggestions.length) % suggestions.length);
return;
}
if (e.key === "Enter") {
e.preventDefault();
const tag = suggestions[autocompleteIndex];
if (tag) {
onAddTag(item.id, tag.id);
setAddingTag(false);
setNewTagInput("");
}
return;
}
if (e.key === "Tab") {
e.preventDefault();
const tag = suggestions[autocompleteIndex];
if (tag) {
onAddTag(item.id, tag.id);
setNewTagInput("");
setAutocompleteIndex(0);
// keep addingTag true so you can add another tag
setTimeout(() => {
e.target.focus();
}, 0);
}
return;
}
}
if (e.key === "Escape") {
setAddingTag(false);
setNewTagInput("");
}
}}
/>
{newTagInput && filteredSuggestions.length > 0 && (
<ul className="absolute bg-white border rounded mt-1 shadow-md z-10">
{filteredSuggestions.map((tag) => (
<li
key={tag.id}
className="px-2 py-1 hover:bg-gray-100 cursor-pointer"
onMouseDown={() => {
onAddTag(item.id, tag.id);
setAddingTag(false);
setNewTagInput("");
}}
>
#{tag.name}
</li>
))}
</ul>
)}
{/* Autocomplete-Dropdown */}
{(() => {
const match = newTagInput.match(/^(\w*)$/);
const suggestions = match
? allTags.filter(
tag =>
tag.name.toLowerCase().startsWith(match[1].toLowerCase()) &&
!item.tags.some((it) => it.id === tag.id)
)
: [];
const showDropdown = suggestions.length > 0 && match && match[1].length >= 0;
if (showDropdown && autocompleteIndex >= suggestions.length) setAutocompleteIndex(0);
return (
showDropdown && (
<ul className="absolute bg-white border rounded mt-1 shadow-md z-10 w-full max-h-40 overflow-auto">
{suggestions.map((tag, idx) => (
<li
key={tag.id}
className={
"px-2 py-1 cursor-pointer flex items-center " +
(idx === autocompleteIndex ? "bg-blue-100 font-bold" : "")
}
onMouseDown={() => {
onAddTag(item.id, tag.id);
setAddingTag(false);
setNewTagInput("");
}}
onMouseEnter={() => setAutocompleteIndex(idx)}
>
#{tag.name}
{tag.mandatory && <span className="text-red-500 font-bold ml-1">!</span>}
</li>
))}
</ul>
)
);
})()}
</div>
)}