diff --git a/apps/web/src/components/comments/CommentInput.tsx b/apps/web/src/components/comments/CommentInput.tsx index 3b1135b..04dde45 100644 --- a/apps/web/src/components/comments/CommentInput.tsx +++ b/apps/web/src/components/comments/CommentInput.tsx @@ -4,7 +4,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { trpc } from "~/lib/trpc/client.js"; interface CommentInputProps { - entityType: string; + entityType: "estimate"; entityId: string; parentId?: string; onSubmit: (body: string) => void; diff --git a/apps/web/src/components/comments/CommentThread.tsx b/apps/web/src/components/comments/CommentThread.tsx index f03bb0d..ff2fe5f 100644 --- a/apps/web/src/components/comments/CommentThread.tsx +++ b/apps/web/src/components/comments/CommentThread.tsx @@ -31,8 +31,10 @@ interface CommentItem { replies: CommentReply[]; } +type CommentEntityType = "estimate"; + interface CommentThreadProps { - entityType: string; + entityType: CommentEntityType; entityId: string; } @@ -116,7 +118,7 @@ function SingleComment({ isReply = false, }: { comment: CommentItem | CommentReply; - entityType: string; + entityType: CommentEntityType; entityId: string; isReply?: boolean; }) { diff --git a/apps/web/src/components/ui/EntityCombobox.tsx b/apps/web/src/components/ui/EntityCombobox.tsx index cc76a0d..5dccf8f 100644 --- a/apps/web/src/components/ui/EntityCombobox.tsx +++ b/apps/web/src/components/ui/EntityCombobox.tsx @@ -1,7 +1,9 @@ "use client"; -import { useState, useRef, useEffect, useMemo, type ReactNode } from "react"; +import { createPortal } from "react-dom"; +import { useState, useRef, useMemo, useCallback, type ReactNode } from "react"; import { useDebounce } from "~/hooks/useDebounce.js"; +import { useAnchoredOverlay } from "~/hooks/useAnchoredOverlay.js"; interface EntityComboboxProps { value: string | null; @@ -35,11 +37,22 @@ export function EntityCombobox({ const debouncedSearch = useDebounce(search, 300); const containerRef = useRef(null); const inputRef = useRef(null); + const closeDropdown = useCallback(() => { + setOpen(false); + setSearch(""); + }, []); const { data: searchItems } = useSearchQuery(debouncedSearch, open); const items = searchItems ?? []; const { data: selectedItems } = useSelectedQuery(value, !!value && !open); + const { panelRef, position } = useAnchoredOverlay({ + open, + onClose: closeDropdown, + align: "start", + matchTriggerWidth: true, + triggerRef: containerRef, + }); const selectedLabel = useMemo(() => { if (!value) return ""; @@ -50,18 +63,6 @@ export function EntityCombobox({ return value; }, [value, items, selectedItems, getLabel]); - useEffect(() => { - if (!open) return; - function handleClick(e: MouseEvent) { - if (containerRef.current && !containerRef.current.contains(e.target as Node)) { - setOpen(false); - setSearch(""); - } - } - document.addEventListener("mousedown", handleClick); - return () => document.removeEventListener("mousedown", handleClick); - }, [open]); - function handleFocus() { if (disabled) return; setOpen(true); @@ -107,29 +108,42 @@ export function EntityCombobox({ {open && ( -
-
    - {items.length === 0 ? ( -
  • No results
  • - ) : ( - items.map((item) => ( -
  • - -
  • - )) - )} -
-
+ typeof document !== "undefined" + ? createPortal( +
+
    + {items.length === 0 ? ( +
  • No results
  • + ) : ( + items.map((item) => ( +
  • + +
  • + )) + )} +
+
, + document.body, + ) + : null )} );