Dropdown menus paint under later glass panels - portal them out
The Signals > Setups filter dropdowns (Direction, Recommended Action) opened UNDER the panels below: glass containers use backdrop-filter, which creates a stacking context, so the menu's z-50 could never compete with later sibling panels regardless of value. The menu now portals to document.body with fixed positioning tracked against the trigger (re-measured on scroll/resize), and click-outside accounts for the portaled node. Applies to every Dropdown usage. Menu background also updated from the old green-tinted #151911 to the Horizon #14161f. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
export interface DropdownOption {
|
||||
value: string;
|
||||
@@ -17,8 +18,12 @@ interface DropdownProps {
|
||||
|
||||
/**
|
||||
* Fully dark, custom dropdown — replaces native <select>, whose popup list
|
||||
* can't be reliably styled (white background on Windows). Button + absolutely
|
||||
* positioned menu, click-outside and Escape to close.
|
||||
* can't be reliably styled (white background on Windows).
|
||||
*
|
||||
* The menu is PORTALED to document.body with fixed positioning: glass panels
|
||||
* use backdrop-filter, which creates a stacking context, so an absolutely
|
||||
* positioned menu inside one would paint UNDER any later sibling panel no
|
||||
* matter its z-index.
|
||||
*/
|
||||
export function Dropdown({
|
||||
value,
|
||||
@@ -30,14 +35,34 @@ export function Dropdown({
|
||||
placeholder = 'Select…',
|
||||
}: DropdownProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [rect, setRect] = useState<{ top: number; left: number; width: number } | null>(null);
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const menuRef = useRef<HTMLUListElement>(null);
|
||||
|
||||
const selected = options.find((o) => o.value === value);
|
||||
|
||||
// Track the button's viewport position while open (scroll/resize).
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const measure = () => {
|
||||
const r = rootRef.current?.getBoundingClientRect();
|
||||
if (r) setRect({ top: r.bottom + 4, left: r.left, width: r.width });
|
||||
};
|
||||
measure();
|
||||
window.addEventListener('scroll', measure, true);
|
||||
window.addEventListener('resize', measure);
|
||||
return () => {
|
||||
window.removeEventListener('scroll', measure, true);
|
||||
window.removeEventListener('resize', measure);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const onDocClick = (e: MouseEvent) => {
|
||||
if (rootRef.current && !rootRef.current.contains(e.target as Node)) setOpen(false);
|
||||
const t = e.target as Node;
|
||||
if (rootRef.current?.contains(t) || menuRef.current?.contains(t)) return;
|
||||
setOpen(false);
|
||||
};
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') setOpen(false);
|
||||
@@ -74,10 +99,12 @@ export function Dropdown({
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
{open && rect && createPortal(
|
||||
<ul
|
||||
ref={menuRef}
|
||||
role="listbox"
|
||||
className="absolute z-50 mt-1 max-h-64 w-full overflow-auto rounded-lg border border-white/[0.1] bg-[#151911] p-1 shadow-2xl shadow-black/50"
|
||||
style={{ position: 'fixed', top: rect.top, left: rect.left, width: rect.width }}
|
||||
className="z-50 max-h-64 overflow-auto rounded-lg border border-white/[0.1] bg-[#14161f] p-1 shadow-2xl shadow-black/50"
|
||||
>
|
||||
{options.map((opt) => {
|
||||
const isSelected = opt.value === value;
|
||||
@@ -109,7 +136,8 @@ export function Dropdown({
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</ul>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user