refactor(select): Dropdown via React Portal (fixed overlay), positioniert relativ zum Trigger; verhindert Überlagerungen zuverlässig

This commit is contained in:
vibe-bot 2025-08-08 18:22:37 +02:00
parent a6531aef9d
commit d4b839f888

View file

@ -1,4 +1,5 @@
import React, { useEffect, useMemo, useRef, useState } from 'react'; 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 { fetchChannels, fetchSounds, playSound, setVolumeLive, getVolume, adminStatus, adminLogin, adminLogout, adminDelete, adminRename, playUrl } from './api';
import type { VoiceChannelInfo, Sound } from './types'; import type { VoiceChannelInfo, Sound } from './types';
import { getCookie, setCookie } from './cookies'; import { getCookie, setCookie } from './cookies';
@ -336,22 +337,44 @@ type SelectProps = {
function CustomSelect({ channels, value, onChange }: SelectProps) { function CustomSelect({ channels, value, onChange }: SelectProps) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement | null>(null); const ref = useRef<HTMLDivElement | null>(null);
const triggerRef = useRef<HTMLButtonElement | null>(null);
const [menuPos, setMenuPos] = useState<{ left: number; top: number; width: number }>({ left: 0, top: 0, width: 0 });
useEffect(() => { useEffect(() => {
const close = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); }; const close = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false); };
window.addEventListener('click', close); window.addEventListener('click', close);
return () => window.removeEventListener('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); const current = channels.find(c => `${c.guildId}:${c.channelId}` === value);
return ( return (
<div className="control select custom-select" ref={ref}> <div className="control select custom-select" ref={ref}>
<button type="button" className="select-trigger" onClick={() => setOpen(v => !v)}> <button ref={triggerRef} type="button" className="select-trigger" onClick={() => setOpen(v => !v)}>
{current ? `${current.guildName} ${current.channelName}` : 'Channel wählen'} {current ? `${current.guildName} ${current.channelName}` : 'Channel wählen'}
<span className="chev"></span> <span className="chev"></span>
</button> </button>
{open && ( {open && typeof document !== 'undefined' && ReactDOM.createPortal(
<div className="select-menu"> <div
className="select-menu"
style={{ position: 'fixed', left: menuPos.left, top: menuPos.top, width: menuPos.width, zIndex: 30000 }}
>
{channels.map((c) => { {channels.map((c) => {
const v = `${c.guildId}:${c.channelId}`; const v = `${c.guildId}:${c.channelId}`;
const active = v === value; const active = v === value;
@ -366,7 +389,8 @@ function CustomSelect({ channels, value, onChange }: SelectProps) {
</button> </button>
); );
})} })}
</div> </div>,
document.body
)} )}
</div> </div>
); );