From b81da79cf2eeafe8ffe77dfb15447bac04f2861e Mon Sep 17 00:00:00 2001 From: Decobus Date: Sun, 20 Jul 2025 01:59:45 -0400 Subject: [PATCH] Implement React Portal for dropdown to escape stacking contexts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete rewrite of dropdown positioning using React Portal: - Renders dropdown in document.body via createPortal - Uses fixed positioning with calculated coordinates - Completely bypasses all CSS stacking contexts - Includes proper SSR handling with mounted state - Maintains click-outside detection for both button and menu This should definitively solve the dropdown layering issue by rendering the dropdown outside any parent containers with backdrop-filter. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- components/Dropdown.tsx | 120 ++++++++++++++++++++++++++-------------- 1 file changed, 78 insertions(+), 42 deletions(-) diff --git a/components/Dropdown.tsx b/components/Dropdown.tsx index a3bcc7f..7ef8199 100644 --- a/components/Dropdown.tsx +++ b/components/Dropdown.tsx @@ -1,6 +1,7 @@ 'use client'; import { useRef, useEffect, useState } from 'react'; +import { createPortal } from 'react-dom'; type DropdownProps = { options: Array<{ id: number; name: string }>; @@ -20,12 +21,19 @@ export default function Dropdown({ onToggle, }: DropdownProps) { const dropdownRef = useRef(null); + const buttonRef = useRef(null); const [isOpen, setIsOpen] = useState(controlledIsOpen ?? false); + const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0, width: 0 }); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { - if (!dropdownRef.current || !(event.target instanceof Node)) return; - if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { + if (!dropdownRef.current || !buttonRef.current || !(event.target instanceof Node)) return; + if (!dropdownRef.current.contains(event.target) && !buttonRef.current.contains(event.target)) { if (onToggle) onToggle(false); else setIsOpen(false); } @@ -40,6 +48,17 @@ export default function Dropdown({ }; }, [controlledIsOpen, isOpen, onToggle]); + useEffect(() => { + if ((controlledIsOpen ?? isOpen) && buttonRef.current && mounted) { + const rect = buttonRef.current.getBoundingClientRect(); + setDropdownPosition({ + top: rect.bottom + window.scrollY + 4, + left: rect.left + window.scrollX, + width: rect.width + }); + } + }, [controlledIsOpen, isOpen, mounted]); + const activeOption = options.find((option) => option.id === activeId) || null; const handleSelect = (option: { id: number }) => { @@ -53,48 +72,65 @@ export default function Dropdown({ else setIsOpen((prev) => !prev); }; - return ( -
- - - {(controlledIsOpen ?? isOpen) && ( -
- {options.length === 0 ? ( -
- No teams available -
- ) : ( - options.map((option) => ( -
handleSelect(option)} - className={`dropdown-item ${activeOption?.id === option.id ? 'active' : ''}`} - > - {option.name} -
- )) - )} + const dropdownMenu = (controlledIsOpen ?? isOpen) && mounted ? ( +
+ {options.length === 0 ? ( +
+ No teams available
+ ) : ( + options.map((option) => ( +
handleSelect(option)} + className={`dropdown-item ${activeOption?.id === option.id ? 'active' : ''}`} + > + {option.name} +
+ )) )}
+ ) : null; + + return ( + <> +
+ +
+ + {mounted && typeof document !== 'undefined' && dropdownMenu ? + createPortal(dropdownMenu, document.body) : null + } + ); } \ No newline at end of file