Browse Source

[Beta] Refactor Navigation (#5001)

main
dan 3 years ago
committed by GitHub
parent
commit
f7c91ebbf1
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      beta/package.json
  2. 79
      beta/src/components/Layout/Nav/MobileNav.tsx
  3. 356
      beta/src/components/Layout/Nav/Nav.tsx
  4. 47
      beta/src/components/Layout/Page.tsx
  5. 56
      beta/src/components/Layout/Sidebar/Sidebar.tsx
  6. 17
      beta/src/components/Layout/Sidebar/SidebarLink.tsx
  7. 12
      beta/src/components/Layout/Sidebar/SidebarRouteTree.tsx
  8. 1
      beta/src/components/Layout/Sidebar/index.tsx
  9. 52
      beta/src/components/Layout/useMediaQuery.tsx
  10. 17
      beta/src/components/MDX/Sandpack/CustomPreset.tsx
  11. 72
      beta/src/components/useMenu.tsx

3
beta/package.json

@ -37,8 +37,7 @@
"parse-numeric-range": "^1.2.0",
"react": "0.0.0-experimental-82c64e1a4-20220520",
"react-collapsed": "3.1.0",
"react-dom": "0.0.0-experimental-82c64e1a4-20220520",
"scroll-into-view-if-needed": "^2.2.25"
"react-dom": "0.0.0-experimental-82c64e1a4-20220520"
},
"devDependencies": {
"@babel/core": "^7.12.9",

79
beta/src/components/Layout/Nav/MobileNav.tsx

@ -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>
);
}

356
beta/src/components/Layout/Nav/Nav.tsx

