1import { css } from '@emotion/react'; 2import { theme } from '@expo/styleguide'; 3import { useRouter } from 'next/router'; 4import React, { ComponentType, useMemo } from 'react'; 5 6import { ApiVersionSelect } from './ApiVersionSelect'; 7import { GroupList } from './GroupList'; 8import { PageLink } from './PageLink'; 9import { SectionList } from './SectionList'; 10import { NavigationNode, NavigationRenderProps, NavigationType } from './types'; 11 12import { LayoutScroll, usePersistScroll } from '~/ui/components/Layout'; 13 14export type NavigationProps = { 15 /** The tree of navigation nodes to render in the sidebar */ 16 routes: NavigationNode[]; 17}; 18 19export function Navigation({ routes }: NavigationProps) { 20 const router = useRouter(); 21 const activeRoutes = useMemo(() => findActiveRoute(routes, router.pathname), [router.pathname]); 22 const persistScroll = usePersistScroll('navigation'); 23 24 return ( 25 <nav css={navigationStyle}> 26 <LayoutScroll {...persistScroll}> 27 <ApiVersionSelect /> 28 {routes.map(route => navigationRenderer(route, activeRoutes))} 29 </LayoutScroll> 30 </nav> 31 ); 32} 33 34const navigationStyle = css({ 35 width: 280, 36 height: '100%', 37 backgroundColor: theme.background.secondary, 38 '[data-expo-theme="dark"] &': { 39 backgroundColor: theme.background.default, 40 }, 41}); 42 43const renderers: Record<NavigationType, ComponentType<NavigationRenderProps>> = { 44 section: SectionList, 45 group: GroupList, 46 page: PageLink, 47}; 48 49function navigationRenderer( 50 route: NavigationNode, 51 activeRoutes: Record<NavigationType, NavigationNode | null> 52) { 53 if (route.hidden) return null; 54 const Component = renderers[route.type]; 55 const routeKey = `${route.type}-${route.name}`; 56 const isActive = activeRoutes[route.type] === route; 57 const hasChildren = route.type !== 'page' && route.children.length; 58 return ( 59 <Component key={routeKey} route={route} isActive={isActive}> 60 {hasChildren && route.children.map(nested => navigationRenderer(nested, activeRoutes))} 61 </Component> 62 ); 63} 64 65/** 66 * Find the active routes by pathname, and do it once. 67 * This will iterate the tree and find the nodes which are "active". 68 * - Page -> if the pathname matches the page's href property 69 * - Group -> if the group contains an active page 70 * - Section -> if the section contains an active group or page 71 */ 72export function findActiveRoute(routes: NavigationNode[], pathname: string) { 73 const activeRoutes: Record<NavigationType, NavigationNode | null> = { 74 page: null, 75 group: null, 76 section: null, 77 }; 78 79 for (const route of routes) { 80 // Try to exit early on hidden routes 81 if (route.hidden) continue; 82 83 switch (route.type) { 84 case 'page': 85 if (route.href === pathname) { 86 activeRoutes.page = route; 87 break; 88 } 89 break; 90 91 case 'group': 92 { 93 const nestedActiveRoutes = findActiveRoute(route.children, pathname); 94 if (nestedActiveRoutes.page) { 95 activeRoutes.page = nestedActiveRoutes.page; 96 activeRoutes.group = route; 97 break; 98 } 99 } 100 break; 101 102 case 'section': 103 { 104 const nestedActiveRoutes = findActiveRoute(route.children, pathname); 105 if (nestedActiveRoutes.group || nestedActiveRoutes.page) { 106 activeRoutes.page = nestedActiveRoutes.page; 107 activeRoutes.group = nestedActiveRoutes.group; 108 activeRoutes.section = route; 109 break; 110 } 111 } 112 break; 113 } 114 } 115 116 return activeRoutes; 117} 118