diff --git a/client/components/BillMerchantRules.jsx b/client/components/BillMerchantRules.jsx index 0269015..3561cb5 100644 --- a/client/components/BillMerchantRules.jsx +++ b/client/components/BillMerchantRules.jsx @@ -8,6 +8,7 @@ import { import { api } from '@/api'; import { cn } from '@/lib/utils'; import { Button } from '@/components/ui/button'; +import * as Portal from '@radix-ui/react-portal'; import { Input } from '@/components/ui/input'; // Debounce helper @@ -89,7 +90,6 @@ export default function BillMerchantRules({ billId, onRulesChanged }) { const [conflicts, setConflicts] = useState([]); const [retroFeedback, setRetroFeedback] = useState(null); const inputRef = useRef(null); - const dropdownRef = useRef(null); const debouncedInput = useDebounce(input.trim(), 380); @@ -132,16 +132,7 @@ export default function BillMerchantRules({ billId, onRulesChanged }) { return () => { cancelled = true; }; }, [debouncedInput, billId]); - // Close suggestion dropdown on outside click - useEffect(() => { - function handler(e) { - if (!dropdownRef.current?.contains(e.target) && !inputRef.current?.contains(e.target)) { - setShowSuggestions(false); - } - } - document.addEventListener('mousedown', handler); - return () => document.removeEventListener('mousedown', handler); - }, []); + // Popover handles its own outside-click dismissal — no manual handler needed async function handleAdd(merchantText) { const text = (merchantText || input).trim(); @@ -265,43 +256,52 @@ export default function BillMerchantRules({ billId, onRulesChanged }) { + {/* Suggestions — rendered in a Portal so the list escapes the BillModal's + overflow-y-auto container and is fully clickable regardless of scroll */} + {showSuggestions && filteredSuggestions.length > 0 && inputRef.current && ( + +
+

+ Recent unmatched transactions +

+
+ {filteredSuggestions.map(s => { + const amountVal = Math.abs(Number(s.amount || 0)) / 100; + return ( + + ); + })} +
+
+
+ )} + {/* Conflict warning */} {conflicts.length > 0 && input.trim().length >= 2 && (
)} - - {/* Suggestions dropdown */} - {showSuggestions && filteredSuggestions.length > 0 && ( -
-

- Recent unmatched transactions -

-
- {filteredSuggestions.map(s => { - const amountVal = Math.abs(Number(s.amount || 0)) / 100; - return ( - - ); - })} -
-
- )} {/* Empty state */}