Skip to content

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 17, 2024
1 parent 229d29d commit 59c8ffb
Show file tree
Hide file tree
Showing 5 changed files with 206 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
71 changes: 71 additions & 0 deletions layouts/DocsPage/Navigation.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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";

import { TabItem } from "./TabItem";

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

View workflow job for this annotation

GitHub Actions / Lint code base

Cannot find module './TabItem' or its corresponding type declarations.
import { Tabs } from "./Tabs";

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

View workflow job for this annotation

GitHub Actions / Lint code base

Cannot find module './Tabs' or its corresponding type declarations.

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 37 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 Tabs> = {
title: "layouts/DocNavigation",
component: NavigationFourLevels,
};
export default meta;
type Story = StoryObj<typeof Tabs>;

// export const InitialStateTab: Story = {
// play: async ({ canvasElement, step }) => {
// const canvas = within(canvasElement);
// await step("Should be the text for the selected option", async () => {
// expect(
// await canvas.findByText(
// "Instructions for installing release using shell commands."
// )
// ).toBeTruthy();
// });
// await step(
// "Selected option should be disabled for re-selection",
// async () => {
// expect(canvas.getByText("Shell")).toBeDisabled();
// }
// );
// },
// };
26 changes: 22 additions & 4 deletions layouts/DocsPage/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,27 @@ 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();

console.log("entries:", entries);

return (
<>
{!!entries.length &&
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 +67,7 @@ const DocsNavigationItems = ({
<DocsNavigationItems
entries={entry.entries}
onClick={onClick}
currentPath={currentPath}
/>
</ul>
)}
Expand All @@ -77,6 +83,7 @@ interface DocNavigationCategoryProps extends NavigationCategory {
opened: boolean;
onToggleOpened: (value: number) => void;
onClick: () => void;
currentPath: string;
}

const DocNavigationCategory = ({
Expand All @@ -87,6 +94,7 @@ const DocNavigationCategory = ({
icon,
title,
entries,
currentPath,
}: DocNavigationCategoryProps) => {
const toggleOpened = useCallback(
() => onToggleOpened(opened ? null : id),
Expand All @@ -105,7 +113,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 +146,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 +188,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 59c8ffb

Please sign in to comment.