Skip to content

Commit 43b354c

Browse files
authored
Merge pull request #4182 from ava-labs/fix/mobile-nav-burger-and-stats-chat
2 parents 66ad965 + 2186731 commit 43b354c

2 files changed

Lines changed: 85 additions & 45 deletions

File tree

components/chat/chat-bubble.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,12 @@ export function ChatBubble() {
125125
return null;
126126
}
127127

128+
// On /stats and /explorer, hide the bubble on mobile via CSS — those pages
129+
// are dense (charts, tables, search results) and the floating button steals
130+
// tap targets. Desktop still shows it.
131+
const hideOnMobile =
132+
pathname.startsWith('/stats') || pathname.startsWith('/explorer');
133+
128134
const handleBubbleClick = () => {
129135
if (state === 'collapsed') {
130136
setShowPrompt(false);
@@ -167,7 +173,13 @@ export function ChatBubble() {
167173
};
168174

169175
return (
170-
<div className="chatbot-container fixed bottom-6 right-6 z-50 flex flex-col items-end gap-3" data-chatbot>
176+
<div
177+
className={cn(
178+
'chatbot-container fixed bottom-6 right-6 z-50 flex-col items-end gap-3',
179+
hideOnMobile ? 'hidden md:flex' : 'flex',
180+
)}
181+
data-chatbot
182+
>
171183
{/* Prompt tooltip */}
172184
{mounted && (
173185
<div

components/navigation/navbar-dropdown-injector.tsx

Lines changed: 72 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,92 @@
11
'use client';
22

3-
import { useEffect } from 'react';
4-
import { createRoot } from 'react-dom/client';
3+
import { useEffect, useState } from 'react';
4+
import { createPortal } from 'react-dom';
55
import { NavbarDropdown } from './navbar-dropdown';
66

77
/**
8-
* Injects custom navbar dropdown at ≤1023px breakpoint
9-
* Replaces fumadocs' dropdown which has CSS specificity issues with Tailwind v4
8+
* Mounts the custom navbar dropdown at ≤1023px breakpoint.
9+
*
10+
* Renders via createPortal (not createRoot) so the portal stays inside the
11+
* parent React tree and inherits providers like SessionProvider. A previous
12+
* createRoot-based version broke when NavbarDropdown started calling
13+
* useSession() — the detached root had no provider and the component crashed,
14+
* leaving an empty <li> with no burger button.
1015
*/
1116
export function NavbarDropdownInjector() {
17+
const [container, setContainer] = useState<HTMLLIElement | null>(null);
18+
const [isMobile, setIsMobile] = useState(false);
19+
1220
useEffect(() => {
13-
const checkAndInject = () => {
14-
const isMobile = window.innerWidth <= 1023;
15-
16-
if (!isMobile) {
17-
// Remove if exists
18-
const existing = document.querySelector('[data-custom-navbar-dropdown]');
19-
if (existing) {
20-
existing.remove();
21-
}
22-
return;
23-
}
21+
const update = () => setIsMobile(window.innerWidth <= 1023);
22+
update();
23+
window.addEventListener('resize', update);
24+
return () => window.removeEventListener('resize', update);
25+
}, []);
2426

25-
const navbar = document.querySelector('nav[aria-label="Main"]') || document.querySelector('header[aria-label="Main"]');
26-
if (!navbar) return;
27+
useEffect(() => {
28+
if (!isMobile) {
29+
document
30+
.querySelectorAll('[data-custom-navbar-dropdown]')
31+
.forEach((el) => el.remove());
32+
setContainer(null);
33+
return;
34+
}
2735

28-
// Check if already exists
29-
if (navbar.querySelector('[data-custom-navbar-dropdown]')) {
30-
return;
31-
}
36+
let injected: HTMLLIElement | null = null;
37+
let cancelled = false;
3238

33-
// Find the right side container (where search icon is)
34-
const rightContainer = navbar.querySelector('ul.flex.flex-row.items-center.ms-auto') ||
35-
navbar.querySelector('ul.ms-auto');
36-
if (!rightContainer) return;
39+
const tryInject = (): boolean => {
40+
if (cancelled) return false;
3741

38-
// Create container
39-
const container = document.createElement('li');
40-
container.setAttribute('data-custom-navbar-dropdown', 'true');
41-
container.className = 'list-none';
42-
43-
// Append at the end (after search icon)
44-
rightContainer.appendChild(container);
42+
const navbar =
43+
document.querySelector('nav[aria-label="Main"]') ||
44+
document.querySelector('header[aria-label="Main"]');
45+
if (!navbar) return false;
4546

46-
// Render React component
47-
const root = createRoot(container);
48-
root.render(<NavbarDropdown />);
47+
const existing = navbar.querySelector<HTMLLIElement>(
48+
'[data-custom-navbar-dropdown]',
49+
);
50+
if (existing) {
51+
injected = existing;
52+
setContainer(existing);
53+
return true;
54+
}
55+
56+
const rightContainer =
57+
navbar.querySelector('ul.flex.flex-row.items-center.ms-auto') ||
58+
navbar.querySelector('ul.ms-auto');
59+
if (!rightContainer) return false;
60+
61+
const li = document.createElement('li');
62+
li.setAttribute('data-custom-navbar-dropdown', 'true');
63+
li.className = 'list-none';
64+
rightContainer.appendChild(li);
65+
injected = li;
66+
setContainer(li);
67+
return true;
4968
};
5069

51-
checkAndInject();
52-
const resizeHandler = () => checkAndInject();
53-
window.addEventListener('resize', resizeHandler);
54-
const timeout = setTimeout(checkAndInject, 100);
70+
if (tryInject()) {
71+
return () => {
72+
cancelled = true;
73+
if (injected?.parentNode) injected.parentNode.removeChild(injected);
74+
};
75+
}
76+
77+
// Navbar wasn't ready yet — watch for it to appear / re-render.
78+
const observer = new MutationObserver(() => {
79+
if (tryInject()) observer.disconnect();
80+
});
81+
observer.observe(document.body, { childList: true, subtree: true });
5582

5683
return () => {
57-
window.removeEventListener('resize', resizeHandler);
58-
clearTimeout(timeout);
84+
cancelled = true;
85+
observer.disconnect();
86+
if (injected?.parentNode) injected.parentNode.removeChild(injected);
5987
};
60-
}, []);
88+
}, [isMobile]);
6189

62-
return null;
90+
if (!container) return null;
91+
return createPortal(<NavbarDropdown />, container);
6392
}
64-

0 commit comments

Comments
 (0)