diff --git a/apps/web/src/components/ui/SkillTagInput.tsx b/apps/web/src/components/ui/SkillTagInput.tsx index 2d9cf3c..026d337 100644 --- a/apps/web/src/components/ui/SkillTagInput.tsx +++ b/apps/web/src/components/ui/SkillTagInput.tsx @@ -1,7 +1,9 @@ "use client"; -import { useEffect, useRef, useState } from "react"; +import { createPortal } from "react-dom"; +import { useCallback, useRef, useState } from "react"; import { trpc } from "~/lib/trpc/client.js"; +import { useAnchoredOverlay } from "~/hooks/useAnchoredOverlay.js"; interface SkillTagInputProps { value: string[]; @@ -16,6 +18,10 @@ export function SkillTagInput({ value, onChange, placeholder = "Add skill…", c const [activeIdx, setActiveIdx] = useState(-1); const containerRef = useRef(null); const inputRef = useRef(null); + const closeSuggestions = useCallback(() => { + setOpen(false); + setActiveIdx(-1); + }, []); const { data: analytics } = trpc.resource.getSkillsAnalytics.useQuery(undefined, { staleTime: 120_000, @@ -26,6 +32,14 @@ export function SkillTagInput({ value, onChange, placeholder = "Add skill…", c .map((a) => a.skill) .filter((s) => !value.includes(s) && s.toLowerCase().includes(input.toLowerCase())) .slice(0, 8); + const dropdownOpen = open && suggestions.length > 0; + const { panelRef, position } = useAnchoredOverlay({ + open: dropdownOpen, + onClose: closeSuggestions, + align: "start", + matchTriggerWidth: true, + triggerRef: containerRef, + }); function addSkill(skill: string) { const trimmed = skill.trim(); @@ -57,25 +71,12 @@ export function SkillTagInput({ value, onChange, placeholder = "Add skill…", c e.preventDefault(); setActiveIdx((i) => Math.max(i - 1, -1)); } else if (e.key === "Escape") { - setOpen(false); - setActiveIdx(-1); + closeSuggestions(); } else if (e.key === "Backspace" && input === "" && value.length > 0) { onChange(value.slice(0, -1)); } } - // Close on outside click - useEffect(() => { - if (!open) return; - function handleClick(e: MouseEvent) { - if (containerRef.current && !containerRef.current.contains(e.target as Node)) { - setOpen(false); - } - } - document.addEventListener("mousedown", handleClick); - return () => document.removeEventListener("mousedown", handleClick); - }, [open]); - return (
{/* Tags + input row */} @@ -112,28 +113,39 @@ export function SkillTagInput({ value, onChange, placeholder = "Add skill…", c
{/* Dropdown */} - {open && suggestions.length > 0 && ( -
    e.preventDefault()} - > - {suggestions.map((skill, idx) => ( -
  • - -
  • - ))} -
- )} + {dropdownOpen && typeof document !== "undefined" + ? createPortal( +
e.preventDefault()} + > +
    + {suggestions.map((skill, idx) => ( +
  • + +
  • + ))} +
+
, + document.body, + ) + : null} ); }