diff --git a/apps/web/src/components/comments/CommentInput.tsx b/apps/web/src/components/comments/CommentInput.tsx index 04dde45..c704aeb 100644 --- a/apps/web/src/components/comments/CommentInput.tsx +++ b/apps/web/src/components/comments/CommentInput.tsx @@ -1,7 +1,9 @@ "use client"; +import { createPortal } from "react-dom"; import { useCallback, useEffect, useRef, useState } from "react"; import { trpc } from "~/lib/trpc/client.js"; +import { useAnchoredOverlay } from "~/hooks/useAnchoredOverlay.js"; interface CommentInputProps { entityType: "estimate"; @@ -55,6 +57,13 @@ export function CommentInput({ ); }).slice(0, 8) : []; + const mentionOpen = mentionQuery !== null && filteredUsers.length > 0; + const { panelRef, position } = useAnchoredOverlay({ + open: mentionOpen, + onClose: () => setMentionQuery(null), + align: "start", + triggerRef: textareaRef, + }); // Reset mention index when filtered list changes useEffect(() => { @@ -177,33 +186,43 @@ export function CommentInput({ /> {/* Mention autocomplete dropdown */} - {mentionQuery !== null && filteredUsers.length > 0 && ( -
- {filteredUsers.map((user, idx) => ( - - ))} -
- )} + {filteredUsers.map((user, idx) => ( + + ))} + , + document.body, + ) + : null}
diff --git a/apps/web/src/components/ui/AutocompleteInput.tsx b/apps/web/src/components/ui/AutocompleteInput.tsx index 840f017..d468a53 100644 --- a/apps/web/src/components/ui/AutocompleteInput.tsx +++ b/apps/web/src/components/ui/AutocompleteInput.tsx @@ -1,6 +1,8 @@ "use client"; -import { useEffect, useRef, useState } from "react"; +import { createPortal } from "react-dom"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { useAnchoredOverlay } from "~/hooks/useAnchoredOverlay.js"; export interface AutocompleteOption { id: string; @@ -36,6 +38,17 @@ export function AutocompleteInput({ options, value, onChange, placeholder = "Sea const [activeIdx, setActiveIdx] = useState(0); const containerRef = useRef(null); const inputRef = useRef(null); + const closeDropdown = useCallback(() => { + setOpen(false); + setInput(selected?.label ?? ""); + }, [selected?.label]); + const { panelRef, position } = useAnchoredOverlay({ + open, + onClose: closeDropdown, + align: "start", + matchTriggerWidth: true, + triggerRef: containerRef, + }); // Keep input text in sync when selected value changes externally useEffect(() => { @@ -80,19 +93,6 @@ export function AutocompleteInput({ options, value, onChange, placeholder = "Sea } } - // Close on outside click - useEffect(() => { - if (!open) return; - function handleClick(e: MouseEvent) { - if (containerRef.current && !containerRef.current.contains(e.target as Node)) { - setOpen(false); - setInput(selected?.label ?? ""); - } - } - document.addEventListener("mousedown", handleClick); - return () => document.removeEventListener("mousedown", handleClick); - }, [open, selected?.label]); - return (
@@ -124,29 +124,40 @@ export function AutocompleteInput({ options, value, onChange, placeholder = "Sea )}
- {open && filtered.length > 0 && ( -
    e.preventDefault()} - > - {filtered.map((opt, idx) => ( -
  • - -
  • - ))} -
- )} + {open && filtered.length > 0 && typeof document !== "undefined" + ? createPortal( +
e.preventDefault()} + > +
    + {filtered.map((opt, idx) => ( +
  • + +
  • + ))} +
+
, + document.body, + ) + : null}
); }