beta/src/components/Layout/Sidebar/SidebarRouteTree.tsx (139 lines of code) (raw):

/* * 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 {removeFromLast} from 'utils/removeFromLast'; import {useRouteMeta} from '../useRouteMeta'; import {SidebarLink} from './SidebarLink'; import useCollapse from 'react-collapsed'; import {useLayoutEffect} from 'react'; interface SidebarRouteTreeProps { isMobile?: boolean; routeTree: RouteItem; level?: number; } function CollapseWrapper({ isExpanded, duration, children, }: { isExpanded: boolean; duration: number; children: any; }) { const ref = React.useRef<HTMLDivElement | null>(null); const timeoutRef = React.useRef<number | null>(null); const {getCollapseProps} = useCollapse({ isExpanded, duration, }); // Disable pointer events while animating. const isExpandedRef = React.useRef(isExpanded); if (typeof window !== 'undefined') { // eslint-disable-next-line react-hooks/rules-of-hooks useLayoutEffect(() => { const wasExpanded = isExpandedRef.current; if (wasExpanded === isExpanded) { return; } isExpandedRef.current = isExpanded; if (ref.current !== null) { const node: HTMLDivElement = ref.current; node.style.pointerEvents = 'none'; if (timeoutRef.current !== null) { window.clearTimeout(timeoutRef.current); } timeoutRef.current = window.setTimeout(() => { node.style.pointerEvents = ''; }, duration + 100); } }); } return ( <div ref={ref} className={cn(isExpanded ? 'opacity-100' : 'opacity-50')} style={{ transition: `opacity ${duration}ms ease-in-out`, animation: `nav-fadein ${duration}ms ease-in-out`, }}> <div {...getCollapseProps()}>{children}</div> </div> ); } export function SidebarRouteTree({ isMobile, routeTree, level = 0, }: SidebarRouteTreeProps) { const {breadcrumbs} = useRouteMeta(routeTree); const {pathname} = useRouter(); const slug = pathname; const currentRoutes = routeTree.routes as RouteItem[]; const expandedPath = currentRoutes.reduce( (acc: string | undefined, curr: RouteItem) => { if (acc) return acc; const breadcrumb = breadcrumbs.find((b) => b.path === curr.path); if (breadcrumb) { return curr.path; } if (curr.path === pathname) { return pathname; } return undefined; }, undefined ); const expanded = expandedPath; return ( <ul> {currentRoutes.map(({path, title, routes, heading}) => { const pagePath = path && removeFromLast(path, '.'); const selected = slug === pagePath; // if current route item has no path and children treat it as an API sidebar heading if (!path || !pagePath || heading) { return ( <SidebarRouteTree level={level + 1} isMobile={isMobile} routeTree={{title, routes}} /> ); } // if route has a path and child routes, treat it as an expandable sidebar item if (routes) { const isExpanded = isMobile || expanded === path; return ( <li key={`${title}-${path}-${level}-heading`}> <SidebarLink key={`${title}-${path}-${level}-link`} href={pagePath} selected={selected} level={level} title={title} isExpanded={isExpanded} isBreadcrumb={expandedPath === path} hideArrow={isMobile} /> <CollapseWrapper duration={250} isExpanded={isExpanded}> <SidebarRouteTree isMobile={isMobile} routeTree={{title, routes}} level={level + 1} /> </CollapseWrapper> </li> ); } // if route has a path and no child routes, treat it as a sidebar link return ( <li key={`${title}-${path}-${level}-link`}> <SidebarLink href={pagePath} selected={selected} level={level} title={title} /> </li> ); })} </ul> ); }