From df1d688fcb4fd2fd5bec6ba20b10d22607c20260 Mon Sep 17 00:00:00 2001 From: Andrew Jiang Date: Fri, 19 Jan 2024 18:56:33 -0500 Subject: [PATCH] fix: make navigation less finicky (#374) --- packages/ui/app/src/App.tsx | 2 +- .../BottomNavigationButton.tsx | 16 ++------ .../ui/app/src/docs-context/DocsContext.ts | 15 +++++++- .../src/docs-context/DocsContextProvider.tsx | 24 +++++++++--- .../navigation-context/NavigationContext.ts | 37 ++----------------- .../NavigationContextProvider.tsx | 36 +++++------------- packages/ui/app/src/search/SearchHit.tsx | 8 ++-- packages/ui/app/src/search/SearchHits.tsx | 8 ++-- 8 files changed, 60 insertions(+), 86 deletions(-) diff --git a/packages/ui/app/src/App.tsx b/packages/ui/app/src/App.tsx index f33d68a16d..ebea9b441c 100644 --- a/packages/ui/app/src/App.tsx +++ b/packages/ui/app/src/App.tsx @@ -38,7 +38,7 @@ export const App: React.FC = ({ docs, resolvedPath }) => { (children, Context) => ( {children} ), - + diff --git a/packages/ui/app/src/bottom-navigation-buttons/BottomNavigationButton.tsx b/packages/ui/app/src/bottom-navigation-buttons/BottomNavigationButton.tsx index 9f5f1a0c78..c42fa484b2 100644 --- a/packages/ui/app/src/bottom-navigation-buttons/BottomNavigationButton.tsx +++ b/packages/ui/app/src/bottom-navigation-buttons/BottomNavigationButton.tsx @@ -4,8 +4,8 @@ import { type DocsNode } from "@fern-api/fdr-sdk"; import { getFullSlugForNavigatable } from "@fern-ui/app-utils"; import { assertNever } from "@fern-ui/core-utils"; import Link from "next/link"; -import { useCallback, useMemo } from "react"; -import { useNavigationContext } from "../navigation-context"; +import { useMemo } from "react"; +import { useDocsContext } from "../docs-context/useDocsContext"; export declare namespace BottomNavigationButton { export interface Props { @@ -15,7 +15,7 @@ export declare namespace BottomNavigationButton { } export const BottomNavigationButton: React.FC = ({ docsNode, direction }) => { - const { navigateToPath, resolver, basePath } = useNavigationContext(); + const { pathResolver, basePath } = useDocsContext(); // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint const visitDirection = ({ previous, next }: { previous: T; next: T }): T => { switch (direction) { @@ -28,7 +28,7 @@ export const BottomNavigationButton: React.FC = ({ } }; - const navigatable = useMemo(() => resolver.resolveNavigatable(docsNode), [resolver, docsNode]); + const navigatable = useMemo(() => pathResolver.resolveNavigatable(docsNode), [pathResolver, docsNode]); const iconName = visitDirection({ previous: IconNames.CHEVRON_LEFT, @@ -37,13 +37,6 @@ export const BottomNavigationButton: React.FC = ({ const iconElement = ; - const onClick = useCallback(() => { - if (navigatable != null) { - const fullSlug = getFullSlugForNavigatable(navigatable, { omitDefault: true, basePath }); - navigateToPath(fullSlug); - } - }, [navigatable, basePath, navigateToPath]); - const text = useMemo(() => { switch (docsNode.type) { case "docs-section": @@ -72,7 +65,6 @@ export const BottomNavigationButton: React.FC = ({ return ( {visitDirection({ diff --git a/packages/ui/app/src/docs-context/DocsContext.ts b/packages/ui/app/src/docs-context/DocsContext.ts index 9e52c182c8..27ff838ed9 100644 --- a/packages/ui/app/src/docs-context/DocsContext.ts +++ b/packages/ui/app/src/docs-context/DocsContext.ts @@ -1,9 +1,18 @@ -import { APIV1Read, DocsV1Read, FdrAPI } from "@fern-api/fdr-sdk"; +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { APIV1Read, DocsDefinitionSummary, DocsV1Read, FdrAPI, PathResolver } from "@fern-api/fdr-sdk"; +import { DefinitionObjectFactory } from "@fern-ui/app-utils"; import React from "react"; +const EMPTY_DEFINITION = DefinitionObjectFactory.createDocsDefinition(); +const EMPTY_DEFINITION_SUMMARY: DocsDefinitionSummary = { + apis: EMPTY_DEFINITION.apis, + docsConfig: EMPTY_DEFINITION.config, +}; + export const DocsContext = React.createContext({ domain: "app.buildwithfern.com", - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + basePath: undefined, + pathResolver: new PathResolver({ definition: EMPTY_DEFINITION_SUMMARY }), docsDefinition: undefined!, resolveApi: () => undefined, resolvePage: () => undefined, @@ -12,7 +21,9 @@ export const DocsContext = React.createContext({ export interface DocsContextValue { domain: string; + basePath: string | undefined; docsDefinition: DocsV1Read.DocsDefinition; + pathResolver: PathResolver; resolveApi: (apiId: FdrAPI.ApiDefinitionId) => APIV1Read.ApiDefinition | undefined; resolvePage: (pageId: DocsV1Read.PageId) => DocsV1Read.PageContent | undefined; diff --git a/packages/ui/app/src/docs-context/DocsContextProvider.tsx b/packages/ui/app/src/docs-context/DocsContextProvider.tsx index 007d930584..0bba5386d0 100644 --- a/packages/ui/app/src/docs-context/DocsContextProvider.tsx +++ b/packages/ui/app/src/docs-context/DocsContextProvider.tsx @@ -1,17 +1,17 @@ -import { APIV1Read, DocsV1Read, FdrAPI } from "@fern-api/fdr-sdk"; +import { APIV1Read, DocsV1Read, DocsV2Read, FdrAPI, PathResolver } from "@fern-api/fdr-sdk"; import { useDeepCompareMemoize } from "@fern-ui/react-commons"; -import { PropsWithChildren, useCallback } from "react"; +import { PropsWithChildren, useCallback, useMemo } from "react"; import { DocsContext } from "./DocsContext"; export declare namespace DocsContextProvider { export type Props = PropsWithChildren<{ docsDefinition: DocsV1Read.DocsDefinition; - domain: string; + baseUrl: DocsV2Read.BaseUrl; }>; } export const DocsContextProvider: React.FC = ({ - domain, + baseUrl, docsDefinition: unmemoizedDocsDefinition, children, }) => { @@ -53,11 +53,25 @@ export const DocsContextProvider: React.FC = ({ [docsDefinition.files] ); + const pathResolver = useMemo( + () => + new PathResolver({ + definition: { + apis: docsDefinition.apis, + docsConfig: docsDefinition.config, + basePath: baseUrl.basePath, + }, + }), + [baseUrl.basePath, docsDefinition.apis, docsDefinition.config] + ); + return ( ({ basePath: undefined, justNavigated: false, - activeNavigatable: NodeFactory.createPage({ - slug: "", - leadingSlug: "", - page: { - id: "", - title: "", - urlSlug: "", - }, - section: null, - context: { - type: "unversioned-untabbed", - root: NodeFactory.createRoot(EMPTY_DEFINITION_SUMMARY), - navigationConfig: { items: [] }, - version: null, - tab: null, - }, - }), + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + activeNavigatable: undefined!, activeNavigatableNeighbors: { previous: null, next: null, @@ -43,7 +16,6 @@ export const NavigationContext = React.createContext({ userIsScrolling: () => false, onScrollToPath: noop, observeDocContent: noop, - resolver: new PathResolver({ definition: EMPTY_DEFINITION_SUMMARY }), registerScrolledToPathListener: () => noop, resolvedPath: { type: "custom-markdown-page", @@ -68,7 +40,6 @@ export interface NavigationContextValue { userIsScrolling: () => boolean; onScrollToPath: (slug: string) => void; observeDocContent: (element: HTMLDivElement) => void; - resolver: PathResolver; registerScrolledToPathListener: (slugWithVersion: string, listener: () => void) => () => void; resolvedPath: ResolvedPath; // the initial path that was hard-navigated hydrated: boolean; diff --git a/packages/ui/app/src/navigation-context/NavigationContextProvider.tsx b/packages/ui/app/src/navigation-context/NavigationContextProvider.tsx index 9cf05954d7..cfc58b7848 100644 --- a/packages/ui/app/src/navigation-context/NavigationContextProvider.tsx +++ b/packages/ui/app/src/navigation-context/NavigationContextProvider.tsx @@ -1,4 +1,3 @@ -import { FdrAPI, PathResolver } from "@fern-api/fdr-sdk"; import { getFullSlugForNavigatable, type ResolvedPath } from "@fern-ui/app-utils"; import { useBooleanState, useEventCallback } from "@fern-ui/react-commons"; import { debounce } from "lodash-es"; @@ -22,7 +21,7 @@ export const NavigationContextProvider: React.FC { - const { docsDefinition } = useDocsContext(); + const { docsDefinition, pathResolver } = useDocsContext(); const router = useRouter(); const userIsScrolling = useRef(false); const resolvedRoute = getRouteForResolvedPath({ @@ -30,34 +29,22 @@ export const NavigationContextProvider: React.FC(resolvedRoute); - type ApiDefinition = FdrAPI.api.v1.read.ApiDefinition; - const resolver = useMemo( - () => - new PathResolver({ - definition: { - apis: docsDefinition.apis as Record, - docsConfig: docsDefinition.config, - basePath, - }, - }), - [basePath, docsDefinition.apis, docsDefinition.config] - ); const resolvedNavigatable = useMemo(() => { - const node = resolver.resolveNavigatable(resolvedPath.fullSlug); + const node = pathResolver.resolveNavigatable(resolvedPath.fullSlug); if (node == null) { throw new Error( `Implementation Error. Cannot resolve navigatable for resolved path ${resolvedPath.fullSlug}` ); } return node; - }, [resolver, resolvedPath.fullSlug]); + }, [pathResolver, resolvedPath.fullSlug]); const [activeNavigatable, setActiveNavigatable] = useState(resolvedNavigatable); const activeNavigatableNeighbors = useMemo(() => { - return resolver.getNeighborsForNavigatable(activeNavigatable); - }, [resolver, activeNavigatable]); + return pathResolver.getNeighborsForNavigatable(activeNavigatable); + }, [pathResolver, activeNavigatable]); const selectedSlug = getFullSlugForNavigatable(activeNavigatable, { omitDefault: true, basePath }); @@ -139,7 +126,7 @@ export const NavigationContextProvider: React.FC { justNavigated.current = true; - const navigatable = resolver.resolveNavigatable(fullSlug); + const navigatable = pathResolver.resolveNavigatable(fullSlug); navigateToRoute.current(`/${fullSlug}`, undefined); if (navigatable != null) { setActiveNavigatable(navigatable); @@ -171,13 +158,9 @@ export const NavigationContextProvider: React.FC { navigateToPath(route.substring(1)); }; - router.events.on("routeChangeStart", handleRouteChangeStart); - router.events.on("hashChangeStart", handleRouteChangeStart); router.events.on("routeChangeComplete", handleRouteChangeStart); router.events.on("hashChangeComplete", handleRouteChangeStart); return () => { - router.events.off("routeChangeStart", handleRouteChangeStart); - router.events.off("hashChangeStart", handleRouteChangeStart); router.events.off("routeChangeComplete", handleRouteChangeStart); router.events.off("hashChangeComplete", handleRouteChangeStart); }; @@ -186,14 +169,14 @@ export const NavigationContextProvider: React.FC { router.beforePopState(({ as }) => { const slugCandidate = as.substring(1, as.length); - const previousNavigatable = resolver.resolveNavigatable(slugCandidate); + const previousNavigatable = pathResolver.resolveNavigatable(slugCandidate); if (previousNavigatable != null) { const fullSlug = getFullSlugForNavigatable(previousNavigatable, { basePath }); navigateToPath(fullSlug); } return true; }); - }, [router, navigateToPath, docsDefinition, resolver, basePath]); + }, [router, navigateToPath, docsDefinition, pathResolver, basePath]); const hydrated = useBooleanState(false); useEffect(() => { @@ -211,7 +194,6 @@ export const NavigationContextProvider: React.FC userIsScrolling.current, onScrollToPath, observeDocContent, - resolver, registerScrolledToPathListener: scrollToPathListeners.registerListener, activeNavigatableNeighbors, resolvedPath, diff --git a/packages/ui/app/src/search/SearchHit.tsx b/packages/ui/app/src/search/SearchHit.tsx index 1273ace05a..badb1cdc31 100644 --- a/packages/ui/app/src/search/SearchHit.tsx +++ b/packages/ui/app/src/search/SearchHit.tsx @@ -3,6 +3,7 @@ import { visitDiscriminatedUnion } from "@fern-ui/core-utils"; import classNames from "classnames"; import Link from "next/link"; import { useCallback, useMemo } from "react"; +import { useDocsContext } from "../docs-context/useDocsContext"; import { useNavigationContext } from "../navigation-context"; import { useSearchContext } from "../search-context/useSearchContext"; import { EndpointRecord } from "./content/EndpointRecord"; @@ -23,17 +24,18 @@ export declare namespace SearchHit { } export const SearchHit: React.FC = ({ setRef, hit, isHovered, onMouseEnter, onMouseLeave }) => { - const { navigateToPath, resolver, basePath } = useNavigationContext(); + const { pathResolver } = useDocsContext(); + const { navigateToPath, basePath } = useNavigationContext(); const { closeSearchDialog } = useSearchContext(); const fullPath = useMemo(() => { const path = getFullPathForSearchRecord(hit, basePath); - const navigatable = resolver.resolveNavigatable(path); + const navigatable = pathResolver.resolveNavigatable(path); if (navigatable == null) { return basePath?.slice(1) ?? ""; } return getFullSlugForNavigatable(navigatable, { omitDefault: true, basePath }); - }, [hit, resolver, basePath]); + }, [hit, pathResolver, basePath]); const handleClick = useCallback(() => { closeSearchDialog(); diff --git a/packages/ui/app/src/search/SearchHits.tsx b/packages/ui/app/src/search/SearchHits.tsx index 9813cf3d6e..967b441a0d 100644 --- a/packages/ui/app/src/search/SearchHits.tsx +++ b/packages/ui/app/src/search/SearchHits.tsx @@ -7,6 +7,7 @@ import { Hit } from "instantsearch.js"; import { useRouter } from "next/router"; import React, { PropsWithChildren, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useInfiniteHits, useInstantSearch } from "react-instantsearch-hooks-web"; +import { useDocsContext } from "../docs-context/useDocsContext"; import { useNavigationContext } from "../navigation-context"; import { useSearchContext } from "../search-context/useSearchContext"; import { SearchHit } from "./SearchHit"; @@ -20,7 +21,8 @@ export const EmptyStateView: React.FC = ({ children }) => { }; export const SearchHits: React.FC = () => { - const { resolver, navigateToPath, basePath } = useNavigationContext(); + const { pathResolver, basePath } = useDocsContext(); + const { navigateToPath } = useNavigationContext(); const { closeSearchDialog } = useSearchContext(); const { hits } = useInfiniteHits(); const search = useInstantSearch(); @@ -31,13 +33,13 @@ export const SearchHits: React.FC = () => { const getFullPathForHit = useCallback( (hit: Hit) => { const path = getFullPathForSearchRecord(hit, basePath); - const navigatable = resolver.resolveNavigatable(path); + const navigatable = pathResolver.resolveNavigatable(path); if (navigatable == null) { return basePath?.slice(1) ?? ""; } return getFullSlugForNavigatable(navigatable, { omitDefault: true, basePath }); }, - [basePath, resolver] + [basePath, pathResolver] ); const refs = useRef(new Map());