Skip to content

Commit

Permalink
fix: implement anchor logic in long scrolling changelog
Browse files Browse the repository at this point in the history
  • Loading branch information
abvthecity committed Sep 12, 2024
1 parent 7e5ac1b commit f30ee5d
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 47 deletions.
88 changes: 44 additions & 44 deletions packages/ui/app/src/changelog/ChangelogPage.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<number>) => {
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 {
Expand Down
19 changes: 16 additions & 3 deletions packages/ui/app/src/util/resolveDocsContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, DocsV1Read.PageContent>,
Expand Down Expand Up @@ -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<string, FernNavigation.PageId> = {};
pageRecords.forEach((record) => {
if (record.anchorTag != null && anchorIds[record.anchorTag] == null) {
anchorIds[record.anchorTag] = record.pageId;
}
});

return {
type: "changelog",
breadcrumbs: found.breadcrumbs,
Expand All @@ -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");
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit f30ee5d

Please sign in to comment.