Skip to content

Commit

Permalink
Allow four levels of sidebar entries
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 _starts with_ the subsection path.

Also edit the CSS padding of navigation links to depend on the current
level of the navigation menu. This allows for indentation of submenu
links beyond the second level.
  • Loading branch information
ptgott committed Jul 18, 2024
1 parent 229d29d commit be80e60
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 87 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
15 changes: 13 additions & 2 deletions layouts/DocsPage/Navigation.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,23 @@
display: block;
}

& .link {
padding-left: var(--m-4);
& .link{
font-size: var(--fs-text-sm);
line-height: var(--lh-md);
}

& .link-1 {
padding-left: var(--m-4);
}

& .link-2 {
padding-left: var(--m-5);
}

& .link-3 {
padding-left: var(--m-6);
}

.link.active + & {
display: block;
}
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;
34 changes: 29 additions & 5 deletions layouts/DocsPage/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,28 +24,38 @@ const SCOPE_DICTIONARY: Record<string, ScopeType> = {
interface DocsNavigationItemsProps {
entries: NavigationItem[];
onClick: () => void;
currentPath: string;
level: number;
}

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

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.startsWith(entry.slug));

return (
<li key={entry.slug}>
<Link
className={cn(
styles.link,
styles[`link-${level}`],
active && styles.active,
selected && styles.selected
)}
Expand All @@ -57,11 +67,13 @@ const DocsNavigationItems = ({
<Icon size="sm" name="ellipsis" className={styles.ellipsis} />
)}
</Link>
{!!entry.entries?.length && (
{!!entry.entries?.length && level <= maxLevel && (
<ul className={cn(styles.submenu, active && styles.opened)}>
<DocsNavigationItems
entries={entry.entries}
onClick={onClick}
currentPath={currentPath}
level={level + 1}
/>
</ul>
)}
Expand All @@ -77,6 +89,7 @@ interface DocNavigationCategoryProps extends NavigationCategory {
opened: boolean;
onToggleOpened: (value: number) => void;
onClick: () => void;
currentPath: string;
}

const DocNavigationCategory = ({
Expand All @@ -87,6 +100,7 @@ const DocNavigationCategory = ({
icon,
title,
entries,
currentPath,
}: DocNavigationCategoryProps) => {
const toggleOpened = useCallback(
() => onToggleOpened(opened ? null : id),
Expand All @@ -105,7 +119,11 @@ const DocNavigationCategory = ({
</HeadlessButton>
{opened && (
<ul className={styles["category-links"]}>
<DocsNavigationItems entries={entries} onClick={onClick} />
<DocsNavigationItems

Check failure on line 122 in layouts/DocsPage/Navigation.tsx

View workflow job for this annotation

GitHub Actions / Lint code base

Property 'level' is missing in type '{ entries: NavigationItem[]; onClick: () => void; currentPath: string; }' but required in type 'DocsNavigationItemsProps'.
entries={entries}
onClick={onClick}
currentPath={currentPath}
/>
</ul>
)}
</>
Expand Down Expand Up @@ -134,14 +152,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 +194,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 be80e60

Please sign in to comment.