Skip to content

Commit c9e2e39

Browse files
authored
[Beta] Refactor navigation logic (#5492)
* Pass route lists explicitly * Inline MarkdownPage into Page * Pass breadcrumbs from above * Remove state from router utils * Pass section from above
1 parent 38bf76a commit c9e2e39

14 files changed

Lines changed: 150 additions & 213 deletions

beta/src/components/Breadcrumbs.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,10 @@
33
*/
44

55
import {Fragment} from 'react';
6-
import {useRouteMeta} from 'components/Layout/useRouteMeta';
76
import Link from 'next/link';
7+
import type {RouteItem} from 'components/Layout/getRouteMeta';
88

9-
function Breadcrumbs() {
10-
const {breadcrumbs} = useRouteMeta();
11-
if (!breadcrumbs) return null;
9+
function Breadcrumbs({breadcrumbs}: {breadcrumbs: RouteItem[]}) {
1210
return (
1311
<div className="flex flex-wrap">
1412
{breadcrumbs.map(

beta/src/components/DocsFooter.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {memo} from 'react';
77
import cn from 'classnames';
88
import {removeFromLast} from 'utils/removeFromLast';
99
import {IconNavArrow} from './Icon/IconNavArrow';
10-
import {RouteMeta} from './Layout/useRouteMeta';
10+
import type {RouteMeta} from './Layout/getRouteMeta';
1111

1212
export type DocsPageFooterProps = Pick<
1313
RouteMeta,

beta/src/components/Layout/MarkdownPage.tsx

Lines changed: 0 additions & 58 deletions
This file was deleted.

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

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,11 @@ import {disableBodyScroll, enableBodyScroll} from 'body-scroll-lock';
1212
import {IconClose} from 'components/Icon/IconClose';
1313
import {IconHamburger} from 'components/Icon/IconHamburger';
1414
import {Search} from 'components/Search';
15-
import {useActiveSection} from 'hooks/useActiveSection';
1615
import {Logo} from '../../Logo';
1716
import {Feedback} from '../Feedback';
1817
import NavLink from './NavLink';
19-
import {SidebarContext} from 'components/Layout/useRouteMeta';
2018
import {SidebarRouteTree} from '../Sidebar/SidebarRouteTree';
21-
import type {RouteItem} from '../useRouteMeta';
19+
import type {RouteItem} from '../getRouteMeta';
2220
import sidebarLearn from '../../../sidebarLearn.json';
2321
import sidebarReference from '../../../sidebarReference.json';
2422

@@ -92,17 +90,22 @@ const lightIcon = (
9290
</svg>
9391
);
9492

95-
export default function Nav() {
93+
export default function Nav({
94+
routeTree,
95+
breadcrumbs,
96+
section,
97+
}: {
98+
routeTree: RouteItem;
99+
breadcrumbs: RouteItem[];
100+
section: 'learn' | 'reference' | 'home';
101+
}) {
96102
const [isOpen, setIsOpen] = useState(false);
97103
const [showFeedback, setShowFeedback] = useState(false);
98104
const scrollParentRef = useRef<HTMLDivElement>(null);
99105
const feedbackAutohideRef = useRef<any>(null);
100-
const section = useActiveSection();
101106
const {asPath} = useRouter();
102107
const feedbackPopupRef = useRef<null | HTMLDivElement>(null);
103108

104-
// In desktop mode, use the route tree for current route.
105-
let routeTree: RouteItem = useContext(SidebarContext);
106109
// In mobile mode, let the user switch tabs there and back without navigating.
107110
// Seed the tab state from the router, but keep it independent.
108111
const [tab, setTab] = useState(section);
@@ -344,6 +347,7 @@ export default function Nav() {
344347
// This avoids unnecessary animations and visual flicker.
345348
key={isOpen ? 'mobile-overlay' : 'desktop-or-hidden'}
346349
routeTree={routeTree}
350+
breadcrumbs={breadcrumbs}
347351
isForceExpanded={isOpen}
348352
/>
349353
</Suspense>

beta/src/components/Layout/Page.tsx

Lines changed: 63 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,52 +6,86 @@ import {Suspense} from 'react';
66
import * as React from 'react';
77
import {useRouter} from 'next/router';
88
import {Nav} from './Nav';
9-
import {RouteItem, SidebarContext} from './useRouteMeta';
10-
import {useActiveSection} from 'hooks/useActiveSection';
119
import {Footer} from './Footer';
1210
import {Toc} from './Toc';
1311
import SocialBanner from '../SocialBanner';
12+
import {DocsPageFooter} from 'components/DocsFooter';
13+
import {Seo} from 'components/Seo';
14+
import PageHeading from 'components/PageHeading';
15+
import {getRouteMeta} from './getRouteMeta';
16+
import {TocContext} from '../MDX/TocContext';
1417
import sidebarLearn from '../../sidebarLearn.json';
1518
import sidebarReference from '../../sidebarReference.json';
1619
import type {TocItem} from 'components/MDX/TocContext';
20+
import type {RouteItem} from 'components/Layout/getRouteMeta';
21+
22+
import(/* webpackPrefetch: true */ '../MDX/CodeBlock/CodeBlock');
1723

1824
interface PageProps {
1925
children: React.ReactNode;
2026
toc: Array<TocItem>;
27+
routeTree: RouteItem;
28+
meta: {title?: string; description?: string};
29+
section: 'learn' | 'reference' | 'home';
2130
}
2231

23-
export function Page({children, toc}: PageProps) {
32+
export function Page({children, toc, routeTree, meta, section}: PageProps) {
2433
const {asPath} = useRouter();
25-
const section = useActiveSection();
26-
let routeTree = sidebarLearn as RouteItem;
27-
switch (section) {
28-
case 'reference':
29-
routeTree = sidebarReference as RouteItem;
30-
break;
31-
}
34+
const cleanedPath = asPath.split(/[\?\#]/)[0];
35+
const {route, nextRoute, prevRoute, breadcrumbs} = getRouteMeta(
36+
cleanedPath,
37+
routeTree
38+
);
39+
const title = meta.title || route?.title || '';
40+
const description = meta.description || route?.description || '';
41+
const isHomePage = cleanedPath === '/';
3242
return (
3343
<>
3444
<SocialBanner />
35-
<SidebarContext.Provider value={routeTree}>
36-
<div className="grid grid-cols-only-content lg:grid-cols-sidebar-content 2xl:grid-cols-sidebar-content-toc">
37-
<div className="fixed lg:sticky top-0 left-0 right-0 py-0 shadow lg:shadow-none z-50">
38-
<Nav />
39-
</div>
40-
{/* No fallback UI so need to be careful not to suspend directly inside. */}
41-
<Suspense fallback={null}>
42-
<main className="min-w-0">
43-
<div className="lg:hidden h-16 mb-2" />
44-
<article className="break-words" key={asPath}>
45-
{children}
46-
</article>
47-
<Footer />
48-
</main>
49-
</Suspense>
50-
<div className="hidden lg:max-w-xs 2xl:block">
51-
{toc.length > 0 && <Toc headings={toc} key={asPath} />}
52-
</div>
45+
<div className="grid grid-cols-only-content lg:grid-cols-sidebar-content 2xl:grid-cols-sidebar-content-toc">
46+
<div className="fixed lg:sticky top-0 left-0 right-0 py-0 shadow lg:shadow-none z-50">
47+
<Nav
48+
routeTree={routeTree}
49+
breadcrumbs={breadcrumbs}
50+
section={section}
51+
/>
52+
</div>
53+
{/* No fallback UI so need to be careful not to suspend directly inside. */}
54+
<Suspense fallback={null}>
55+
<main className="min-w-0">
56+
<div className="lg:hidden h-16 mb-2" />
57+
<article className="break-words" key={asPath}>
58+
<div className="pl-0">
59+
<Seo title={title} />
60+
{!isHomePage && (
61+
<PageHeading
62+
title={title}
63+
description={description}
64+
tags={route?.tags}
65+
breadcrumbs={breadcrumbs}
66+
/>
67+
)}
68+
<div className="px-5 sm:px-12">
69+
<div className="max-w-7xl mx-auto">
70+
<TocContext.Provider value={toc}>
71+
{children}
72+
</TocContext.Provider>
73+
</div>
74+
<DocsPageFooter
75+
route={route}
76+
nextRoute={nextRoute}
77+
prevRoute={prevRoute}
78+
/>
79+
</div>
80+
</div>
81+
</article>
82+
<Footer />
83+
</main>
84+
</Suspense>
85+
<div className="hidden lg:max-w-xs 2xl:block">
86+
{toc.length > 0 && <Toc headings={toc} key={asPath} />}
5387
</div>
54-
</SidebarContext.Provider>
88+
</div>
5589
</>
5690
);
5791
}

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

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@
55
import {useRef, useLayoutEffect, Fragment} from 'react';
66

77
import cn from 'classnames';
8-
import {RouteItem} from 'components/Layout/useRouteMeta';
98
import {useRouter} from 'next/router';
109
import {removeFromLast} from 'utils/removeFromLast';
11-
import {useRouteMeta} from '../useRouteMeta';
1210
import {SidebarLink} from './SidebarLink';
1311
import useCollapse from 'react-collapsed';
1412
import usePendingRoute from 'hooks/usePendingRoute';
13+
import type {RouteItem} from 'components/Layout/getRouteMeta';
1514

1615
interface SidebarRouteTreeProps {
1716
isForceExpanded: boolean;
17+
breadcrumbs: RouteItem[];
1818
routeTree: RouteItem;
1919
level?: number;
2020
}
@@ -72,31 +72,13 @@ function CollapseWrapper({
7272

7373
export function SidebarRouteTree({
7474
isForceExpanded,
75+
breadcrumbs,
7576
routeTree,
7677
level = 0,
7778
}: SidebarRouteTreeProps) {
78-
const {breadcrumbs} = useRouteMeta(routeTree);
79-
const cleanedPath = useRouter().asPath.split(/[\?\#]/)[0];
79+
const slug = useRouter().asPath.split(/[\?\#]/)[0];
8080
const pendingRoute = usePendingRoute();
81-
82-
const slug = cleanedPath;
8381
const currentRoutes = routeTree.routes as RouteItem[];
84-
const expandedPath = currentRoutes.reduce(
85-
(acc: string | undefined, curr: RouteItem) => {
86-
if (acc) return acc;
87-
const breadcrumb = breadcrumbs.find((b) => b.path === curr.path);
88-
if (breadcrumb) {
89-
return curr.path;
90-
}
91-
if (curr.path === cleanedPath) {
92-
return cleanedPath;
93-
}
94-
return undefined;
95-
},
96-
undefined
97-
);
98-
99-
const expanded = expandedPath;
10082
return (
10183
<ul>
10284
{currentRoutes.map(
@@ -106,7 +88,6 @@ export function SidebarRouteTree({
10688
) => {
10789
const pagePath = path && removeFromLast(path, '.');
10890
const selected = slug === pagePath;
109-
11091
let listItem = null;
11192
if (!path || !pagePath || heading) {
11293
// if current route item has no path and children treat it as an API sidebar heading
@@ -115,11 +96,15 @@ export function SidebarRouteTree({
11596
level={level + 1}
11697
isForceExpanded={isForceExpanded}
11798
routeTree={{title, routes}}
99+
breadcrumbs={[]}
118100
/>
119101
);
120102
} else if (routes) {
121103
// if route has a path and child routes, treat it as an expandable sidebar item
122-
const isExpanded = isForceExpanded || expanded === path;
104+
const isBreadcrumb =
105+
breadcrumbs.length > 1 &&
106+
breadcrumbs[breadcrumbs.length - 1].path === path;
107+
const isExpanded = isForceExpanded || isBreadcrumb || selected;
123108
listItem = (
124109
<li key={`${title}-${path}-${level}-heading`}>
125110
<SidebarLink
@@ -131,13 +116,14 @@ export function SidebarRouteTree({
131116
title={title}
132117
wip={wip}
133118
isExpanded={isExpanded}
134-
isBreadcrumb={expandedPath === path}
119+
isBreadcrumb={isBreadcrumb}
135120
hideArrow={isForceExpanded}
136121
/>
137122
<CollapseWrapper duration={250} isExpanded={isExpanded}>
138123
<SidebarRouteTree
139124
isForceExpanded={isForceExpanded}
140125
routeTree={{title, routes}}
126+
breadcrumbs={breadcrumbs}
141127
level={level + 1}
142128
/>
143129
</CollapseWrapper>

beta/src/components/Layout/useRouteMeta.tsx renamed to beta/src/components/Layout/getRouteMeta.tsx

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22
* Copyright (c) Facebook, Inc. and its affiliates.
33
*/
44

5-
import {useContext, createContext} from 'react';
6-
import {useRouter} from 'next/router';
7-
85
/**
96
* While Next.js provides file-based routing, we still need to construct
107
* a sidebar for navigation and provide each markdown page
@@ -57,30 +54,19 @@ export interface RouteMeta {
5754
breadcrumbs?: RouteItem[];
5855
}
5956

60-
export function useRouteMeta(rootRoute?: RouteItem) {
61-
const sidebarContext = useContext(SidebarContext);
62-
const routeTree = rootRoute || sidebarContext;
63-
const router = useRouter();
64-
if (router.pathname === '/404') {
65-
return {
66-
breadcrumbs: [],
67-
};
68-
}
69-
const cleanedPath = router.asPath.split(/[\?\#]/)[0];
57+
export function getRouteMeta(cleanedPath: string, routeTree: RouteItem) {
7058
const breadcrumbs = getBreadcrumbs(cleanedPath, routeTree);
7159
return {
72-
...getRouteMeta(cleanedPath, routeTree),
60+
...buildRouteMeta(cleanedPath, routeTree, {}),
7361
breadcrumbs: breadcrumbs.length > 0 ? breadcrumbs : [routeTree],
7462
};
7563
}
7664

77-
export const SidebarContext = createContext<RouteItem>({title: 'root'});
78-
7965
// Performs a depth-first search to find the current route and its previous/next route
80-
function getRouteMeta(
66+
function buildRouteMeta(
8167
searchPath: string,
8268
currentRoute: RouteItem,
83-
ctx: RouteMeta = {}
69+
ctx: RouteMeta
8470
): RouteMeta {
8571
const {routes} = currentRoute;
8672

@@ -101,7 +87,7 @@ function getRouteMeta(
10187
}
10288

10389
for (const route of routes) {
104-
getRouteMeta(searchPath, route, ctx);
90+
buildRouteMeta(searchPath, route, ctx);
10591
}
10692

10793
return ctx;

0 commit comments

Comments
 (0)