1
1
import { useCallback , useEffect , useRef , useState } from 'react'
2
2
import cx from 'classnames'
3
3
import { useRouter } from 'next/router'
4
- import { AnchoredOverlay , IconButton } from '@primer/react'
4
+ import { AnchoredOverlay , Dialog , IconButton } from '@primer/react'
5
5
import {
6
6
KebabHorizontalIcon ,
7
7
LinkExternalIcon ,
8
8
MarkGithubIcon ,
9
9
SearchIcon ,
10
+ ThreeBarsIcon ,
10
11
XIcon ,
11
12
} from '@primer/octicons-react'
12
13
@@ -19,14 +20,17 @@ import { HeaderNotifications } from 'components/page-header/HeaderNotifications'
19
20
import { ApiVersionPicker } from 'components/sidebar/ApiVersionPicker'
20
21
import { useTranslation } from 'components/hooks/useTranslation'
21
22
import { Search } from 'components/Search'
23
+ import { Breadcrumbs } from 'components/page-header/Breadcrumbs'
22
24
import { VersionPicker } from 'components/page-header/VersionPicker'
25
+ import { SidebarNav } from 'components/sidebar/SidebarNav'
26
+ import { AllProductsLink } from 'components/sidebar/AllProductsLink'
23
27
24
28
import styles from './Header.module.scss'
25
29
26
30
export const Header = ( ) => {
27
31
const router = useRouter ( )
28
32
const { error } = useMainContext ( )
29
- const { currentProduct, allVersions } = useMainContext ( )
33
+ const { isHomepageVersion , currentProduct, currentProductTree , allVersions } = useMainContext ( )
30
34
const { currentVersion } = useVersion ( )
31
35
const { t } = useTranslation ( [ 'header' ] )
32
36
const isRestPage = currentProduct && currentProduct . id === 'rest'
@@ -36,12 +40,21 @@ export const Header = () => {
36
40
const [ isMenuOpen , setIsMenuOpen ] = useState ( false )
37
41
const openMenuOverlay = useCallback ( ( ) => setIsMenuOpen ( true ) , [ setIsMenuOpen ] )
38
42
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 ] )
39
46
const isMounted = useRef ( false )
40
47
const menuButtonRef = useRef < HTMLButtonElement > ( null )
41
-
48
+ const { asPath } = useRouter ( )
49
+ const isSearchResultsPage = router . route === '/search'
42
50
const signupCTAVisible =
43
51
hasAccount === false && // don't show if `null`
44
52
( 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
+ } , [ ] )
45
58
46
59
useEffect ( ( ) => {
47
60
function onScroll ( ) {
@@ -75,6 +88,40 @@ export const Header = () => {
75
88
}
76
89
} , [ isSearchOpen ] )
77
90
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
+
78
125
return (
79
126
< >
80
127
< div
@@ -86,12 +133,12 @@ export const Header = () => {
86
133
{ error !== '404' && < HeaderNotifications /> }
87
134
< header
88
135
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' ,
90
137
scroll && 'color-shadow-small'
91
138
) }
92
139
>
93
140
< 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"
95
142
data-testid = "desktop-header"
96
143
>
97
144
< div
@@ -170,8 +217,9 @@ export const Header = () => {
170
217
/>
171
218
172
219
{ /* The ... navigation menu at medium and smaller widths */ }
173
- < nav >
220
+ < div >
174
221
< AnchoredOverlay
222
+ anchorRef = { menuButtonRef }
175
223
renderAnchor = { ( anchorProps ) => (
176
224
< IconButton
177
225
data-testid = "mobile-menu"
@@ -205,7 +253,7 @@ export const Header = () => {
205
253
</ span >
206
254
{ isRestPage && allVersions [ currentVersion ] . apiVersions . length > 0 && (
207
255
< span className = "pb-2 m-2 d-block" >
208
- < ApiVersionPicker mediumOrLower = { true } />
256
+ < ApiVersionPicker />
209
257
</ span >
210
258
) }
211
259
{ signupCTAVisible && (
@@ -222,9 +270,66 @@ export const Header = () => {
222
270
) }
223
271
</ div >
224
272
</ AnchoredOverlay >
225
- </ nav >
273
+ </ div >
226
274
</ div >
227
275
</ 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
+ ) }
228
333
</ header >
229
334
</ div >
230
335
</ >
0 commit comments