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
+
- Recent unmatched transactions -
-