diff --git a/src/components/tags/autocomplete.tsx b/src/components/tags/autocomplete.tsx index ee865eb61..916e7aeed 100644 --- a/src/components/tags/autocomplete.tsx +++ b/src/components/tags/autocomplete.tsx @@ -1,10 +1,23 @@ -import React, { useCallback, useEffect, useRef, useState } from "react"; -// import { Command, CommandList, CommandItem, CommandGroup, CommandEmpty } from '../ui/command'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { TagInputStyleClassesProps, type Tag as TagType } from "./tag-input"; -import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList +} from "../ui/command"; +import { + Popover, + PopoverAnchor, + PopoverContent, + PopoverTrigger +} from "../ui/popover"; import { Button } from "../ui/button"; import { cn } from "@app/lib/cn"; import { useTranslations } from "next-intl"; +import { Check } from "lucide-react"; type AutocompleteProps = { tags: TagType[]; @@ -20,6 +33,8 @@ type AutocompleteProps = { inlineTags?: boolean; classStyleProps: TagInputStyleClassesProps["autoComplete"]; usePortal?: boolean; + /** Narrows the dropdown list from the main field (cmdk search filters further). */ + filterQuery?: string; }; export const Autocomplete: React.FC = ({ @@ -35,10 +50,10 @@ export const Autocomplete: React.FC = ({ inlineTags, children, classStyleProps, - usePortal + usePortal, + filterQuery = "" }) => { const triggerContainerRef = useRef(null); - const triggerRef = useRef(null); const inputRef = useRef(null); const popoverContentRef = useRef(null); const t = useTranslations(); @@ -46,17 +61,21 @@ export const Autocomplete: React.FC = ({ const [popoverWidth, setPopoverWidth] = useState(0); const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [inputFocused, setInputFocused] = useState(false); - const [popooverContentTop, setPopoverContentTop] = useState(0); - const [selectedIndex, setSelectedIndex] = useState(-1); + const [commandResetKey, setCommandResetKey] = useState(0); - // Dynamically calculate the top position for the popover content - useEffect(() => { - if (!triggerContainerRef.current || !triggerRef.current) return; - setPopoverContentTop( - triggerContainerRef.current?.getBoundingClientRect().bottom - - triggerRef.current?.getBoundingClientRect().bottom + const visibleOptions = useMemo(() => { + const q = filterQuery.trim().toLowerCase(); + if (!q) return autocompleteOptions; + return autocompleteOptions.filter((option) => + option.text.toLowerCase().includes(q) ); - }, [tags]); + }, [autocompleteOptions, filterQuery]); + + useEffect(() => { + if (isPopoverOpen) { + setCommandResetKey((k) => k + 1); + } + }, [isPopoverOpen]); // Close the popover when clicking outside of it useEffect(() => { @@ -135,36 +154,6 @@ export const Autocomplete: React.FC = ({ if (userOnBlur) userOnBlur(event); }; - const handleKeyDown = (event: React.KeyboardEvent) => { - if (!isPopoverOpen) return; - - switch (event.key) { - case "ArrowUp": - event.preventDefault(); - setSelectedIndex((prevIndex) => - prevIndex <= 0 - ? autocompleteOptions.length - 1 - : prevIndex - 1 - ); - break; - case "ArrowDown": - event.preventDefault(); - setSelectedIndex((prevIndex) => - prevIndex === autocompleteOptions.length - 1 - ? 0 - : prevIndex + 1 - ); - break; - case "Enter": - event.preventDefault(); - if (selectedIndex !== -1) { - toggleTag(autocompleteOptions[selectedIndex]); - setSelectedIndex(-1); - } - break; - } - }; - const toggleTag = (option: TagType) => { // Check if the tag already exists in the array const index = tags.findIndex((tag) => tag.text === option.text); @@ -197,18 +186,25 @@ export const Autocomplete: React.FC = ({ } } } - setSelectedIndex(-1); }; - const childrenWithProps = React.cloneElement( - children as React.ReactElement, - { - onKeyDown: handleKeyDown, - onFocus: handleInputFocus, - onBlur: handleInputBlur, - ref: inputRef + const child = children as React.ReactElement< + React.InputHTMLAttributes & { + ref?: React.Ref; } - ); + >; + const userOnKeyDown = child.props.onKeyDown; + + const childrenWithProps = React.cloneElement(child, { + onKeyDown: userOnKeyDown, + onFocus: handleInputFocus, + onBlur: handleInputBlur, + ref: inputRef + } as Partial< + React.InputHTMLAttributes & { + ref?: React.Ref; + } + >); return (
= ({ onOpenChange={handleOpenChange} modal={usePortal} > -
- {childrenWithProps} - - - -
+ + + + + +
+ -
- {autocompleteOptions.length > 0 ? ( -
+ + {t("noResults")} + - - Suggestions - -
- {autocompleteOptions.map((option, index) => { - const isSelected = index === selectedIndex; + {visibleOptions.map((option) => { + const isChosen = tags.some( + (tag) => tag.text === option.text + ); return ( -
toggleTag(option)} + value={`${option.text} ${option.id}`} + onSelect={() => toggleTag(option)} + className={classStyleProps?.commandItem} > -
- {option.text} - {tags.some( - (tag) => - tag.text === option.text - ) && ( - - - + -
+ /> + {option.text} + ); })} -
- ) : ( -
- {t("noResults")} -
- )} -
+
+
+
diff --git a/src/components/tags/tag-input.tsx b/src/components/tags/tag-input.tsx index 269967ccb..e8cfa370a 100644 --- a/src/components/tags/tag-input.tsx +++ b/src/components/tags/tag-input.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useMemo } from "react"; +import React from "react"; import { Input } from "../ui/input"; import { Button } from "../ui/button"; import { type VariantProps } from "class-variance-authority"; @@ -434,14 +434,6 @@ const TagInput = React.forwardRef( // const filteredAutocompleteOptions = autocompleteFilter // ? autocompleteOptions?.filter((option) => autocompleteFilter(option.text)) // : autocompleteOptions; - const filteredAutocompleteOptions = useMemo(() => { - return (autocompleteOptions || []).filter((option) => - option.text - .toLowerCase() - .includes(inputValue ? inputValue.toLowerCase() : "") - ); - }, [inputValue, autocompleteOptions]); - const displayedTags = sortTags ? [...tags].sort() : tags; const truncatedTags = truncate @@ -571,9 +563,9 @@ const TagInput = React.forwardRef( tags={tags} setTags={setTags} setInputValue={setInputValue} - autocompleteOptions={ - filteredAutocompleteOptions as Tag[] - } + autocompleteOptions={(autocompleteOptions || + []) as Tag[]} + filterQuery={inputValue} setTagCount={setTagCount} maxTags={maxTags} onTagAdd={onTagAdd} diff --git a/src/components/tags/tag-popover.tsx b/src/components/tags/tag-popover.tsx index 533619a7c..93f5e2c04 100644 --- a/src/components/tags/tag-popover.tsx +++ b/src/components/tags/tag-popover.tsx @@ -1,5 +1,10 @@ import React, { useCallback, useEffect, useRef, useState } from "react"; -import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; +import { + Popover, + PopoverAnchor, + PopoverContent, + PopoverTrigger +} from "../ui/popover"; import { TagInputStyleClassesProps, type Tag as TagType } from "./tag-input"; import { TagList, TagListProps } from "./tag-list"; import { Button } from "../ui/button"; @@ -33,33 +38,27 @@ export const TagPopover: React.FC = ({ ...tagProps }) => { const triggerContainerRef = useRef(null); - const triggerRef = useRef(null); const popoverContentRef = useRef(null); const inputRef = useRef(null); const [popoverWidth, setPopoverWidth] = useState(0); const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [inputFocused, setInputFocused] = useState(false); - const [sideOffset, setSideOffset] = useState(0); const t = useTranslations(); useEffect(() => { const handleResize = () => { - if (triggerContainerRef.current && triggerRef.current) { + if (triggerContainerRef.current) { setPopoverWidth(triggerContainerRef.current.offsetWidth); - setSideOffset( - triggerContainerRef.current.offsetWidth - - triggerRef?.current?.offsetWidth - ); } }; - handleResize(); // Call on mount and layout changes + handleResize(); - window.addEventListener("resize", handleResize); // Adjust on window resize + window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); - }, [triggerContainerRef, triggerRef]); + }, []); // Close the popover when clicking outside of it useEffect(() => { @@ -135,52 +134,54 @@ export const TagPopover: React.FC = ({ onOpenChange={handleOpenChange} modal={usePortal} > -
- {React.cloneElement(children as React.ReactElement, { - onFocus: handleInputFocus, - onBlur: handleInputBlur, - ref: inputRef - })} - - - -
+ + + + + +
+