diff --git a/beta/package.json b/beta/package.json index f38255fc..ee9d7798 100644 --- a/beta/package.json +++ b/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", diff --git a/beta/src/components/Layout/Nav/MobileNav.tsx b/beta/src/components/Layout/Nav/MobileNav.tsx deleted file mode 100644 index 351ae879..00000000 --- a/beta/src/components/Layout/Nav/MobileNav.tsx +++ /dev/null @@ -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 ( - <> -
- setTab('home')}> - Home - - setTab('learn')}> - Learn - - setTab('apis')}> - API - -
- {/* No fallback UI so need to be careful not to suspend directly inside. */} - - - - - ); -} - -function TabButton({ - children, - onClick, - isActive, -}: { - children: any; - onClick: (event: React.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 ( - - ); -} diff --git a/beta/src/components/Layout/Nav/Nav.tsx b/beta/src/components/Layout/Nav/Nav.tsx index 30b6a06e..e682dfff 100644 --- a/beta/src/components/Layout/Nav/Nav.tsx +++ b/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(null); const feedbackAutohideRef = React.useRef(null); const section = useActiveSection(); + const {asPath} = useRouter(); const feedbackPopupRef = React.useRef(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 ( - + + + ); +} + +function TabButton({ + children, + onClick, + isActive, +}: { + children: any; + onClick: (event: React.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 ( + ); } diff --git a/beta/src/components/Layout/Page.tsx b/beta/src/components/Layout/Page.tsx index bbd4bfa6..17f4d698 100644 --- a/beta/src/components/Layout/Page.tsx +++ b/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 ( <> - - -
-
-
+ +
+
+
- {/* No fallback UI so need to be careful not to suspend directly inside. */} - -
-
-
-
- {children} -
-
-
-
+ {/* No fallback UI so need to be careful not to suspend directly inside. */} + +
+
+
+
+ {children} +
+
+
- -
- - +
+
+
+
); } diff --git a/beta/src/components/Layout/Sidebar/Sidebar.tsx b/beta/src/components/Layout/Sidebar/Sidebar.tsx deleted file mode 100644 index 13b999f8..00000000 --- a/beta/src/components/Layout/Sidebar/Sidebar.tsx +++ /dev/null @@ -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 ( - - ); -} diff --git a/beta/src/components/Layout/Sidebar/SidebarLink.tsx b/beta/src/components/Layout/Sidebar/SidebarLink.tsx index b3744591..0124acba 100644 --- a/beta/src/components/Layout/Sidebar/SidebarLink.tsx +++ b/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(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 ( diff --git a/beta/src/components/Layout/Sidebar/SidebarRouteTree.tsx b/beta/src/components/Layout/Sidebar/SidebarRouteTree.tsx index 9f97413a..a72f21df 100644 --- a/beta/src/components/Layout/Sidebar/SidebarRouteTree.tsx +++ b/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 ( ); @@ -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 (
  • diff --git a/beta/src/components/Layout/Sidebar/index.tsx b/beta/src/components/Layout/Sidebar/index.tsx index a2204a50..d0e29154 100644 --- a/beta/src/components/Layout/Sidebar/index.tsx +++ b/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'; diff --git a/beta/src/components/Layout/useMediaQuery.tsx b/beta/src/components/Layout/useMediaQuery.tsx deleted file mode 100644 index e05a2aeb..00000000 --- a/beta/src/components/Layout/useMediaQuery.tsx +++ /dev/null @@ -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}; diff --git a/beta/src/components/MDX/Sandpack/CustomPreset.tsx b/beta/src/components/MDX/Sandpack/CustomPreset.tsx index bb0f1c67..5960b9c2 100644 --- a/beta/src/components/MDX/Sandpack/CustomPreset.tsx +++ b/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', + }); + } } }}> diff --git a/beta/src/components/useMenu.tsx b/beta/src/components/useMenu.tsx deleted file mode 100644 index 4f551947..00000000 --- a/beta/src/components/useMenu.tsx +++ /dev/null @@ -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(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>( - {} as ReturnType -); - -export function MenuProvider(props: {children: React.ReactNode}) { - return ; -}