committed by
GitHub
11 changed files with 294 additions and 418 deletions
@ -1,79 +0,0 @@ |
|||||
/* |
|
||||
* Copyright (c) Facebook, Inc. and its affiliates. |
|
||||
*/ |
|
||||
|
|
||||
import * as React from 'react'; |
|
||||
import cn from 'classnames'; |
|
||||
import {RouteItem} from 'components/Layout/useRouteMeta'; |
|
||||
import {useRouter} from 'next/router'; |
|
||||
import {useActiveSection} from 'hooks/useActiveSection'; |
|
||||
import {SidebarRouteTree} from '../Sidebar'; |
|
||||
import sidebarHome from '../../../sidebarHome.json'; |
|
||||
import sidebarLearn from '../../../sidebarLearn.json'; |
|
||||
import sidebarReference from '../../../sidebarReference.json'; |
|
||||
|
|
||||
export function MobileNav() { |
|
||||
// This is where we actually are according to the router.
|
|
||||
const section = useActiveSection(); |
|
||||
|
|
||||
// Let the user switch tabs there and back without navigating.
|
|
||||
// Seed the tab state from the router, but keep it independent.
|
|
||||
const [tab, setTab] = React.useState(section); |
|
||||
|
|
||||
let tree = null; |
|
||||
switch (tab) { |
|
||||
case 'home': |
|
||||
tree = sidebarHome.routes[0]; |
|
||||
break; |
|
||||
case 'learn': |
|
||||
tree = sidebarLearn.routes[0]; |
|
||||
break; |
|
||||
case 'apis': |
|
||||
tree = sidebarReference.routes[0]; |
|
||||
break; |
|
||||
} |
|
||||
|
|
||||
return ( |
|
||||
<> |
|
||||
<div className="sticky top-0 px-5 mb-2 bg-wash dark:bg-wash-dark flex justify-end border-b border-border dark:border-border-dark items-center self-center w-full z-10"> |
|
||||
<TabButton isActive={tab === 'home'} onClick={() => setTab('home')}> |
|
||||
Home |
|
||||
</TabButton> |
|
||||
<TabButton isActive={tab === 'learn'} onClick={() => setTab('learn')}> |
|
||||
Learn |
|
||||
</TabButton> |
|
||||
<TabButton isActive={tab === 'apis'} onClick={() => setTab('apis')}> |
|
||||
API |
|
||||
</TabButton> |
|
||||
</div> |
|
||||
{/* No fallback UI so need to be careful not to suspend directly inside. */} |
|
||||
<React.Suspense fallback={null}> |
|
||||
<SidebarRouteTree routeTree={tree as RouteItem} isMobile={true} /> |
|
||||
</React.Suspense> |
|
||||
</> |
|
||||
); |
|
||||
} |
|
||||
|
|
||||
function TabButton({ |
|
||||
children, |
|
||||
onClick, |
|
||||
isActive, |
|
||||
}: { |
|
||||
children: any; |
|
||||
onClick: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void; |
|
||||
isActive: boolean; |
|
||||
}) { |
|
||||
const classes = cn( |
|
||||
'inline-flex items-center w-full border-b-2 justify-center text-base leading-9 px-3 py-0.5 hover:text-link hover:gray-5', |
|
||||
{ |
|
||||
'text-link dark:text-link-dark dark:border-link-dark border-link font-bold': |
|
||||
isActive, |
|
||||
'border-transparent': !isActive, |
|
||||
} |
|
||||
); |
|
||||
return ( |
|
||||
<button className={classes} onClick={onClick}> |
|
||||
{children} |
|
||||
</button> |
|
||||
); |
|
||||
} |
|
@ -1,56 +0,0 @@ |
|||||
/* |
|
||||
* Copyright (c) Facebook, Inc. and its affiliates. |
|
||||
*/ |
|
||||
|
|
||||
import * as React from 'react'; |
|
||||
import cn from 'classnames'; |
|
||||
import {SidebarContext} from 'components/Layout/useRouteMeta'; |
|
||||
import {MenuContext} from 'components/useMenu'; |
|
||||
import {useMediaQuery} from '../useMediaQuery'; |
|
||||
import {SidebarRouteTree} from './SidebarRouteTree'; |
|
||||
import {Search} from 'components/Search'; |
|
||||
import {MobileNav} from '../Nav/MobileNav'; |
|
||||
import {Feedback} from '../Feedback'; |
|
||||
|
|
||||
const SIDEBAR_BREAKPOINT = 1023; |
|
||||
|
|
||||
export function Sidebar() { |
|
||||
const {menuRef, isOpen} = React.useContext(MenuContext); |
|
||||
const isMobileSidebar = useMediaQuery(SIDEBAR_BREAKPOINT); |
|
||||
let routeTree = React.useContext(SidebarContext); |
|
||||
const isHidden = isMobileSidebar ? !isOpen : false; |
|
||||
|
|
||||
// HACK. Fix up the data structures instead.
|
|
||||
if ((routeTree as any).routes.length === 1) { |
|
||||
routeTree = (routeTree as any).routes[0]; |
|
||||
} |
|
||||
return ( |
|
||||
<aside |
|
||||
className={cn( |
|
||||
`lg:grow lg:flex flex-col w-full pt-4 pb-8 lg:pb-0 lg:max-w-xs fixed lg:sticky bg-wash dark:bg-wash-dark z-10 top-0`, |
|
||||
isOpen ? 'block z-40' : 'hidden lg:block' |
|
||||
)} |
|
||||
aria-hidden={isHidden}> |
|
||||
<div className="px-5 pt-16 sm:pt-10 lg:pt-0"> |
|
||||
<Search /> |
|
||||
</div> |
|
||||
<nav |
|
||||
role="navigation" |
|
||||
ref={menuRef} |
|
||||
style={{'--bg-opacity': '.2'} as React.CSSProperties} // Need to cast here because CSS vars aren't considered valid in TS types (cuz they could be anything)
|
|
||||
className="w-full h-screen lg:h-auto grow pr-0 lg:pr-5 pt-6 pb-44 lg:pb-0 lg:py-6 md:pt-4 lg:pt-4 overflow-y-scroll lg:overflow-y-auto scrolling-touch scrolling-gpu"> |
|
||||
{isMobileSidebar ? ( |
|
||||
<MobileNav /> |
|
||||
) : ( |
|
||||
/* No fallback UI so need to be careful not to suspend directly inside. */ |
|
||||
<React.Suspense fallback={null}> |
|
||||
<SidebarRouteTree routeTree={routeTree} /> |
|
||||
</React.Suspense> |
|
||||
)} |
|
||||
</nav> |
|
||||
<div className="sticky bottom-0 hidden lg:block"> |
|
||||
<Feedback /> |
|
||||
</div> |
|
||||
</aside> |
|
||||
); |
|
||||
} |
|
@ -1,52 +0,0 @@ |
|||||
/* |
|
||||
* Copyright (c) Facebook, Inc. and its affiliates. |
|
||||
*/ |
|
||||
|
|
||||
import {useState, useCallback, useEffect} from 'react'; |
|
||||
|
|
||||
const useMediaQuery = (width: number) => { |
|
||||
const [targetReached, setTargetReached] = useState(false); |
|
||||
|
|
||||
const updateTarget = useCallback((e: MediaQueryListEvent) => { |
|
||||
if (e.matches) { |
|
||||
setTargetReached(true); |
|
||||
} else { |
|
||||
setTargetReached(false); |
|
||||
} |
|
||||
}, []); |
|
||||
|
|
||||
useEffect(() => { |
|
||||
const media = window.matchMedia(`(max-width: ${width}px)`); |
|
||||
|
|
||||
try { |
|
||||
// Chrome & Firefox
|
|
||||
media.addEventListener('change', updateTarget); |
|
||||
} catch { |
|
||||
// @deprecated method - Safari <= iOS12
|
|
||||
media.addListener(updateTarget); |
|
||||
} |
|
||||
|
|
||||
// Check on mount (callback is not called until a change occurs)
|
|
||||
if (media.matches) { |
|
||||
setTargetReached(true); |
|
||||
} |
|
||||
|
|
||||
return () => { |
|
||||
try { |
|
||||
// Chrome & Firefox
|
|
||||
media.removeEventListener('change', updateTarget); |
|
||||
} catch { |
|
||||
// @deprecated method - Safari <= iOS12
|
|
||||
media.removeListener(updateTarget); |
|
||||
} |
|
||||
}; |
|
||||
}, [updateTarget, width]); |
|
||||
|
|
||||
return targetReached; |
|
||||
}; |
|
||||
|
|
||||
const useIsMobile = () => { |
|
||||
return useMediaQuery(640); |
|
||||
}; |
|
||||
|
|
||||
export {useMediaQuery, useIsMobile}; |
|
@ -1,72 +0,0 @@ |
|||||
/* |
|
||||
* Copyright (c) Facebook, Inc. and its affiliates. |
|
||||
*/ |
|
||||
|
|
||||
import * as React from 'react'; |
|
||||
import { |
|
||||
clearAllBodyScrollLocks, |
|
||||
disableBodyScroll, |
|
||||
enableBodyScroll, |
|
||||
} from 'body-scroll-lock'; |
|
||||
import {useRouter} from 'next/router'; |
|
||||
|
|
||||
/** |
|
||||
* Menu toggle that enables body scroll locking (for |
|
||||
* iOS Mobile and Tablet, Android, desktop Safari/Chrome/Firefox) |
|
||||
* without breaking scrolling of a target |
|
||||
* element. |
|
||||
*/ |
|
||||
export const useMenu = () => { |
|
||||
const [isOpen, setIsOpen] = React.useState(false); |
|
||||
const menuRef = React.useRef<HTMLDivElement>(null); |
|
||||
const router = useRouter(); |
|
||||
|
|
||||
const showSidebar = React.useCallback(() => { |
|
||||
setIsOpen(true); |
|
||||
if (menuRef.current != null) { |
|
||||
disableBodyScroll(menuRef.current); |
|
||||
} |
|
||||
}, []); |
|
||||
|
|
||||
const hideSidebar = React.useCallback(() => { |
|
||||
setIsOpen(false); |
|
||||
if (menuRef.current != null) { |
|
||||
enableBodyScroll(menuRef.current); |
|
||||
} |
|
||||
}, []); |
|
||||
|
|
||||
const toggleOpen = React.useCallback(() => { |
|
||||
if (isOpen) { |
|
||||
hideSidebar(); |
|
||||
} else { |
|
||||
showSidebar(); |
|
||||
} |
|
||||
}, [showSidebar, hideSidebar, isOpen]); |
|
||||
|
|
||||
React.useEffect(() => { |
|
||||
hideSidebar(); |
|
||||
return () => { |
|
||||
clearAllBodyScrollLocks(); |
|
||||
}; |
|
||||
}, [router.asPath, hideSidebar]); |
|
||||
|
|
||||
// Avoid top-level context re-renders
|
|
||||
return React.useMemo( |
|
||||
() => ({ |
|
||||
hideSidebar, |
|
||||
showSidebar, |
|
||||
toggleOpen, |
|
||||
menuRef, |
|
||||
isOpen, |
|
||||
}), |
|
||||
[hideSidebar, showSidebar, toggleOpen, menuRef, isOpen] |
|
||||
); |
|
||||
}; |
|
||||
|
|
||||
export const MenuContext = React.createContext<ReturnType<typeof useMenu>>( |
|
||||
{} as ReturnType<typeof useMenu> |
|
||||
); |
|
||||
|
|
||||
export function MenuProvider(props: {children: React.ReactNode}) { |
|
||||
return <MenuContext.Provider value={useMenu()} {...props} />; |
|
||||
} |
|
Loading…
Reference in new issue