From f30ee5dbbaca3b0ff320c5f56fe40ce46066d0d3 Mon Sep 17 00:00:00 2001 From: Andrew Jiang Date: Thu, 12 Sep 2024 17:13:40 -0400 Subject: [PATCH] fix: implement anchor logic in long scrolling changelog --- .../ui/app/src/changelog/ChangelogPage.tsx | 88 +++++++++---------- .../ui/app/src/util/resolveDocsContent.ts | 19 +++- 2 files changed, 60 insertions(+), 47 deletions(-) diff --git a/packages/ui/app/src/changelog/ChangelogPage.tsx b/packages/ui/app/src/changelog/ChangelogPage.tsx index bd3fa47b2c..502a172dbd 100644 --- a/packages/ui/app/src/changelog/ChangelogPage.tsx +++ b/packages/ui/app/src/changelog/ChangelogPage.tsx @@ -1,10 +1,10 @@ import type { FernNavigation } from "@fern-api/fdr-sdk"; import { EMPTY_ARRAY } from "@fern-ui/core-utils"; -import { usePrevious } from "@fern-ui/react-commons"; import clsx from "clsx"; -import { SetStateAction, atom, useAtom, useAtomValue } from "jotai"; +import { atom, useAtomValue } from "jotai"; import { chunk } from "lodash-es"; import { Fragment, ReactElement, useEffect, useMemo } from "react"; +import { useMemoOne } from "use-memo-one"; import { IS_READY_ATOM, LOCATION_ATOM, SCROLL_BODY_ATOM, SIDEBAR_ROOT_NODE_ATOM } from "../atoms"; import { BottomNavigationButtons } from "../components/BottomNavigationButtons"; import { FernLink } from "../components/FernLink"; @@ -21,65 +21,65 @@ function flattenChangelogEntries(page: DocsContent.ChangelogPage): FernNavigatio } const CHANGELOG_PAGE_SIZE = 10; -const CHANGELOG_PAGE_ATOM = atom( - (get) => { - if (!get(IS_READY_ATOM)) { - return 1; - } - - const hash = get(LOCATION_ATOM).hash; - if (hash == null) { - return 1; - } - const match = hash.match(/^#page-(\d+)$/)?.[1]; - if (match == null) { - return 1; - } - return Math.max(parseInt(match, 10), 1); - }, - (get, set, page: SetStateAction) => { - const newPage = typeof page === "function" ? page(get(CHANGELOG_PAGE_ATOM)) : page; - set(LOCATION_ATOM, { hash: newPage > 1 ? `#page-${newPage}` : "" }, { replace: false }); - }, -); function getOverviewMdx(page: DocsContent.ChangelogPage): BundledMDX | undefined { return page.node.overviewPageId != null ? page.pages[page.node.overviewPageId] : undefined; } export function ChangelogPage({ content }: { content: DocsContent.ChangelogPage }): ReactElement { - const [page, setPage] = useAtom(CHANGELOG_PAGE_ATOM); + const flattenedEntries = useMemo(() => flattenChangelogEntries(content), [content]); + const chunkedEntries = useMemo(() => chunk(flattenedEntries, CHANGELOG_PAGE_SIZE), [flattenedEntries]); + const page = useAtomValue( + useMemoOne(() => { + const pageAtom = atom((get) => { + if (!get(IS_READY_ATOM)) { + return 1; + } + + const hash = get(LOCATION_ATOM).hash; + if (hash == null) { + return 1; + } + + const pageId = content.anchorIds[hash.slice(1)]; + + if (pageId != null) { + const entry = flattenedEntries.findIndex((entry) => entry.pageId === pageId); + if (entry !== -1) { + return Math.floor(entry / CHANGELOG_PAGE_SIZE) + 1; + } + } + + const match = hash.match(/^#page-(\d+)$/)?.[1]; + if (match == null) { + return 1; + } + /** + * Ensure the page number is within the bounds of the changelog entries + */ + return Math.min(Math.max(parseInt(match, 10), 1), chunkedEntries.length); + }); + return pageAtom; + }, [content.anchorIds, flattenedEntries, chunkedEntries.length]), + ); const overview = getOverviewMdx(content); - const chunkedEntries = useMemo(() => chunk(flattenChangelogEntries(content), CHANGELOG_PAGE_SIZE), [content]); const toHref = useToHref(); const fullWidth = useAtomValue(SIDEBAR_ROOT_NODE_ATOM) == null; - /** - * Reset paging when navigating between different changelog pages - */ - const prevousNodeId = usePrevious(content.node.id); - useEffect(() => { - if (prevousNodeId !== content.node.id) { - setPage(1); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [prevousNodeId, content.node.id]); - - /** - * Ensure that the page is within bounds - */ - useEffect(() => { - setPage((prev) => Math.min(prev, chunkedEntries.length)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [chunkedEntries.length]); - /** * Scroll to the top of the page when navigating to a new page of the changelog */ const scrollBody = useAtomValue(SCROLL_BODY_ATOM); useEffect(() => { + const element = document.getElementById(window.location.hash.slice(1)); + + if (element != null) { + element.scrollIntoView(); + return; + } + if (scrollBody instanceof Document) { window.scrollTo(0, 0); } else { diff --git a/packages/ui/app/src/util/resolveDocsContent.ts b/packages/ui/app/src/util/resolveDocsContent.ts index 598acf0d8e..3b049b8dec 100644 --- a/packages/ui/app/src/util/resolveDocsContent.ts +++ b/packages/ui/app/src/util/resolveDocsContent.ts @@ -14,8 +14,6 @@ import { ApiTypeResolver } from "../resolver/ApiTypeResolver"; import type { DocsContent } from "../resolver/DocsContent"; import type { ResolvedApiEndpoint, ResolvedRootPackage } from "../resolver/types"; -const slugger = new GithubSlugger(); - async function getSubtitle( node: FernNavigation.NavigationNodeNeighbor, pages: Record, @@ -109,6 +107,16 @@ export async function resolveDocsContent({ }) : undefined; + /** + * if there are duplicate anchor tags, the anchor from the first page where it appears will be used + */ + const anchorIds: Record = {}; + pageRecords.forEach((record) => { + if (record.anchorTag != null && anchorIds[record.anchorTag] == null) { + anchorIds[record.anchorTag] = record.pageId; + } + }); + return { type: "changelog", breadcrumbs: found.breadcrumbs, @@ -118,7 +126,7 @@ export async function resolveDocsContent({ // items: await Promise.all(itemsPromise), // neighbors, slug: found.node.slug, - anchorIds: Object.fromEntries(pageRecords.map((record) => [record.anchorTag ?? "", record.pageId])), + anchorIds, }; } else if (node.type === "changelogEntry") { const changelogNode = reverse(parents).find((n): n is FernNavigation.ChangelogNode => n.type === "changelog"); @@ -338,6 +346,11 @@ async function getNeighbors( } export function parseMarkdownPageToAnchorTag(markdown: string): string | undefined { + /** + * new slugger instance per page to avoid conflicts between pages + */ + const slugger = new GithubSlugger(); + // This regex match is temporary and will be replaced with a more robust solution const matches = markdown.match(/^(#{1,6})\s+(.+)$/gm); let anchorTag = undefined;