Skip to content
This repository has been archived by the owner on Jan 8, 2025. It is now read-only.

Commit

Permalink
Generate sidebar entries at an arbitrary depth
Browse files Browse the repository at this point in the history
Currently, the code that generates the navigation sidebar from a
directory tree stops at the second level of a given top-level section.
However, some sections include three levels of content. This change
edits the sidebar generator so it works recursively.

Also fix an issue with the `DocsNavigationItems` component that prevents
the docs site from highlighting sidebar entries past two levels of
depth. The component treats a sidebar subsection as "active" if one of
its entries is equivalent to the current page path.

But if the current page path is a grandchild of a sidebar subsection,
this means that the component hides the grandchild, since none of the
children of the subsection is equivalent to the current page. This
change determines that a sidebar subsection is "active" if the selected
path _includes_ the subsection path.
  • Loading branch information
ptgott committed Jul 18, 2024
1 parent 229d29d commit 144547d
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 84 deletions.
5 changes: 4 additions & 1 deletion .storybook/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { StorybookConfig } from "@storybook/nextjs";

const config: StorybookConfig = {
stories: ["../components/**/*.stories.@(js|jsx|ts|tsx)"],
stories: [
"../components/**/*.stories.@(js|jsx|ts|tsx)",
"../layouts/**/*.stories.@(js|jsx|ts|tsx)",
],
addons: ["@storybook/addon-interactions", "@storybook/addon-viewport"],
framework: {
name: "@storybook/nextjs",
Expand Down
48 changes: 48 additions & 0 deletions layouts/DocsPage/Navigation.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { Meta, StoryObj } from "@storybook/react";
import { userEvent, within } from "@storybook/testing-library";
import { expect } from "@storybook/jest";
import { default as DocNavigation } from "layouts/DocsPage/Navigation";

export const NavigationFourLevels = () => {
const data = [
{
icon: "wrench",
title: "Enroll Resources",
entries: [
{
title: "Machine ID",
slug: "/enroll-resources/machine-id/",
entries: [
{
title: "Deploy Machine ID",
slug: "/enroll-resources/machine-id/deployment/",
entries: [
{
title: "Deploy Machine ID on AWS",
slug: "/enroll-resources/machine-id/deployment/aws/",
},
],
},
],
},
],
},
];

return (
<DocNavigation
data={data}

Check failure on line 34 in layouts/DocsPage/Navigation.stories.tsx

View workflow job for this annotation

GitHub Actions / Lint code base

Type '{ icon: string; title: string; entries: { title: string; slug: string; entries: { title: string; slug: string; entries: { title: string; slug: string; }[]; }[]; }[]; }[]' is not assignable to type 'NavigationCategory[]'.
section={true}
currentVersion="16.x"
currentPathGetter={() => {
return "/enroll-resources/machine-id/deployment/aws/";
}}
></DocNavigation>
);
};

const meta: Meta<typeof DocNavigation> = {
title: "layouts/DocNavigation",
component: NavigationFourLevels,
};
export default meta;
24 changes: 20 additions & 4 deletions layouts/DocsPage/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ const SCOPE_DICTIONARY: Record<string, ScopeType> = {
interface DocsNavigationItemsProps {
entries: NavigationItem[];
onClick: () => void;
currentPath: string;
}

const DocsNavigationItems = ({
entries,
onClick,
currentPath,
}: DocsNavigationItemsProps) => {
const docPath = useCurrentHref().split(SCOPELESS_HREF_REGEX)[0];
const docPath = currentPath.split(SCOPELESS_HREF_REGEX)[0];
const { getVersionAgnosticRoute } = useVersionAgnosticPages();

return (
Expand All @@ -39,7 +41,8 @@ const DocsNavigationItems = ({
entries.map((entry) => {
const selected = entry.slug === docPath;
const active =
selected || entry.entries?.some((entry) => entry.slug === docPath);
selected ||
entry.entries?.some((entry) => docPath.includes(entry.slug));

return (
<li key={entry.slug}>
Expand All @@ -62,6 +65,7 @@ const DocsNavigationItems = ({
<DocsNavigationItems
entries={entry.entries}
onClick={onClick}
currentPath={currentPath}
/>
</ul>
)}
Expand All @@ -77,6 +81,7 @@ interface DocNavigationCategoryProps extends NavigationCategory {
opened: boolean;
onToggleOpened: (value: number) => void;
onClick: () => void;
currentPath: string;
}

const DocNavigationCategory = ({
Expand All @@ -87,6 +92,7 @@ const DocNavigationCategory = ({
icon,
title,
entries,
currentPath,
}: DocNavigationCategoryProps) => {
const toggleOpened = useCallback(
() => onToggleOpened(opened ? null : id),
Expand All @@ -105,7 +111,11 @@ const DocNavigationCategory = ({
</HeadlessButton>
{opened && (
<ul className={styles["category-links"]}>
<DocsNavigationItems entries={entries} onClick={onClick} />
<DocsNavigationItems
entries={entries}
onClick={onClick}
currentPath={currentPath}
/>
</ul>
)}
</>
Expand Down Expand Up @@ -134,14 +144,19 @@ interface DocNavigationProps {
section?: boolean;
currentVersion?: string;
data: NavigationCategory[];
currentPathGetter: () => string;
}

const DocNavigation = ({
data,
section,
currentVersion,
currentPathGetter,
}: DocNavigationProps) => {
const route = useCurrentHref();
if (!currentPathGetter) {
currentPathGetter = useCurrentHref;
}
const route = currentPathGetter();

const [openedId, setOpenedId] = useState<number>(
getCurrentCategoryIndex(data, route)
Expand Down Expand Up @@ -171,6 +186,7 @@ const DocNavigation = ({
opened={index === openedId}
onToggleOpened={setOpenedId}
onClick={toggleMenu}
currentPath={route}
{...props}
/>
</li>
Expand Down
69 changes: 26 additions & 43 deletions server/pages-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ export const getPageInfo = <T = MDXPageFrontmatter>(
return result;
};

// getEntryForPath returns a navigation item for the file at filePath in the
// given filesystem.
const getEntryForPath = (fs, filePath) => {
const txt = fs.readFileSync(filePath, "utf8");
const { data } = matter(txt);
Expand Down Expand Up @@ -108,23 +110,36 @@ const categoryPagePathForDir = (fs, dirPath) => {
);
};

export const generateNavPaths = (fs, dirPath) => {
export const navEntriesForDir = (fs, dirPath) => {
const firstLvl = fs.readdirSync(dirPath, "utf8");
let result = [];
let firstLvlFiles = new Set();
let firstLvlDirs = new Set();

// Sort the contents of dirPath into files and directoreis.
firstLvl.forEach((p) => {
const fullPath = join(dirPath, p);
const info = fs.statSync(fullPath);
if (info.isDirectory()) {
firstLvlDirs.add(fullPath);
return;
}
const fileName = parse(fullPath).name;
const dirName = parse(dirPath).name;

// This is a category page for the containing directory. We would have
// already handled this in the previous iteration. The first iteration
// does not require a category page.
if (fileName == dirName) {
return;
}

firstLvlFiles.add(fullPath);
});

// Map category pages to the directories they introduce so we can can add a
// sidebar entry for the category page, then traverse the directory.
// sidebar entry for each category page, then traverse each directory to add
// further sidebar pages.
let sectionIntros = new Map();
firstLvlDirs.forEach((d: string) => {
sectionIntros.set(categoryPagePathForDir(fs, d), d);
Expand All @@ -145,56 +160,24 @@ export const generateNavPaths = (fs, dirPath) => {
result.push(getEntryForPath(fs, f));
});

// Add a category page for each section intro, then traverse the contents of
// the directory that the category page introduces, adding the contents to
// entries.
sectionIntros.forEach((dirPath, categoryPagePath) => {
const { slug, title } = getEntryForPath(fs, categoryPagePath);
const section = {
title: title,
slug: slug,
entries: [],
};
const secondLvl = new Set(fs.readdirSync(dirPath, "utf8"));

// Find all second-level category pages first so we don't
// repeat them in the sidebar.
secondLvl.forEach((f2: string) => {
let fullPath2 = join(dirPath, f2);
const stat = fs.statSync(fullPath2);

// List category pages on the second level, but not their contents.
if (!stat.isDirectory()) {
return;
}
const catPath = categoryPagePathForDir(fs, fullPath2);
fullPath2 = catPath;
secondLvl.delete(f2);

// Delete the category page from the set so we don't add it again
// when we add individual files.
secondLvl.delete(parse(catPath).base);
section.entries.push(getEntryForPath(fs, fullPath2));
});

secondLvl.forEach((f2: string) => {
// Only add entries for MDX files here
if (!f2.endsWith(".mdx")) {
return;
}

let fullPath2 = join(dirPath, f2);

// This is a first-level category page that happens to exist on the second
// level.
if (sectionIntros.has(fullPath2)) {
return;
}

const stat = fs.statSync(fullPath2);
section.entries.push(getEntryForPath(fs, fullPath2));
});

section.entries.sort(sortByTitle);

section.entries = navEntriesForDir(fs, dirPath);
result.push(section);
});
result.sort(sortByTitle);
return result;
};

export const generateNavPaths = (fs, dirPath) => {
return navEntriesForDir(fs, dirPath);
};
Loading

0 comments on commit 144547d

Please sign in to comment.