fix(web): portal remaining overlay menus

This commit is contained in:
2026-03-30 14:20:05 +02:00
parent ea2efabd7f
commit 27b0e38b93
3 changed files with 111 additions and 71 deletions
@@ -1,5 +1,6 @@
"use client";
import { createPortal } from "react-dom";
import { useState, useCallback, useEffect, useMemo, useRef } from "react";
import { clsx } from "clsx";
import type { StaffingRequirement } from "@capakraken/shared";
@@ -13,6 +14,7 @@ import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
import { formatCents } from "~/lib/format.js";
import { ConfettiBurst } from "~/components/ui/ConfettiBurst.js";
import { SuccessToast } from "~/components/ui/SuccessToast.js";
import { useAnchoredOverlay } from "~/hooks/useAnchoredOverlay.js";
// ─── Constants ────────────────────────────────────────────────────────────────
@@ -307,7 +309,7 @@ function ResourcePersonPicker({ value, onChange }: { value: string; onChange: (v
const [query, setQuery] = useState(value);
const [open, setOpen] = useState(false);
const [debouncedSearch, setDebouncedSearch] = useState("");
const containerRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
// Debounce search query to avoid excessive API calls
useEffect(() => {
@@ -331,21 +333,18 @@ function ResourcePersonPicker({ value, onChange }: { value: string; onChange: (v
setQuery(value);
}, [value]);
// 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]);
const { panelRef, position } = useAnchoredOverlay<HTMLInputElement>({
open,
onClose: () => setOpen(false),
align: "start",
matchTriggerWidth: true,
triggerRef: inputRef,
});
return (
<div ref={containerRef} className="relative">
<div className="relative">
<input
ref={inputRef}
type="text"
value={query}
onChange={(e) => {
@@ -357,29 +356,40 @@ function ResourcePersonPicker({ value, onChange }: { value: string; onChange: (v
placeholder="Search by name or EID…"
className={INPUT_CLS}
/>
{open && filtered.length > 0 && (
<ul
className="absolute z-50 left-0 right-0 mt-1 bg-white border border-gray-200 rounded-lg shadow-lg overflow-hidden max-h-48 overflow-y-auto"
onMouseDown={(e) => e.preventDefault()}
>
{filtered.map((r) => (
<li key={r.id}>
<button
type="button"
onMouseDown={() => {
onChange(r.displayName);
setQuery(r.displayName);
setOpen(false);
}}
className="w-full text-left px-3 py-2 text-sm flex items-baseline gap-2 hover:bg-gray-50 transition-colors"
>
<span className="truncate">{r.displayName}</span>
<span className="text-xs text-gray-400 font-mono shrink-0">{r.eid}</span>
</button>
</li>
))}
</ul>
)}
{open && filtered.length > 0 && typeof document !== "undefined"
? createPortal(
<div
ref={panelRef}
className="fixed z-[9998] max-h-48 overflow-y-auto rounded-lg border border-gray-200 bg-white shadow-lg"
style={{
top: position.top,
left: position.left,
width: position.minWidth,
}}
onMouseDown={(e) => e.preventDefault()}
>
<ul>
{filtered.map((r) => (
<li key={r.id}>
<button
type="button"
onMouseDown={() => {
onChange(r.displayName);
setQuery(r.displayName);
setOpen(false);
}}
className="flex w-full items-baseline gap-2 px-3 py-2 text-left text-sm transition-colors hover:bg-gray-50"
>
<span className="truncate">{r.displayName}</span>
<span className="shrink-0 font-mono text-xs text-gray-400">{r.eid}</span>
</button>
</li>
))}
</ul>
</div>,
document.body,
)
: null}
</div>
);
}