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