From d4b839f888bd1c674e5cbbfe895b3ac78b775c6d Mon Sep 17 00:00:00 2001 From: vibe-bot Date: Fri, 8 Aug 2025 18:22:37 +0200 Subject: [PATCH] =?UTF-8?q?refactor(select):=20Dropdown=20via=20React=20Po?= =?UTF-8?q?rtal=20(fixed=20overlay),=20positioniert=20relativ=20zum=20Trig?= =?UTF-8?q?ger;=20verhindert=20=C3=9Cberlagerungen=20zuverl=C3=A4ssig?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/App.tsx | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/web/src/App.tsx b/web/src/App.tsx index 53305e1..4c6944c 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; +import ReactDOM from 'react-dom'; import { fetchChannels, fetchSounds, playSound, setVolumeLive, getVolume, adminStatus, adminLogin, adminLogout, adminDelete, adminRename, playUrl } from './api'; import type { VoiceChannelInfo, Sound } from './types'; import { getCookie, setCookie } from './cookies'; @@ -336,22 +337,44 @@ type SelectProps = { function CustomSelect({ channels, value, onChange }: SelectProps) { const [open, setOpen] = useState(false); const ref = useRef(null); + const triggerRef = useRef(null); + const [menuPos, setMenuPos] = useState<{ left: number; top: number; width: number }>({ left: 0, top: 0, width: 0 }); useEffect(() => { const close = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); }; window.addEventListener('click', close); return () => window.removeEventListener('click', close); }, []); + useEffect(() => { + if (!open) return; + const update = () => { + const el = triggerRef.current; + if (!el) return; + const r = el.getBoundingClientRect(); + setMenuPos({ left: Math.round(r.left), top: Math.round(r.bottom + 6), width: Math.round(r.width) }); + }; + update(); + window.addEventListener('resize', update); + window.addEventListener('scroll', update, true); + return () => { + window.removeEventListener('resize', update); + window.removeEventListener('scroll', update, true); + }; + }, [open]); + const current = channels.find(c => `${c.guildId}:${c.channelId}` === value); return (
- - {open && ( -
+ {open && typeof document !== 'undefined' && ReactDOM.createPortal( +
{channels.map((c) => { const v = `${c.guildId}:${c.channelId}`; const active = v === value; @@ -366,7 +389,8 @@ function CustomSelect({ channels, value, onChange }: SelectProps) { ); })} -
+
, + document.body )}
);