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 [hover, setHover] = useState(false);
const [addingTag, setAddingTag] = useState(false); const [addingTag, setAddingTag] = useState(false);
const [newTagInput, setNewTagInput] = useState(""); const [newTagInput, setNewTagInput] = useState("");
const [autocompleteIndex, setAutocompleteIndex] = useState(0);
const filteredSuggestions = allTags.filter( const filteredSuggestions = allTags.filter(
(t) => (t) =>
@ -132,32 +133,103 @@ export default function ItemRow({
className="border rounded px-1 py-0.5" className="border rounded px-1 py-0.5"
placeholder="Tag..." placeholder="Tag..."
value={newTagInput} value={newTagInput}
onChange={(e) => setNewTagInput(e.target.value)} onChange={e => {
setNewTagInput(e.target.value);
setAutocompleteIndex(0);
}}
autoFocus 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") { if (e.key === "Escape") {
setAddingTag(false); setAddingTag(false);
setNewTagInput(""); setNewTagInput("");
} }
}} }}
/> />
{newTagInput && filteredSuggestions.length > 0 && ( {/* Autocomplete-Dropdown */}
<ul className="absolute bg-white border rounded mt-1 shadow-md z-10"> {(() => {
{filteredSuggestions.map((tag) => ( 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 <li
key={tag.id} key={tag.id}
className="px-2 py-1 hover:bg-gray-100 cursor-pointer" className={
"px-2 py-1 cursor-pointer flex items-center " +
(idx === autocompleteIndex ? "bg-blue-100 font-bold" : "")
}
onMouseDown={() => { onMouseDown={() => {
onAddTag(item.id, tag.id); onAddTag(item.id, tag.id);
setAddingTag(false); setAddingTag(false);
setNewTagInput(""); setNewTagInput("");
}} }}
onMouseEnter={() => setAutocompleteIndex(idx)}
> >
#{tag.name} #{tag.name}
{tag.mandatory && <span className="text-red-500 font-bold ml-1">!</span>}
</li> </li>
))} ))}
</ul> </ul>
)} )
);
})()}
</div> </div>
)} )}