11import { useCallback , useEffect , useRef , useState } from 'react'
22import cx from 'classnames'
33import { useRouter } from 'next/router'
4- import { AnchoredOverlay , IconButton } from '@primer/react'
4+ import { AnchoredOverlay , Dialog , IconButton } from '@primer/react'
55import {
66 KebabHorizontalIcon ,
77 LinkExternalIcon ,
88 MarkGithubIcon ,
99 SearchIcon ,
10+ ThreeBarsIcon ,
1011 XIcon ,
1112} from '@primer/octicons-react'
1213
@@ -19,14 +20,17 @@ import { HeaderNotifications } from 'components/page-header/HeaderNotifications'
1920import { ApiVersionPicker } from 'components/sidebar/ApiVersionPicker'
2021import { useTranslation } from 'components/hooks/useTranslation'
2122import { Search } from 'components/Search'
23+ import { Breadcrumbs } from 'components/page-header/Breadcrumbs'
2224import { VersionPicker } from 'components/page-header/VersionPicker'
25+ import { SidebarNav } from 'components/sidebar/SidebarNav'
26+ import { AllProductsLink } from 'components/sidebar/AllProductsLink'
2327
2428import styles from './Header.module.scss'
2529
2630export const Header = ( ) => {
2731 const router = useRouter ( )
2832 const { error } = useMainContext ( )
29- const { currentProduct, allVersions } = useMainContext ( )
33+ const { isHomepageVersion , currentProduct, currentProductTree , allVersions } = useMainContext ( )
3034 const { currentVersion } = useVersion ( )
3135 const { t } = useTranslation ( [ 'header' ] )
3236 const isRestPage = currentProduct && currentProduct . id === 'rest'
@@ -36,12 +40,21 @@ export const Header = () => {
3640 const [ isMenuOpen , setIsMenuOpen ] = useState ( false )
3741 const openMenuOverlay = useCallback ( ( ) => setIsMenuOpen ( true ) , [ setIsMenuOpen ] )
3842 const closeMenuOverlay = useCallback ( ( ) => setIsMenuOpen ( false ) , [ setIsMenuOpen ] )
43+ const [ isSidebarOpen , setIsSidebarOpen ] = useState ( false )
44+ const openSidebar = useCallback ( ( ) => setIsSidebarOpen ( true ) , [ isSidebarOpen ] )
45+ const closeSidebar = useCallback ( ( ) => setIsSidebarOpen ( false ) , [ isSidebarOpen ] )
3946 const isMounted = useRef ( false )
4047 const menuButtonRef = useRef < HTMLButtonElement > ( null )
41-
48+ const { asPath } = useRouter ( )
49+ const isSearchResultsPage = router . route === '/search'
4250 const signupCTAVisible =
4351 hasAccount === false && // don't show if `null`
4452 ( currentVersion === DEFAULT_VERSION || currentVersion === 'enterprise-cloud@latest' )
53+ const productTitle = currentProductTree ?. shortTitle || currentProductTree ?. title
54+ const [ windowSize , setWindowSize ] = useState ( 0 )
55+ const handleWindowResize = useCallback ( ( ) => {
56+ setWindowSize ( window . innerWidth )
57+ } , [ ] )
4558
4659 useEffect ( ( ) => {
4760 function onScroll ( ) {
@@ -75,6 +88,40 @@ export const Header = () => {
7588 }
7689 } , [ isSearchOpen ] )
7790
91+ // When the sidebar overlay is opened, prevent the main content from being
92+ // scrollable.
93+ useEffect ( ( ) => {
94+ const bodyDiv = document . querySelector ( 'body div' ) as HTMLElement
95+ const body = document . querySelector ( 'body' )
96+ if ( bodyDiv && body ) {
97+ // The full sidebar automatically shows at the xl window size so unlock
98+ // scrolling if the overlay was opened and the window size is increased to xl.
99+ body . style . overflow = isSidebarOpen && windowSize < 1280 ? 'hidden' : 'auto'
100+ }
101+ window . addEventListener ( 'resize' , handleWindowResize )
102+ return ( ) => window . removeEventListener ( 'resize' , handleWindowResize )
103+ } , [ isSidebarOpen , windowSize ] )
104+
105+ // with client side navigation clicking sidebar overlay links doesn't dismiss
106+ // the overlay so we close it ourselves when the path changes
107+ useEffect ( ( ) => {
108+ setIsSidebarOpen ( false )
109+ } , [ asPath ] )
110+
111+ // on REST pages there are sidebar links that are hash anchor links to different
112+ // sections on the same page so the sidebar overlay doesn't dismiss. we listen
113+ // for hash changes and close the overlay when the hash changes.
114+ useEffect ( ( ) => {
115+ const hashChangeHandler = ( ) => {
116+ setIsSidebarOpen ( false )
117+ }
118+ window . addEventListener ( 'hashchange' , hashChangeHandler )
119+
120+ return ( ) => {
121+ window . removeEventListener ( 'hashchange' , hashChangeHandler )
122+ }
123+ } , [ ] )
124+
78125 return (
79126 < >
80127 < div
@@ -86,12 +133,12 @@ export const Header = () => {
86133 { error !== '404' && < HeaderNotifications /> }
87134 < header
88135 className = { cx (
89- 'color-bg-default px-3 pt-3 pb-3 position-sticky top-0 z-1 border-bottom' ,
136+ 'color-bg-default p-2 position-sticky top-0 z-1 border-bottom' ,
90137 scroll && 'color-shadow-small'
91138 ) }
92139 >
93140 < div
94- className = "d-flex flex-justify-between flex-items-center flex-wrap"
141+ className = "d-flex flex-justify-between p-2 flex-items-center flex-wrap"
95142 data-testid = "desktop-header"
96143 >
97144 < div
@@ -170,8 +217,9 @@ export const Header = () => {
170217 />
171218
172219 { /* The ... navigation menu at medium and smaller widths */ }
173- < nav >
220+ < div >
174221 < AnchoredOverlay
222+ anchorRef = { menuButtonRef }
175223 renderAnchor = { ( anchorProps ) => (
176224 < IconButton
177225 data-testid = "mobile-menu"
@@ -205,7 +253,7 @@ export const Header = () => {
205253 </ span >
206254 { isRestPage && allVersions [ currentVersion ] . apiVersions . length > 0 && (
207255 < span className = "pb-2 m-2 d-block" >
208- < ApiVersionPicker mediumOrLower = { true } />
256+ < ApiVersionPicker />
209257 </ span >
210258 ) }
211259 { signupCTAVisible && (
@@ -222,9 +270,66 @@ export const Header = () => {
222270 ) }
223271 </ div >
224272 </ AnchoredOverlay >
225- </ nav >
273+ </ div >
226274 </ div >
227275 </ div >
276+ { ! isHomepageVersion && ! isSearchResultsPage && (
277+ < div className = "d-flex flex-items-center d-xl-none mt-2" >
278+ < div className = { cx ( styles . sidebarOverlayCloseButtonContainer , 'mr-2' ) } >
279+ < IconButton
280+ data-testid = "sidebar-hamburger"
281+ className = "color-fg-muted"
282+ variant = "invisible"
283+ icon = { ThreeBarsIcon }
284+ aria-label = "Open Sidebar"
285+ onClick = { openSidebar }
286+ />
287+ < Dialog
288+ isOpen = { isSidebarOpen }
289+ onDismiss = { closeSidebar }
290+ aria-labelledby = "menu-title"
291+ sx = { {
292+ position : 'fixed' ,
293+ top : '0' ,
294+ left : '0' ,
295+ marginTop : '0' ,
296+ maxHeight : '100vh' ,
297+ width : 'auto !important' ,
298+ transform : 'none' ,
299+ borderRadius : '0' ,
300+ borderRight : '1px solid var(--color-border-default)' ,
301+ } }
302+ >
303+ < Dialog . Header
304+ style = { { paddingTop : '0px' , background : 'none' } }
305+ id = "sidebar-overlay-header"
306+ sx = { { display : 'block' } }
307+ >
308+ < AllProductsLink />
309+ { error === '404' ||
310+ ! currentProduct ||
311+ isSearchResultsPage ||
312+ ! currentProductTree ? null : (
313+ < div className = "mt-3" >
314+ < Link
315+ data-testid = "sidebar-product-dialog"
316+ href = { currentProductTree . href }
317+ className = "d-block pl-1 mb-2 h3 color-fg-default no-underline"
318+ >
319+ { productTitle }
320+ </ Link >
321+ </ div >
322+ ) }
323+ { isRestPage && < ApiVersionPicker /> }
324+ </ Dialog . Header >
325+ < SidebarNav variant = "overlay" />
326+ </ Dialog >
327+ </ div >
328+ < div className = "mr-auto width-full" data-search = "breadcrumbs" >
329+ < Breadcrumbs inHeader = { true } />
330+ </ div >
331+ </ div >
332+ ) }
228333 </ header >
229334 </ div >
230335 </ >
0 commit comments