fix: globe clickable after tab switch — deferred init with ResizeObserver

Globe.gl needs non-zero container dimensions for initialization and click
handling. With the tab persistence fix (display:none for hidden tabs), the
globe container starts at 0×0 when radio isn't the first tab. Added a
separate ResizeObserver that detects when the container becomes visible
and triggers globe initialization via containerVisible state dependency.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Daniel 2026-03-06 23:17:48 +01:00
parent 056024d753
commit 7786d02f86
2 changed files with 52 additions and 7 deletions

View file

@ -123,15 +123,20 @@ export default function App() {
<p>Plugins werden im Server konfiguriert.</p>
</div>
) : (
/* Render ALL tabs, hide inactive ones with CSS to preserve state */
/* Render ALL tabs, hide inactive ones to preserve state.
Active tab gets full dimensions; hidden tabs stay in DOM but invisible. */
plugins.map(p => {
const Comp = tabComponents[p.name];
if (!Comp) return null;
const isActive = activeTab === p.name;
return (
<div
key={p.name}
className="hub-tab-panel"
style={{ display: activeTab === p.name ? 'contents' : 'none' }}
className={`hub-tab-panel ${isActive ? 'active' : ''}`}
style={isActive
? { display: 'flex', flexDirection: 'column', width: '100%', height: '100%' }
: { display: 'none' }
}
>
<Comp data={pluginData[p.name] || {}} />
</div>

View file

@ -89,6 +89,7 @@ export default function RadioTab({ data }: { data: any }) {
const [volume, setVolume] = useState(0.5);
const [voiceStats, setVoiceStats] = useState<VoiceStats | null>(null);
const [showConnModal, setShowConnModal] = useState(false);
const [containerVisible, setContainerVisible] = useState(false);
const searchTimeout = useRef<ReturnType<typeof setTimeout>>(undefined);
const volumeTimeout = useRef<ReturnType<typeof setTimeout>>(undefined);
const selectedGuildRef = useRef(selectedGuild);
@ -211,15 +212,47 @@ export default function RadioTab({ data }: { data: any }) {
.catch(() => setStationsLoading(false));
};
// ── Watch container visibility (detects tab becoming active) ──
useEffect(() => {
const el = containerRef.current;
if (!el) return;
// Check immediately
if (el.clientWidth > 0 && el.clientHeight > 0) {
setContainerVisible(true);
}
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
if (width > 0 && height > 0) {
setContainerVisible(true);
}
}
});
observer.observe(el);
return () => observer.disconnect();
}, []);
// ── Initialize globe ──
useEffect(() => {
if (!containerRef.current || places.length === 0) return;
// If container is hidden (display:none), wait for it to become visible
const cw = containerRef.current.clientWidth;
const ch = containerRef.current.clientHeight;
if (globeRef.current) {
globeRef.current.pointsData(places);
// Re-apply dimensions in case we were hidden during init
if (cw > 0 && ch > 0) globeRef.current.width(cw).height(ch);
return;
}
// Don't initialize globe with zero dimensions — containerVisible will re-trigger
if (cw === 0 || ch === 0) return;
// Read accent color from theme
const initStyle = getComputedStyle(containerRef.current.parentElement!);
const initRgb = initStyle.getPropertyValue('--accent-rgb').trim() || '230, 126, 34';
@ -286,21 +319,28 @@ export default function RadioTab({ data }: { data: any }) {
const onResize = () => {
if (containerRef.current && globeRef.current) {
globeRef.current
.width(containerRef.current.clientWidth)
.height(containerRef.current.clientHeight);
const w = containerRef.current.clientWidth;
const h = containerRef.current.clientHeight;
if (w > 0 && h > 0) {
globeRef.current.width(w).height(h);
}
}
};
window.addEventListener('resize', onResize);
// ResizeObserver: detects when tab becomes visible (0×0 → real size)
const resizeObserver = new ResizeObserver(() => onResize());
resizeObserver.observe(el);
return () => {
controls.removeEventListener('change', onControlsChange);
el.removeEventListener('mousedown', onInteract);
el.removeEventListener('touchstart', onInteract);
el.removeEventListener('wheel', onInteract);
window.removeEventListener('resize', onResize);
resizeObserver.disconnect();
};
}, [places, pauseRotation]);
}, [places, pauseRotation, containerVisible]);
// ── Play handler ──
const handlePlay = useCallback(async (