Skip to content

Commit b36bdff

Browse files
committed
fix nav throttle
1 parent a9d9ee4 commit b36bdff

2 files changed

Lines changed: 113 additions & 33 deletions

File tree

frontend/src/components/layout/Nav.jsx

Lines changed: 101 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ import {
2424
ChevronDown,
2525
} from 'lucide-react';
2626

27+
const NAV_REVEAL_TOP_OFFSET = 24;
28+
const NAV_HIDE_SCROLL_DISTANCE = 72;
29+
const NAV_SHOW_SCROLL_DISTANCE = 36;
30+
2731
const NavDropdown = memo(({ menuItem, activeDropdown, handleMouseEnter }) => {
2832
const isActive = activeDropdown === menuItem.key;
2933
const hasMega = Boolean(menuItem.mega);
@@ -148,11 +152,15 @@ const Nav = () => {
148152
const [isUserMenuOpen, setUserMenuOpen] = useState(false);
149153
const [activeDropdown, setActiveDropdown] = useState(null);
150154
const [openMobileSections, setOpenMobileSections] = useState(() => new Set());
155+
const scrollRafRef = useRef(null);
151156
const lastScrollYRef = useRef(0);
157+
const lastToggleYRef = useRef(0);
158+
const desktopSubNavVisibleRef = useRef(true);
159+
const scrollUiStateRef = useRef({ scrolled: false });
152160
const dispatch = useDispatch();
153161
const menuItems = useMemo(
154162
() => NAV_ORDER.map((key) => ({ key, ...NAVIGATION[key] })),
155-
[NAV_ORDER, NAVIGATION]
163+
[]
156164
);
157165
const activeMenu = useMemo(
158166
() => (activeDropdown ? NAVIGATION[activeDropdown] : null),
@@ -161,7 +169,7 @@ const Nav = () => {
161169
const activeDropdownData = activeMenu?.mega || null;
162170
const searchSuggestions = useMemo(
163171
() => SEARCH_SUGGESTIONS,
164-
[SEARCH_SUGGESTIONS]
172+
[]
165173
);
166174
const {
167175
query: searchQuery,
@@ -177,27 +185,84 @@ const Nav = () => {
177185
});
178186

179187
useEffect(() => {
180-
const handleScroll = () => {
188+
const updateFromScroll = () => {
181189
const currentScrollY = window.scrollY;
190+
const nextScrolled = currentScrollY > 10;
191+
192+
if (scrollUiStateRef.current.scrolled !== nextScrolled) {
193+
scrollUiStateRef.current.scrolled = nextScrolled;
194+
setScrolled(nextScrolled);
195+
}
196+
197+
const isDesktop = window.innerWidth >= 768;
198+
if (!isDesktop) {
199+
lastScrollYRef.current = currentScrollY;
200+
if (!desktopSubNavVisibleRef.current) {
201+
desktopSubNavVisibleRef.current = true;
202+
setShowDesktopSubNav(true);
203+
}
204+
return;
205+
}
206+
182207
const delta = currentScrollY - lastScrollYRef.current;
208+
lastScrollYRef.current = currentScrollY;
209+
if (Math.abs(delta) < 2) return;
183210

184-
setScrolled(currentScrollY > 10);
211+
if (currentScrollY <= NAV_REVEAL_TOP_OFFSET) {
212+
lastToggleYRef.current = currentScrollY;
213+
if (!desktopSubNavVisibleRef.current) {
214+
desktopSubNavVisibleRef.current = true;
215+
setShowDesktopSubNav(true);
216+
}
217+
return;
218+
}
185219

186-
if (currentScrollY < 24) {
187-
setShowDesktopSubNav(true);
188-
} else if (delta > 6) {
220+
if (
221+
delta > 0 &&
222+
desktopSubNavVisibleRef.current &&
223+
currentScrollY - lastToggleYRef.current >= NAV_HIDE_SCROLL_DISTANCE
224+
) {
225+
desktopSubNavVisibleRef.current = false;
226+
lastToggleYRef.current = currentScrollY;
189227
setShowDesktopSubNav(false);
190228
setActiveDropdown(null);
191-
} else if (delta < -6) {
229+
return;
230+
}
231+
232+
if (
233+
delta < 0 &&
234+
!desktopSubNavVisibleRef.current &&
235+
lastToggleYRef.current - currentScrollY >= NAV_SHOW_SCROLL_DISTANCE
236+
) {
237+
desktopSubNavVisibleRef.current = true;
238+
lastToggleYRef.current = currentScrollY;
192239
setShowDesktopSubNav(true);
193240
}
241+
};
194242

195-
lastScrollYRef.current = currentScrollY;
243+
const handleScroll = () => {
244+
if (scrollRafRef.current !== null) return;
245+
scrollRafRef.current = window.requestAnimationFrame(() => {
246+
scrollRafRef.current = null;
247+
updateFromScroll();
248+
});
196249
};
197250

198-
lastScrollYRef.current = window.scrollY;
251+
const initialScrollY = window.scrollY;
252+
lastScrollYRef.current = initialScrollY;
253+
lastToggleYRef.current = initialScrollY;
254+
scrollUiStateRef.current.scrolled = initialScrollY > 10;
255+
desktopSubNavVisibleRef.current = true;
256+
setScrolled(scrollUiStateRef.current.scrolled);
257+
setShowDesktopSubNav(true);
258+
199259
window.addEventListener('scroll', handleScroll, { passive: true });
200-
return () => window.removeEventListener('scroll', handleScroll);
260+
return () => {
261+
window.removeEventListener('scroll', handleScroll);
262+
if (scrollRafRef.current !== null) {
263+
window.cancelAnimationFrame(scrollRafRef.current);
264+
}
265+
};
201266
}, []);
202267

203268
useEffect(() => {
@@ -236,7 +301,7 @@ const Nav = () => {
236301
setActiveDropdown(null);
237302
}
238303
},
239-
[activeDropdown, NAVIGATION]
304+
[activeDropdown]
240305
);
241306

242307
const handleMouseLeave = useCallback(() => {
@@ -278,8 +343,9 @@ const Nav = () => {
278343
<>
279344
{/* Main Navigation */}
280345
<nav
281-
className={`sticky top-0 z-50 transition-all duration-300 ease-out relative
282-
${scrolled ? 'bg-stone-950/95 backdrop-blur-xl shadow-md shadow-black/20 border-b border-white/10' : 'bg-stone-950'} `}
346+
style={{ position: 'sticky' }}
347+
className={`relative top-0 z-[200] nav-shell backdrop-blur-xl transition-[background-color,border-color,box-shadow] duration-300 ease-out
348+
${scrolled ? 'bg-stone-950/95 shadow-md shadow-black/20 border-b border-white/10' : 'bg-stone-950/90 border-b border-transparent'} `}
283349
>
284350
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
285351
{/* Mobile Header */}
@@ -319,7 +385,7 @@ const Nav = () => {
319385
<User className="w-5 h-5" strokeWidth={1.5} />
320386
</button>
321387
{isUserMenuOpen && (
322-
<div className="absolute top-full right-0 mt-2 w-52 bg-stone-950 rounded-lg shadow-xl border border-gray-800 p-2 animate-in slide-in-from-top-2 z-2">
388+
<div className="absolute top-full right-0 mt-2 w-52 bg-stone-950 rounded-lg shadow-xl border border-gray-800 p-2 animate-in slide-in-from-top-2 z-[220] nav-transition-layer">
323389
<div className="px-3 py-2 border-b border-gray-700">
324390
<div className="text-sm font-medium text-white">{user.name || 'User'}</div>
325391
<div className="text-xs text-gray-400">
@@ -467,7 +533,7 @@ const Nav = () => {
467533
<User className="w-5 h-5" strokeWidth={1.5} />
468534
</button>
469535
{isUserMenuOpen && (
470-
<div className="absolute top-full right-0 mt-2 w-52 bg-stone-950 rounded-lg shadow-xl border border-gray-800 p-2 animate-in slide-in-from-top-2 z-2">
536+
<div className="absolute top-full right-0 mt-2 w-52 bg-stone-950 rounded-lg shadow-xl border border-gray-800 p-2 animate-in slide-in-from-top-2 z-[220] nav-transition-layer">
471537
<div className="px-3 py-2 border-b border-gray-700">
472538
<div className="text-sm font-medium text-white">{user.name || 'User'}</div>
473539
<div className="text-xs text-gray-400">
@@ -565,32 +631,34 @@ const Nav = () => {
565631

566632
{/* Desktop Navigation Menu */}
567633
<div
568-
className={`hidden md:flex items-center justify-center overflow-hidden transition-all duration-300 ease-out ${
634+
className={`hidden md:flex items-center justify-center overflow-hidden transform-gpu nav-transition-layer transition-[height,opacity,transform,border-color] duration-300 ease-out ${
569635
showDesktopSubNav
570-
? 'max-h-20 py-3 opacity-100 border-t border-white/10'
571-
: 'max-h-0 py-0 opacity-0 border-t border-transparent pointer-events-none'
636+
? 'h-[58px] opacity-100 translate-y-0 border-t border-white/10'
637+
: 'h-0 opacity-0 -translate-y-2 border-t border-transparent pointer-events-none'
572638
}`}
573639
>
574-
<div
575-
className="flex items-center rounded-full border border-white/10 bg-white/[0.03] px-2 divide-x divide-white/15 dropdown-container"
576-
onMouseLeave={handleMouseLeave}
577-
>
578-
{menuItems.map((menuItem) => (
579-
<div key={menuItem.key} className="px-3.5">
580-
<NavDropdown
581-
menuItem={menuItem}
582-
activeDropdown={activeDropdown}
583-
handleMouseEnter={handleMouseEnter}
584-
/>
585-
</div>
586-
))}
640+
<div className="h-[58px] flex items-center justify-center">
641+
<div
642+
className="flex items-center rounded-full border border-white/10 bg-white/[0.03] px-2 divide-x divide-white/15 dropdown-container"
643+
onMouseLeave={handleMouseLeave}
644+
>
645+
{menuItems.map((menuItem) => (
646+
<div key={menuItem.key} className="px-3.5">
647+
<NavDropdown
648+
menuItem={menuItem}
649+
activeDropdown={activeDropdown}
650+
handleMouseEnter={handleMouseEnter}
651+
/>
652+
</div>
653+
))}
654+
</div>
587655
</div>
588656
</div>
589657

590658
{/* Desktop Mega Menu */}
591659
{showDesktopSubNav && activeDropdownData && (
592660
<div
593-
className="absolute left-0 right-0 top-full mt-2 bg-stone-950/95 backdrop-blur-xl shadow-2xl animate-in slide-in-from-top-4 duration-300 z-50 dropdown-container"
661+
className="absolute left-0 right-0 top-full mt-2 bg-stone-950/95 backdrop-blur-xl shadow-2xl animate-in slide-in-from-top-4 duration-300 z-[210] dropdown-container transform-gpu nav-transition-layer"
594662
onMouseEnter={() => clearTimeout(dropdownTimeoutRef.current)}
595663
onMouseLeave={handleMouseLeave}
596664
>

frontend/src/index.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,15 @@
1818
.animate-slide-in {
1919
animation: slide-in 0.3s ease-out;
2020
}
21+
22+
.nav-shell {
23+
will-change: background-color, border-color, box-shadow;
24+
transform: translateZ(0);
25+
backface-visibility: hidden;
26+
}
27+
28+
.nav-transition-layer {
29+
will-change: transform, opacity;
30+
transform: translateZ(0);
31+
backface-visibility: hidden;
32+
}

0 commit comments

Comments
 (0)