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:
parent
056024d753
commit
7786d02f86
2 changed files with 52 additions and 7 deletions
|
|
@ -123,15 +123,20 @@ export default function App() {
|
||||||
<p>Plugins werden im Server konfiguriert.</p>
|
<p>Plugins werden im Server konfiguriert.</p>
|
||||||
</div>
|
</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 => {
|
plugins.map(p => {
|
||||||
const Comp = tabComponents[p.name];
|
const Comp = tabComponents[p.name];
|
||||||
if (!Comp) return null;
|
if (!Comp) return null;
|
||||||
|
const isActive = activeTab === p.name;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={p.name}
|
key={p.name}
|
||||||
className="hub-tab-panel"
|
className={`hub-tab-panel ${isActive ? 'active' : ''}`}
|
||||||
style={{ display: activeTab === p.name ? 'contents' : 'none' }}
|
style={isActive
|
||||||
|
? { display: 'flex', flexDirection: 'column', width: '100%', height: '100%' }
|
||||||
|
: { display: 'none' }
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Comp data={pluginData[p.name] || {}} />
|
<Comp data={pluginData[p.name] || {}} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,7 @@ export default function RadioTab({ data }: { data: any }) {
|
||||||
const [volume, setVolume] = useState(0.5);
|
const [volume, setVolume] = useState(0.5);
|
||||||
const [voiceStats, setVoiceStats] = useState<VoiceStats | null>(null);
|
const [voiceStats, setVoiceStats] = useState<VoiceStats | null>(null);
|
||||||
const [showConnModal, setShowConnModal] = useState(false);
|
const [showConnModal, setShowConnModal] = useState(false);
|
||||||
|
const [containerVisible, setContainerVisible] = useState(false);
|
||||||
const searchTimeout = useRef<ReturnType<typeof setTimeout>>(undefined);
|
const searchTimeout = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||||
const volumeTimeout = useRef<ReturnType<typeof setTimeout>>(undefined);
|
const volumeTimeout = useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||||
const selectedGuildRef = useRef(selectedGuild);
|
const selectedGuildRef = useRef(selectedGuild);
|
||||||
|
|
@ -211,15 +212,47 @@ export default function RadioTab({ data }: { data: any }) {
|
||||||
.catch(() => setStationsLoading(false));
|
.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 ──
|
// ── Initialize globe ──
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!containerRef.current || places.length === 0) return;
|
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) {
|
if (globeRef.current) {
|
||||||
globeRef.current.pointsData(places);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't initialize globe with zero dimensions — containerVisible will re-trigger
|
||||||
|
if (cw === 0 || ch === 0) return;
|
||||||
|
|
||||||
// Read accent color from theme
|
// Read accent color from theme
|
||||||
const initStyle = getComputedStyle(containerRef.current.parentElement!);
|
const initStyle = getComputedStyle(containerRef.current.parentElement!);
|
||||||
const initRgb = initStyle.getPropertyValue('--accent-rgb').trim() || '230, 126, 34';
|
const initRgb = initStyle.getPropertyValue('--accent-rgb').trim() || '230, 126, 34';
|
||||||
|
|
@ -286,21 +319,28 @@ export default function RadioTab({ data }: { data: any }) {
|
||||||
|
|
||||||
const onResize = () => {
|
const onResize = () => {
|
||||||
if (containerRef.current && globeRef.current) {
|
if (containerRef.current && globeRef.current) {
|
||||||
globeRef.current
|
const w = containerRef.current.clientWidth;
|
||||||
.width(containerRef.current.clientWidth)
|
const h = containerRef.current.clientHeight;
|
||||||
.height(containerRef.current.clientHeight);
|
if (w > 0 && h > 0) {
|
||||||
|
globeRef.current.width(w).height(h);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener('resize', onResize);
|
window.addEventListener('resize', onResize);
|
||||||
|
|
||||||
|
// ResizeObserver: detects when tab becomes visible (0×0 → real size)
|
||||||
|
const resizeObserver = new ResizeObserver(() => onResize());
|
||||||
|
resizeObserver.observe(el);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
controls.removeEventListener('change', onControlsChange);
|
controls.removeEventListener('change', onControlsChange);
|
||||||
el.removeEventListener('mousedown', onInteract);
|
el.removeEventListener('mousedown', onInteract);
|
||||||
el.removeEventListener('touchstart', onInteract);
|
el.removeEventListener('touchstart', onInteract);
|
||||||
el.removeEventListener('wheel', onInteract);
|
el.removeEventListener('wheel', onInteract);
|
||||||
window.removeEventListener('resize', onResize);
|
window.removeEventListener('resize', onResize);
|
||||||
|
resizeObserver.disconnect();
|
||||||
};
|
};
|
||||||
}, [places, pauseRotation]);
|
}, [places, pauseRotation, containerVisible]);
|
||||||
|
|
||||||
// ── Play handler ──
|
// ── Play handler ──
|
||||||
const handlePlay = useCallback(async (
|
const handlePlay = useCallback(async (
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue