From c16fa32af293797d385714c024c31ebe5b0f7bfa Mon Sep 17 00:00:00 2001 From: Felix Zett Date: Wed, 17 Sep 2025 21:21:07 +0200 Subject: [PATCH] feat: tag autocomplete functionality in ItemRow component --- frontend/src/components/ItemRow.tsx | 110 +++++++++++++++++++++++----- 1 file changed, 91 insertions(+), 19 deletions(-) diff --git a/frontend/src/components/ItemRow.tsx b/frontend/src/components/ItemRow.tsx index 319f9ca..d798e05 100644 --- a/frontend/src/components/ItemRow.tsx +++ b/frontend/src/components/ItemRow.tsx @@ -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 && ( - - )} + {/* 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 && ( + + ) + ); + })()} )}