From e7bd9a9d42c4c9d1796eb8b1190b11fbc88573bd Mon Sep 17 00:00:00 2001 From: Aslam H Date: Sat, 7 Sep 2024 01:06:19 +0700 Subject: [PATCH] chore: enhance Autocomplete --- web/components/routes/public/Autocomplete.tsx | 103 ++++++++++++------ 1 file changed, 72 insertions(+), 31 deletions(-) diff --git a/web/components/routes/public/Autocomplete.tsx b/web/components/routes/public/Autocomplete.tsx index e288e99b..fae825d5 100644 --- a/web/components/routes/public/Autocomplete.tsx +++ b/web/components/routes/public/Autocomplete.tsx @@ -1,10 +1,9 @@ -"use client" - -import React, { useState, useRef, useCallback, useMemo } from "react" +import React, { useState, useRef, useCallback, useMemo, useEffect } from "react" import { Command, CommandGroup, CommandItem, CommandList } from "@/components/ui/command" import { Command as CommandPrimitive } from "cmdk" import { motion, AnimatePresence } from "framer-motion" -import { cn, searchSafeRegExp } from "@/lib/utils" +import { cn, searchSafeRegExp, shuffleArray } from "@/lib/utils" +import { useMountedState } from "react-use" interface GraphNode { name: string @@ -20,22 +19,30 @@ interface AutocompleteProps { export function Autocomplete({ topics = [], onSelect, onInputChange }: AutocompleteProps): JSX.Element { const inputRef = useRef(null) + const isMounted = useMountedState() const [open, setOpen] = useState(false) const [inputValue, setInputValue] = useState("") + const [isInitialOpen, setIsInitialOpen] = useState(true) + const [hasInteracted, setHasInteracted] = useState(false) + + const initialShuffledTopics = useMemo(() => shuffleArray(topics).slice(0, 5), [topics]) const filteredTopics = useMemo(() => { if (!inputValue) { - return topics.slice(0, 5) + return initialShuffledTopics } const regex = searchSafeRegExp(inputValue) - return topics.filter( - topic => - regex.test(topic.name) || - regex.test(topic.prettyName) || - topic.connectedTopics.some(connectedTopic => regex.test(connectedTopic)) - ) - }, [inputValue, topics]) + return topics + .filter( + topic => + regex.test(topic.name) || + regex.test(topic.prettyName) || + topic.connectedTopics.some(connectedTopic => regex.test(connectedTopic)) + ) + .sort((a, b) => a.prettyName.localeCompare(b.prettyName)) + .slice(0, 10) + }, [inputValue, topics, initialShuffledTopics]) const handleSelect = useCallback( (topic: GraphNode) => { @@ -52,62 +59,96 @@ export function Autocomplete({ topics = [], onSelect, onInputChange }: Autocompl handleSelect(filteredTopics[0]) } else if ((e.key === "Backspace" || e.key === "Delete") && inputRef.current?.value === "") { setOpen(true) + setIsInitialOpen(true) + } + if (!hasInteracted) { + setHasInteracted(true) } }, - [filteredTopics, handleSelect] + [filteredTopics, handleSelect, hasInteracted] ) const handleInputChange = useCallback( (value: string) => { setInputValue(value) setOpen(true) + setIsInitialOpen(false) onInputChange(value) + if (!hasInteracted) { + setHasInteracted(true) + } }, - [onInputChange] + [onInputChange, hasInteracted] ) + const commandKey = useMemo(() => { + return filteredTopics + .map(topic => `${topic.name}:${topic.prettyName}:${topic.connectedTopics.join(",")}`) + .join("__") + }, [filteredTopics]) + + useEffect(() => { + if (inputRef.current && isMounted() && hasInteracted) { + inputRef.current.focus() + } + }, [commandKey, isMounted, hasInteracted]) + + const animationProps = { + initial: { opacity: 0, y: -10 }, + animate: { opacity: 1, y: 0 }, + exit: { opacity: 0, y: -10 }, + transition: { duration: 0.1 } + } + return ( -
+
setTimeout(() => setOpen(false), 100)} - onFocus={() => setOpen(true)} + onBlur={() => { + setTimeout(() => { + setOpen(false) + setIsInitialOpen(true) + }, 100) + }} + onFocus={() => { + setOpen(true) + setIsInitialOpen(true) + if (!hasInteracted) { + setHasInteracted(true) + } + }} placeholder="Search for a topic..." - className={cn("placeholder:text-muted-foreground flex-1 bg-transparent px-2 py-1 outline-none", { - "mb-1 border-b pb-2.5": open - })} + className={cn("placeholder:text-muted-foreground flex-1 bg-transparent px-2 outline-none")} />
{open && ( - - - {filteredTopics.map(topic => ( + + + {filteredTopics.map((topic, index) => ( handleSelect(topic)} className="min-h-10 rounded-none px-3 py-1.5" > {topic.prettyName} - {topic.connectedTopics.length > 0 ? topic.connectedTopics.join(", ") : "-"} + {topic.connectedTopics.length > 0 && topic.connectedTopics.join(", ")} ))}