| | import { useCallback, useEffect, useRef, useState } from 'react' |
| | import type { JSX } from 'react' |
| | import cx from 'classnames' |
| | import { useRouter } from 'next/router' |
| | import { Dialog, IconButton } from '@primer/react' |
| | import { MarkGithubIcon, ThreeBarsIcon } from '@primer/octicons-react' |
| |
|
| | import { DEFAULT_VERSION, useVersion } from '@/versions/components/useVersion' |
| | import { Link } from '@/frame/components/Link' |
| | import { useMainContext } from '@/frame/components/context/MainContext' |
| | import { HeaderNotifications } from '@/frame/components/page-header/HeaderNotifications' |
| | import { ApiVersionPicker } from '@/rest/components/ApiVersionPicker' |
| | import { useTranslation } from '@/languages/components/useTranslation' |
| | import { Breadcrumbs } from '@/frame/components/page-header/Breadcrumbs' |
| | import { VersionPicker } from '@/versions/components/VersionPicker' |
| | import { SidebarNav } from '@/frame/components/sidebar/SidebarNav' |
| | import { SearchBarButton } from '@/search/components/input/SearchBarButton' |
| | import { HeaderSearchAndWidgets } from './HeaderSearchAndWidgets' |
| | import { useInnerWindowWidth } from './hooks/useInnerWindowWidth' |
| | import { useMultiQueryParams } from '@/search/components/hooks/useMultiQueryParams' |
| | import { SearchOverlayContainer } from '@/search/components/input/SearchOverlayContainer' |
| | import { useCTAPopoverContext } from '@/frame/components/context/CTAContext' |
| | import { useSearchOverlayContext } from '@/search/components/context/SearchOverlayContext' |
| |
|
| | import styles from './Header.module.scss' |
| |
|
| | export const Header = () => { |
| | const router = useRouter() |
| | const { error } = useMainContext() |
| | const { isHomepageVersion, currentProduct, currentProductName } = useMainContext() |
| | const { currentVersion } = useVersion() |
| | const { t } = useTranslation(['header']) |
| | const isRestPage = currentProduct && currentProduct.id === 'rest' |
| | const { params, updateParams } = useMultiQueryParams() |
| | const [scroll, setScroll] = useState(false) |
| | const [isSidebarOpen, setIsSidebarOpen] = useState(false) |
| | const openSidebar = useCallback(() => setIsSidebarOpen(true), []) |
| | const closeSidebar = useCallback(() => setIsSidebarOpen(false), []) |
| | const isMounted = useRef(false) |
| | const menuButtonRef = useRef<HTMLButtonElement>(null) |
| | const { asPath } = useRouter() |
| | const isSearchResultsPage = router.route === '/search' |
| | const isEarlyAccessPage = currentProduct && currentProduct.id === 'early-access' |
| | const { width } = useInnerWindowWidth() |
| | const returnFocusRef = useRef(null) |
| | const searchButtonRef = useRef<HTMLButtonElement>(null) |
| | const { initializeCTA } = useCTAPopoverContext() |
| | const { isSearchOpen, setIsSearchOpen } = useSearchOverlayContext() |
| |
|
| | const SearchButtonLarge: JSX.Element = ( |
| | <SearchBarButton |
| | isSearchOpen={isSearchOpen} |
| | setIsSearchOpen={setIsSearchOpen} |
| | params={params} |
| | searchButtonRef={searchButtonRef} |
| | instanceId="large" |
| | /> |
| | ) |
| |
|
| | const SearchButtonSmall: JSX.Element = ( |
| | <SearchBarButton |
| | isSearchOpen={isSearchOpen} |
| | setIsSearchOpen={setIsSearchOpen} |
| | params={params} |
| | searchButtonRef={searchButtonRef} |
| | instanceId="small" |
| | /> |
| | ) |
| |
|
| | |
| | initializeCTA() |
| |
|
| | useEffect(() => { |
| | function onScroll() { |
| | setScroll(window.scrollY > 10) |
| | } |
| | window.addEventListener('scroll', onScroll) |
| | return () => { |
| | window.removeEventListener('scroll', onScroll) |
| | } |
| | }, []) |
| |
|
| | useEffect(() => { |
| | const close = (e: { key: string }) => { |
| | if (e.key === 'Escape') { |
| | setIsSearchOpen(false) |
| | } |
| | } |
| | window.addEventListener('keydown', close) |
| | return () => window.removeEventListener('keydown', close) |
| | }, []) |
| |
|
| | |
| | |
| | useEffect(() => { |
| | if (!isSearchOpen && isMounted.current && menuButtonRef.current) { |
| | menuButtonRef.current.focus() |
| | } |
| |
|
| | if (!isMounted.current) { |
| | isMounted.current = true |
| | } |
| | }, [isSearchOpen]) |
| |
|
| | |
| | |
| | useEffect(() => { |
| | const bodyDiv = document.querySelector('body div') as HTMLElement |
| | const body = document.querySelector('body') |
| | if (bodyDiv && body) { |
| | |
| | |
| | body.style.overflow = isSidebarOpen && width && width < 1280 ? 'hidden' : 'auto' |
| | } |
| | }, [isSidebarOpen]) |
| |
|
| | |
| | |
| | useEffect(() => { |
| | setIsSidebarOpen(false) |
| | }, [asPath]) |
| |
|
| | |
| | |
| | |
| | useEffect(() => { |
| | const hashChangeHandler = () => { |
| | setIsSidebarOpen(false) |
| | } |
| | window.addEventListener('hashchange', hashChangeHandler) |
| |
|
| | return () => { |
| | window.removeEventListener('hashchange', hashChangeHandler) |
| | } |
| | }, []) |
| |
|
| | let homeURL = `/${router.locale}` |
| | if (currentVersion !== DEFAULT_VERSION) { |
| | homeURL += `/${currentVersion}` |
| | } |
| |
|
| | return ( |
| | <div |
| | data-container="header" |
| | className={cx( |
| | 'border-bottom d-unset color-border-muted no-print z-3 color-bg-default', |
| | styles.header, |
| | )} |
| | > |
| | {error !== '404' && <HeaderNotifications />} |
| | <header |
| | className={cx( |
| | 'color-bg-default p-2 position-sticky top-0 z-2 border-bottom', |
| | scroll && 'color-shadow-small', |
| | )} |
| | role="banner" |
| | aria-label="Main" |
| | > |
| | <div |
| | className={cx( |
| | 'd-flex flex-justify-between p-2 flex-items-center flex-wrap', |
| | styles.headerContainer, |
| | )} |
| | data-testid="desktop-header" |
| | > |
| | <div |
| | tabIndex={-1} |
| | className={cx(isSearchOpen ? styles.logoWithOpenSearch : styles.logoWithClosedSearch)} |
| | id="github-logo" |
| | > |
| | <Link |
| | href={homeURL} |
| | className="d-flex flex-items-center color-fg-default no-underline mr-3" |
| | > |
| | <MarkGithubIcon size={32} /> |
| | <span className="h4 text-semibold ml-2 mr-3">{t('github_docs')}</span> |
| | </Link> |
| | <div className="hide-sm border-left pl-3 d-flex flex-items-center"> |
| | <VersionPicker /> |
| | {/* In larger viewports, we want to show the search bar next to the version picker */} |
| | <div className={styles.displayOverLarge}>{SearchButtonLarge}</div> |
| | </div> |
| | </div> |
| | <HeaderSearchAndWidgets |
| | isSearchOpen={isSearchOpen} |
| | SearchButton={SearchButtonSmall} |
| | width={width} |
| | /> |
| | </div> |
| | {!isHomepageVersion && !isSearchResultsPage && ( |
| | <div className="d-flex flex-items-center d-xxl-none mt-2" data-testid="header-subnav"> |
| | {!isEarlyAccessPage && ( |
| | <div |
| | className={cx(styles.sidebarOverlayCloseButtonContainer, 'mr-2')} |
| | data-testid="header-subnav-hamburger" |
| | > |
| | <IconButton |
| | data-testid="sidebar-hamburger" |
| | className="color-fg-muted" |
| | variant="invisible" |
| | icon={ThreeBarsIcon} |
| | aria-label="Open Sidebar" |
| | onClick={openSidebar} |
| | ref={returnFocusRef} |
| | /> |
| | {isSidebarOpen && ( |
| | <Dialog |
| | returnFocusRef={returnFocusRef} |
| | onClose={closeSidebar} |
| | className={cx(styles.dialog, 'd-xxl-none')} |
| | position="left" |
| | title={ |
| | error === '404' || !currentProduct || isSearchResultsPage |
| | ? null |
| | : currentProductName || currentProduct.name |
| | } |
| | subtitle={isRestPage && <ApiVersionPicker />} |
| | width="medium" |
| | > |
| | <SidebarNav variant="overlay" /> |
| | </Dialog> |
| | )} |
| | </div> |
| | )} |
| | <div className="mr-auto width-full" data-search="breadcrumbs"> |
| | <Breadcrumbs inHeader={true} /> |
| | </div> |
| | </div> |
| | )} |
| | <SearchOverlayContainer |
| | isSearchOpen={isSearchOpen} |
| | setIsSearchOpen={setIsSearchOpen} |
| | params={params} |
| | updateParams={updateParams} |
| | searchButtonRef={searchButtonRef} |
| | /> |
| | </header> |
| | </div> |
| | ) |
| | } |
| |
|