Skip to content

Commit

Permalink
fix: make navigation less finicky (#374)
Browse files Browse the repository at this point in the history
  • Loading branch information
abvthecity authored Jan 19, 2024
1 parent 1376f1c commit df1d688
Show file tree
Hide file tree
Showing 8 changed files with 60 additions and 86 deletions.
2 changes: 1 addition & 1 deletion packages/ui/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const App: React.FC<App.Props> = ({ docs, resolvedPath }) => {
(children, Context) => (
<Context>{children}</Context>
),
<DocsContextProvider docsDefinition={docs.definition} domain={docs.baseUrl.domain}>
<DocsContextProvider docsDefinition={docs.definition} baseUrl={docs.baseUrl}>
<NavigationContextProvider resolvedPath={resolvedPath} basePath={docs.baseUrl.basePath}>
<Docs />
</NavigationContextProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -15,7 +15,7 @@ export declare namespace BottomNavigationButton {
}

export const BottomNavigationButton: React.FC<BottomNavigationButton.Props> = ({ docsNode, direction }) => {
const { navigateToPath, resolver, basePath } = useNavigationContext();
const { pathResolver, basePath } = useDocsContext();
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint
const visitDirection = <T extends unknown>({ previous, next }: { previous: T; next: T }): T => {
switch (direction) {
Expand All @@ -28,7 +28,7 @@ export const BottomNavigationButton: React.FC<BottomNavigationButton.Props> = ({
}
};

const navigatable = useMemo(() => resolver.resolveNavigatable(docsNode), [resolver, docsNode]);
const navigatable = useMemo(() => pathResolver.resolveNavigatable(docsNode), [pathResolver, docsNode]);

const iconName = visitDirection({
previous: IconNames.CHEVRON_LEFT,
Expand All @@ -37,13 +37,6 @@ export const BottomNavigationButton: React.FC<BottomNavigationButton.Props> = ({

const iconElement = <Icon icon={iconName} />;

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":
Expand Down Expand Up @@ -72,7 +65,6 @@ export const BottomNavigationButton: React.FC<BottomNavigationButton.Props> = ({
return (
<Link
className="text-accent-primary/80 dark:text-accent-primary-dark/80 hover:text-accent-primary hover:dark:text-accent-primary-dark flex cursor-pointer items-center gap-2 rounded text-sm !no-underline transition"
onClick={onClick}
href={`/${fullSlug}`}
>
{visitDirection({
Expand Down
15 changes: 13 additions & 2 deletions packages/ui/app/src/docs-context/DocsContext.ts
Original file line number Diff line number Diff line change
@@ -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<DocsContextValue>({
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,
Expand All @@ -12,7 +21,9 @@ export const DocsContext = React.createContext<DocsContextValue>({

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;
Expand Down
24 changes: 19 additions & 5 deletions packages/ui/app/src/docs-context/DocsContextProvider.tsx
Original file line number Diff line number Diff line change
@@ -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<DocsContextProvider.Props> = ({
domain,
baseUrl,
docsDefinition: unmemoizedDocsDefinition,
children,
}) => {
Expand Down Expand Up @@ -53,11 +53,25 @@ export const DocsContextProvider: React.FC<DocsContextProvider.Props> = ({
[docsDefinition.files]
);

const pathResolver = useMemo(
() =>
new PathResolver({
definition: {
apis: docsDefinition.apis,
docsConfig: docsDefinition.config,
basePath: baseUrl.basePath,
},
}),
[baseUrl.basePath, docsDefinition.apis, docsDefinition.config]
);

return (
<DocsContext.Provider
value={{
domain,
domain: baseUrl.domain,
basePath: baseUrl.basePath,
docsDefinition,
pathResolver,
resolveApi,
resolvePage,
resolveFile,
Expand Down
37 changes: 4 additions & 33 deletions packages/ui/app/src/navigation-context/NavigationContext.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,13 @@
import {
NodeFactory,
PathResolver,
type DocsDefinitionSummary,
type NavigatableDocsNode,
type NodeNeighbors,
} from "@fern-api/fdr-sdk";
import { DefinitionObjectFactory, type ResolvedPath } from "@fern-ui/app-utils";
import { type NavigatableDocsNode, type NodeNeighbors } from "@fern-api/fdr-sdk";
import { type ResolvedPath } from "@fern-ui/app-utils";
import { noop } from "@fern-ui/core-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 NavigationContext = React.createContext<NavigationContextValue>({
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,
Expand All @@ -43,7 +16,6 @@ export const NavigationContext = React.createContext<NavigationContextValue>({
userIsScrolling: () => false,
onScrollToPath: noop,
observeDocContent: noop,
resolver: new PathResolver({ definition: EMPTY_DEFINITION_SUMMARY }),
registerScrolledToPathListener: () => noop,
resolvedPath: {
type: "custom-markdown-page",
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -22,42 +21,30 @@ export const NavigationContextProvider: React.FC<NavigationContextProvider.Props
children,
basePath,
}) => {
const { docsDefinition } = useDocsContext();
const { docsDefinition, pathResolver } = useDocsContext();
const router = useRouter();
const userIsScrolling = useRef(false);
const resolvedRoute = getRouteForResolvedPath({
resolvedSlug: resolvedPath.fullSlug,
asPath: router.asPath, // do not include basepath because it is already included
});
const justNavigatedTo = useRef<string | undefined>(resolvedRoute);
type ApiDefinition = FdrAPI.api.v1.read.ApiDefinition;
const resolver = useMemo(
() =>
new PathResolver({
definition: {
apis: docsDefinition.apis as Record<ApiDefinition["id"], ApiDefinition>,
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 });

Expand Down Expand Up @@ -139,7 +126,7 @@ export const NavigationContextProvider: React.FC<NavigationContextProvider.Props
if (justNavigated.current || fullSlug === selectedSlug) {
return;
}
const navigatable = resolver.resolveNavigatable(fullSlug);
const navigatable = pathResolver.resolveNavigatable(fullSlug);
if (navigatable != null) {
setActiveNavigatable(navigatable);
}
Expand All @@ -151,7 +138,7 @@ export const NavigationContextProvider: React.FC<NavigationContextProvider.Props

const navigateToPath = useEventCallback((fullSlug: string) => {
justNavigated.current = true;
const navigatable = resolver.resolveNavigatable(fullSlug);
const navigatable = pathResolver.resolveNavigatable(fullSlug);
navigateToRoute.current(`/${fullSlug}`, undefined);
if (navigatable != null) {
setActiveNavigatable(navigatable);
Expand All @@ -171,13 +158,9 @@ export const NavigationContextProvider: React.FC<NavigationContextProvider.Props
const handleRouteChangeStart = (route: string) => {
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);
};
Expand All @@ -186,14 +169,14 @@ export const NavigationContextProvider: React.FC<NavigationContextProvider.Props
useEffect(() => {
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(() => {
Expand All @@ -211,7 +194,6 @@ export const NavigationContextProvider: React.FC<NavigationContextProvider.Props
userIsScrolling: () => userIsScrolling.current,
onScrollToPath,
observeDocContent,
resolver,
registerScrolledToPathListener: scrollToPathListeners.registerListener,
activeNavigatableNeighbors,
resolvedPath,
Expand Down
8 changes: 5 additions & 3 deletions packages/ui/app/src/search/SearchHit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -23,17 +24,18 @@ export declare namespace SearchHit {
}

export const SearchHit: React.FC<SearchHit.Props> = ({ 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();
Expand Down
8 changes: 5 additions & 3 deletions packages/ui/app/src/search/SearchHits.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -20,7 +21,8 @@ export const EmptyStateView: React.FC<PropsWithChildren> = ({ children }) => {
};

export const SearchHits: React.FC = () => {
const { resolver, navigateToPath, basePath } = useNavigationContext();
const { pathResolver, basePath } = useDocsContext();
const { navigateToPath } = useNavigationContext();
const { closeSearchDialog } = useSearchContext();
const { hits } = useInfiniteHits<SearchRecord>();
const search = useInstantSearch();
Expand All @@ -31,13 +33,13 @@ export const SearchHits: React.FC = () => {
const getFullPathForHit = useCallback(
(hit: Hit<SearchRecord>) => {
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<string, HTMLAnchorElement>());
Expand Down

1 comment on commit df1d688

@vercel
Copy link

@vercel vercel bot commented on df1d688 Jan 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

fern-dev – ./packages/ui/fe-bundle

fern-dev-git-main-buildwithfern.vercel.app
app-dev.buildwithfern.com
fern-dev-buildwithfern.vercel.app

Please sign in to comment.