11'use client' ;
22
3- import { useState , useEffect , type CSSProperties } from 'react' ;
3+ import { useState , useEffect , useLayoutEffect , useRef , type CSSProperties } from 'react' ;
44import { createPortal } from 'react-dom' ;
55import Image from 'next/image' ;
66import Link from 'next/link' ;
@@ -38,8 +38,11 @@ type Props = {
3838} ;
3939
4040export default function Navbar ( { variant = 'light' } : Props ) {
41+ const navRef = useRef < HTMLElement > ( null ) ;
4142 const [ scrolled , setScrolled ] = useState ( false ) ;
4243 const [ mobileOpen , setMobileOpen ] = useState ( false ) ;
44+ /** Measured from TopBar + nav (px). Fixed 7rem was wrong when TopBar wraps → gap + short panel on iOS. */
45+ const [ mobileMenuTopPx , setMobileMenuTopPx ] = useState < number | null > ( null ) ;
4346 const [ openDropdown , setOpenDropdown ] = useState < 'jobSupport' | 'locations' | null > ( null ) ;
4447 const [ mobileJobOpen , setMobileJobOpen ] = useState ( false ) ;
4548 const [ mobileLocOpen , setMobileLocOpen ] = useState ( false ) ;
@@ -61,6 +64,47 @@ export default function Navbar({ variant = 'light' }: Props) {
6164 } ;
6265 } , [ mobileOpen ] ) ;
6366
67+ useLayoutEffect ( ( ) => {
68+ if ( ! mobileOpen || typeof document === 'undefined' ) {
69+ setMobileMenuTopPx ( null ) ;
70+ return ;
71+ }
72+ const measure = ( ) => {
73+ const nav = navRef . current ;
74+ const topbar = document . querySelector ( '[data-pts-topbar]' ) ;
75+ let bottom = 0 ;
76+ if ( topbar instanceof HTMLElement ) {
77+ bottom = Math . max ( bottom , topbar . getBoundingClientRect ( ) . bottom ) ;
78+ }
79+ if ( nav ) {
80+ bottom = Math . max ( bottom , nav . getBoundingClientRect ( ) . bottom ) ;
81+ }
82+ setMobileMenuTopPx ( Math . max ( 0 , Math . ceil ( bottom ) ) ) ;
83+ } ;
84+ measure ( ) ;
85+ window . addEventListener ( 'resize' , measure ) ;
86+ window . addEventListener ( 'orientationchange' , measure ) ;
87+ const vv = window . visualViewport ;
88+ vv ?. addEventListener ( 'resize' , measure ) ;
89+ vv ?. addEventListener ( 'scroll' , measure ) ;
90+ return ( ) => {
91+ window . removeEventListener ( 'resize' , measure ) ;
92+ window . removeEventListener ( 'orientationchange' , measure ) ;
93+ vv ?. removeEventListener ( 'resize' , measure ) ;
94+ vv ?. removeEventListener ( 'scroll' , measure ) ;
95+ } ;
96+ } , [ mobileOpen ] ) ;
97+
98+ const mobileOverlayPosition : CSSProperties | undefined =
99+ mobileMenuTopPx != null
100+ ? {
101+ top : mobileMenuTopPx ,
102+ height : `calc(100dvh - ${ mobileMenuTopPx } px)` ,
103+ maxHeight : `calc(100svh - ${ mobileMenuTopPx } px)` ,
104+ bottom : 'auto' ,
105+ }
106+ : undefined ;
107+
64108 /** Above portal panel (2101) so the bar + ✕/☰ stay visible on legacy iOS when the sheet overlaps the row. */
65109 const navStackZ = mobileOpen ? 4000 : 1000 ;
66110
@@ -112,7 +156,7 @@ export default function Navbar({ variant = 'light' }: Props) {
112156 } ;
113157
114158 return (
115- < nav style = { navStyle } >
159+ < nav ref = { navRef } style = { navStyle } >
116160 < div
117161 style = { {
118162 width : '100%' ,
@@ -377,10 +421,12 @@ export default function Navbar({ variant = 'light' }: Props) {
377421 className = "pts-mobile-nav-backdrop"
378422 aria-label = "Close menu"
379423 onClick = { ( ) => setMobileOpen ( false ) }
424+ style = { mobileOverlayPosition }
380425 />
381426 < div
382427 className = "pts-mobile-nav-panel"
383428 style = { {
429+ ...mobileOverlayPosition ,
384430 background : dark ? 'var(--pts-nav-bg)' : 'var(--pts-bg)' ,
385431 borderTop : dark ? '1px solid rgba(255,255,255,0.1)' : '1px solid var(--pts-border)' ,
386432 padding : '0.75rem 1rem 1.25rem' ,
@@ -661,6 +707,7 @@ export default function Navbar({ variant = 'light' }: Props) {
661707 bottom: 0;
662708 z-index: 2101;
663709 overflow-y: auto;
710+ overscroll-behavior: contain;
664711 -webkit-overflow-scrolling: touch;
665712 box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.12);
666713 touch-action: manipulation;
0 commit comments