@ -6,16 +6,25 @@ import * as React from 'react';
import cn from 'classnames';
import NextLink from 'next/link';
import {useRouter} from 'next/router';
import {
clearAllBodyScrollLocks,
disableBodyScroll,
enableBodyScroll,
} from 'body-scroll-lock';
import {IconClose} from 'components/Icon/IconClose';
import {IconHamburger} from 'components/Icon/IconHamburger';
import {Search} from 'components/Search';
import {MenuContext} from 'components/useMenu';
import {useActiveSection} from 'hooks/useActiveSection';
import {Logo} from '../../Logo';
import {Feedback} from '../Feedback';
import NavLink from './NavLink';
import {SidebarContext} from 'components/Layout/useRouteMeta';
import {SidebarRouteTree} from '../Sidebar/SidebarRouteTree';
import type {RouteItem} from '../useRouteMeta';
import sidebarHome from '../../../sidebarHome.json';
import sidebarLearn from '../../../sidebarLearn.json';
import sidebarReference from '../../../sidebarReference.json';
declare global {
interface Window {
@ -88,12 +97,80 @@ const lightIcon = (
);
export default function Nav() {
const {isOpen, toggleOpen} = React.useContext(MenuContext);
const [isOpen, setIsOpen] = React.useState(false);
const [showFeedback, setShowFeedback] = React.useState(false);
const menuRef = React.useRef<HTMLDivElement>(null);
const feedbackAutohideRef = React.useRef<any>(null);
const section = useActiveSection();
const {asPath} = useRouter();
const feedbackPopupRef = React.useRef<null | HTMLDivElement>(null);
// In desktop mode, use the route tree for current route.
let routeTree: RouteItem = React.useContext(SidebarContext);
// In mobile mode, 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);
const [prevSection, setPrevSection] = React.useState(section);
if (prevSection !== section) {
setPrevSection(section);
setTab(section);
}
if (isOpen) {
switch (tab) {
case 'home':
routeTree = sidebarHome as RouteItem;
break;
case 'learn':
routeTree = sidebarLearn as RouteItem;
break;
case 'apis':
routeTree = sidebarReference as RouteItem;
break;
}
}
// HACK. Fix up the data structures instead.
if ((routeTree as any).routes.length === 1) {
routeTree = (routeTree as any).routes[0];
}
// While the overlay is open, disable body scroll.
React.useEffect(() => {
if (isOpen) {
const preferredScrollParent = menuRef.current!;
disableBodyScroll(preferredScrollParent);
return () => enableBodyScroll(preferredScrollParent);
} else {
return undefined;
}
}, [isOpen]);
// Close the overlay on any navigation.
React.useEffect(() => {
setIsOpen(false);
}, [asPath]);
// Also close the overlay if the window gets resized past mobile layout.
// (This is also important because we don't want to keep the body locked!)
React.useEffect(() => {
const media = window.matchMedia(`(max-width: 1023px)`);
function closeIfNeeded() {
if (!media.matches) {
setIsOpen(false);
}
}
closeIfNeeded();
media.addEventListener('change', closeIfNeeded);
return () => {
media.removeEventListener('change', closeIfNeeded);
};
}, []);
function handleFeedback() {
clearTimeout(feedbackAutohideRef.current);
setShowFeedback(!showFeedback);
}
// Hide the Feedback widget on any click outside.
React.useEffect(() => {
if (!showFeedback) {
return;
@ -113,121 +190,186 @@ export default function Nav() {
capture: true,
});
}, [showFeedback]);
function handleFeedback() {
clearTimeout(feedbackAutohideRef.current);
setShowFeedback(!showFeedback);
}
return (
<nav className="sticky top-0 items-center w-full flex lg:block justify-between bg-wash dark:bg-wash-dark pt-0 lg:pt-4 pr-5 lg:px-5 z-50">
<div className="xl:w-full xl:max-w-xs flex items-center">
<button
type="button"
aria-label="Menu"
onClick={toggleOpen}
className={cn('flex lg:hidden items-center h-full px-4', {
'text-link dark:text-link-dark mr-0': isOpen,
})}>
{!isOpen ? <IconHamburger /> : <IconClose />}
</button>
<NextLink href="/">
<a className="inline-flex text-l font-normal items-center text-primary dark:text-primary-dark py-1 mr-0 sm:mr-3 whitespace-nowrap">
<Logo className="text-sm mr-2 w-8 h-8 text-link dark:text-link-dark" />
React Docs
</a>
</NextLink>
<div className="lg:w-full leading-loose hidden sm:flex flex-initial items-center h-auto pr-5 lg:pr-5 pt-0.5">
<div className="px-1 mb-px bg-highlight dark:bg-highlight-dark rounded text-link dark:text-link-dark uppercase font-bold tracking-wide text-xs whitespace-nowrap">
Beta
</div>
</div>
<div className="block dark:hidden">
<>
<nav className="lg:sticky top-0 items-center w-full flex lg:block justify-between bg-wash dark:bg-wash-dark pt-0 lg:pt-4 pr-5 lg:px-5 z-50">
<div className="xl:w-full xl:max-w-xs flex items-center">
<button
type="button"
aria-label="Use Dark Mode"
onClick={() => {
window.__setPreferredTheme('dark');
}}
className="hidden lg:flex items-center h-full pr-2">
{darkIcon}
aria-label="Menu"
onClick={() => setIsOpen(!isOpen)}
className={cn('flex lg:hidden items-center h-full px-4', {
'text-link dark:text-link-dark mr-0': isOpen,
})}>
{isOpen ? <IconClose /> : <IconHamburger />}
</button>
<NextLink href="/">
<a className="inline-flex text-l font-normal items-center text-primary dark:text-primary-dark py-1 mr-0 sm:mr-3 whitespace-nowrap">
<Logo className="text-sm mr-2 w-8 h-8 text-link dark:text-link-dark" />
React Docs
</a>
</NextLink>
<div className="lg:w-full leading-loose hidden sm:flex flex-initial items-center h-auto pr-5 lg:pr-5 pt-0.5">
<div className="px-1 mb-px bg-highlight dark:bg-highlight-dark rounded text-link dark:text-link-dark uppercase font-bold tracking-wide text-xs whitespace-nowrap">
Beta
</div>
</div>
<div className="block dark:hidden">
<button
type="button"
aria-label="Use Dark Mode"
onClick={() => {
window.__setPreferredTheme('dark');
}}
className="hidden lg:flex items-center h-full pr-2">
{darkIcon}
</button>
</div>
<div className="hidden dark:block">
<button
type="button"
aria-label="Use Light Mode"
onClick={() => {
window.__setPreferredTheme('light');
}}
className="hidden lg:flex items-center h-full pr-2">
{lightIcon}
</button>
</div>
</div>
<div className="hidden dark:block">
<button
type="button"
aria-label="Use Light Mode"
onClick={() => {
window.__setPreferredTheme('light');
}}
className="hidden lg:flex items-center h-full pr-2">
{lightIcon}
</button>
<div className="px-0 pt-2 w-full 2xl:max-w-xs hidden lg:flex items-center self-center border-b-0 lg:border-b border-border dark:border-border-dark">
<NavLink href="/" isActive={section === 'home'}>
Home
</NavLink>
<NavLink href="/learn" isActive={section === 'learn'}>
Learn
</NavLink>
<NavLink href="/apis/react" isActive={section === 'apis'}>
API
</NavLink>
</div>
</div>
<div className="px-0 pt-2 w-full 2xl:max-w-xs hidden lg:flex items-center self-center border-b-0 lg:border-b border-border dark:border-border-dark">
<NavLink href="/" isActive={section === 'home'}>
Home
</NavLink>
<NavLink href="/learn" isActive={section === 'learn'}>
Learn
</NavLink>
<NavLink href="/apis/react" isActive={section === 'apis'}>
API
</NavLink>
</div>
<div className="flex my-4 h-10 mx-0 w-full lg:hidden justify-end lg:max-w-sm">
<Search />
<button
aria-label="Give feedback"
type="button"
className={cn(
'inline-flex lg:hidden items-center rounded-full px-1.5 ml-4 lg:ml-6 relative top-px',
{
'bg-card dark:bg-card-dark': showFeedback,
}
)}
onClick={handleFeedback}>
{feedbackIcon}
</button>
<div className="block dark:hidden">
<div className="flex my-4 h-10 mx-0 w-full lg:hidden justify-end lg:max-w-sm">
<Search />
<button
aria-label="Give feedback"
type="button"
aria-label="Use Dark Mode"
onClick={() => {
window.__setPreferredTheme('dark');
}}
className="flex lg:hidden items-center p-1 h-full ml-4 lg:ml-6">
{darkIcon}
className={cn(
'inline-flex lg:hidden items-center rounded-full px-1.5 ml-4 lg:ml-6 relative top-px',
{
'bg-card dark:bg-card-dark': showFeedback,
}
)}
onClick={handleFeedback}>
{feedbackIcon}
</button>
<div className="block dark:hidden">
<button
type="button"
aria-label="Use Dark Mode"
onClick={() => {
window.__setPreferredTheme('dark');
}}
className="flex lg:hidden items-center p-1 h-full ml-4 lg:ml-6">
{darkIcon}
</button>
</div>
<div
ref={feedbackPopupRef}
className={cn(
'fixed top-12 right-0',
showFeedback ? 'block' : 'hidden'
)}>
<Feedback
onSubmit={() => {
clearTimeout(feedbackAutohideRef.current);
feedbackAutohideRef.current = setTimeout(() => {
setShowFeedback(false);
}, 1000);
}}
/>
</div>
<div className="hidden dark:block">
<button
type="button"
aria-label="Use Light Mode"
onClick={() => {
window.__setPreferredTheme('light');
}}
className="flex lg:hidden items-center p-1 h-full ml-4 lg:ml-6">
{lightIcon}
</button>
</div>
</div>
<div
ref={feedbackPopupRef}
className={cn(
'fixed top-12 right-0',
showFeedback ? 'block' : 'hidden'
)}>
<Feedback
onSubmit={() => {
clearTimeout(feedbackAutohideRef.current);
feedbackAutohideRef.current = setTimeout(() => {
setShowFeedback(false);
}, 1000);
}}
/>
</nav>
{isOpen && (
<div className="bg-wash dark:bg-wash-dark px-5 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>
<div className="hidden dark:block">
<button
type="button"
aria-label="Use Light Mode"
onClick={() => {
window.__setPreferredTheme('light');
}}
className="flex lg:hidden items-center p-1 h-full ml-4 lg:ml-6">
{lightIcon}
</button>
)}
<aside
className={cn(
`lg:grow lg:flex flex-col w-full pb-8 lg:pb-0 lg:max-w-xs bg-wash dark:bg-wash-dark z-10`,
isOpen ? 'block z-40' : 'hidden lg:block'
)}>
{!isOpen && (
<div className="px-5 sm:pt-10 lg:pt-4">
<Search />
</div>
)}
<nav
role="navigation"
ref={menuRef}
style={{'--bg-opacity': '.2'} as React.CSSProperties}
className="w-full h-screen lg:h-auto grow pr-0 lg:pr-5 pt-2 pb-44 lg:pb-0 lg:py-6 md:pt-2 lg:pt-4 overflow-y-scroll lg:overflow-y-auto scrolling-touch scrolling-gpu">
{/* No fallback UI so need to be careful not to suspend directly inside. */}
<React.Suspense fallback={null}>
<SidebarRouteTree
// Don't share state between the desktop and mobile versions.
// This avoids unnecessary animations and visual flicker.
key={isOpen ? 'mobile-overlay' : 'desktop-or-hidden'}
routeTree={routeTree}
isForceExpanded={isOpen}
/>
</React.Suspense>
</nav>
<div className="sticky bottom-0 hidden lg:block">
<Feedback />
</div>
</div>
</nav>
</aside>
</>
);
}
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 pb-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>
);
}

47
beta/src/components/Layout/Page.tsx

@ -2,13 +2,11 @@
* Copyright (c) Facebook, Inc. and its affiliates.
*/
import {MenuProvider} from 'components/useMenu';
import * as React from 'react';
import {useRouter} from 'next/router';
import {Nav} from './Nav';
import {RouteItem, SidebarContext} from './useRouteMeta';
import {useActiveSection} from 'hooks/useActiveSection';
import {Sidebar} from './Sidebar';
import {Footer} from './Footer';
import SocialBanner from '../SocialBanner';
import sidebarHome from '../../sidebarHome.json';
@ -34,32 +32,29 @@ export function Page({children}: PageProps) {
return (
<>
<SocialBanner />
<MenuProvider>
<SidebarContext.Provider value={routeTree}>
<div className="h-auto lg:h-screen flex flex-row">
<div className="no-bg-scrollbar h-auto lg:h-[calc(100%-40px)] lg:overflow-y-scroll fixed flex flex-row lg:flex-col py-0 top-16 sm:top-10 left-0 right-0 lg:max-w-xs w-full shadow lg:shadow-none z-50">
<Nav />
<Sidebar />
</div>
<SidebarContext.Provider value={routeTree}>
<div className="h-auto lg:h-screen flex flex-row">
<div className="no-bg-scrollbar h-auto lg:h-[calc(100%-40px)] lg:overflow-y-scroll fixed flex flex-col py-0 top-16 sm:top-10 left-0 right-0 lg:max-w-xs w-full shadow lg:shadow-none z-50">
<Nav />
</div>
{/* No fallback UI so need to be careful not to suspend directly inside. */}
<React.Suspense fallback={null}>
<div className="flex flex-1 w-full h-full self-stretch">
<div className="w-full min-w-0">
<main className="flex flex-1 self-stretch mt-16 sm:mt-10 flex-col items-end justify-around">
<article
key={asPath}
className="h-full mx-auto relative w-full min-w-0">
{children}
</article>
<Footer />
</main>
</div>
{/* No fallback UI so need to be careful not to suspend directly inside. */}
<React.Suspense fallback={null}>
<div className="flex flex-1 w-full h-full self-stretch">
<div className="w-full min-w-0">
<main className="flex flex-1 self-stretch mt-16 sm:mt-10 flex-col items-end justify-around">
<article
key={asPath}
className="h-full mx-auto relative w-full min-w-0">
{children}
</article>
<Footer />
</main>
</div>
</React.Suspense>
</div>
</SidebarContext.Provider>
</MenuProvider>
</div>
</React.Suspense>
</div>
</SidebarContext.Provider>
</>
);
}

56
beta/src/components/Layout/Sidebar/Sidebar.tsx

@ -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>
);
}

17
beta/src/components/Layout/Sidebar/SidebarLink.tsx

@ -5,11 +5,9 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import * as React from 'react';
import scrollIntoView from 'scroll-into-view-if-needed';
import cn from 'classnames';
import {IconNavArrow} from 'components/Icon/IconNavArrow';
import Link from 'next/link';
import {useIsMobile} from '../useMediaQuery';
interface SidebarLinkProps {
href: string;
@ -38,17 +36,16 @@ export function SidebarLink({
isPending,
}: SidebarLinkProps) {
const ref = React.useRef<HTMLAnchorElement>(null);
const isMobile = useIsMobile();
React.useEffect(() => {
if (ref && ref.current && !!selected && !isMobile) {
scrollIntoView(ref.current, {
scrollMode: 'if-needed',
block: 'center',
inline: 'nearest',
});
if (selected && ref && ref.current) {
// @ts-ignore
if (typeof ref.current.scrollIntoViewIfNeeded === 'function') {
// @ts-ignore
ref.current.scrollIntoViewIfNeeded();
}
}
}, [ref, selected, isMobile]);
}, [ref, selected]);
return (
<Link href={href}>

12
beta/src/components/Layout/Sidebar/SidebarRouteTree.tsx

@ -14,7 +14,7 @@ import {useLayoutEffect} from 'react';
import usePendingRoute from 'hooks/usePendingRoute';
interface SidebarRouteTreeProps {
isMobile?: boolean;
isForceExpanded: boolean;
routeTree: RouteItem;
level?: number;
}
@ -72,7 +72,7 @@ function CollapseWrapper({
}
export function SidebarRouteTree({
isMobile,
isForceExpanded,
routeTree,
level = 0,
}: SidebarRouteTreeProps) {
@ -109,7 +109,7 @@ export function SidebarRouteTree({
return (
<SidebarRouteTree
level={level + 1}
isMobile={isMobile}
isForceExpanded={isForceExpanded}
routeTree={{title, routes}}
/>
);
@ -117,7 +117,7 @@ export function SidebarRouteTree({
// if route has a path and child routes, treat it as an expandable sidebar item
if (routes) {
const isExpanded = isMobile || expanded === path;
const isExpanded = isForceExpanded || expanded === path;
return (
<li key={`${title}-${path}-${level}-heading`}>
<SidebarLink
@ -130,11 +130,11 @@ export function SidebarRouteTree({
wip={wip}
isExpanded={isExpanded}
isBreadcrumb={expandedPath === path}
hideArrow={isMobile}
hideArrow={isForceExpanded}
/>
<CollapseWrapper duration={250} isExpanded={isExpanded}>
<SidebarRouteTree
isMobile={isMobile}
isForceExpanded={isForceExpanded}
routeTree={{title, routes}}
level={level + 1}
/>

1
beta/src/components/Layout/Sidebar/index.tsx

@ -2,7 +2,6 @@
* Copyright (c) Facebook, Inc. and its affiliates.
*/
export {Sidebar} from './Sidebar';
export {SidebarButton} from './SidebarButton';
export {SidebarLink} from './SidebarLink';
export {SidebarRouteTree} from './SidebarRouteTree';

52
beta/src/components/Layout/useMediaQuery.tsx

@ -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};

17
beta/src/components/MDX/Sandpack/CustomPreset.tsx

@ -2,7 +2,6 @@
* Copyright (c) Facebook, Inc. and its affiliates.
*/
import React from 'react';
// @ts-ignore
import {flushSync} from 'react-dom';
import {
useSandpack,
@ -11,7 +10,6 @@ import {
SandpackThemeProvider,
SandpackReactDevTools,
} from '@codesandbox/sandpack-react';
import scrollIntoView from 'scroll-into-view-if-needed';
import cn from 'classnames';
import {IconChevron} from 'components/Icon/IconChevron';
@ -85,11 +83,16 @@ export function CustomPreset({
setIsExpanded(nextIsExpanded);
});
if (!nextIsExpanded && containerRef.current !== null) {
scrollIntoView(containerRef.current, {
scrollMode: 'if-needed',
block: 'nearest',
inline: 'nearest',
});
// @ts-ignore
if (containerRef.current.scrollIntoViewIfNeeded) {
// @ts-ignore
containerRef.current.scrollIntoViewIfNeeded();
} else {
containerRef.current.scrollIntoView({
block: 'nearest',
inline: 'nearest',
});
}
}
}}>
<span className="flex p-2 focus:outline-none text-primary dark:text-primary-dark">

72
beta/src/components/useMenu.tsx

@ -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…
Cancel
Save