Skip to content

Commit f3ab24a

Browse files
committed
feat: adapt navigation to support sub-navigation with and without landing page
1 parent e81525e commit f3ab24a

File tree

5 files changed

+151
-79
lines changed

5 files changed

+151
-79
lines changed

app.navigation.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ export const appNavigation: AppNavigation = [
33
title: "Products & Services",
44
children: [
55
{ title: "Foundations", path: "products-and-services/foundations" },
6-
{ title: "Components & Patterns", path: "products-and-services/components-and-patterns", },
6+
{ title: "Components & Patterns", path: "products-and-services/components-and-patterns" },
77
{ title: "Templates", path: "products-and-services/templates" },
88
{ title: "Extensions", path: "products-and-services/extensions" },
99
],
@@ -13,9 +13,11 @@ export const appNavigation: AppNavigation = [
1313
children: [
1414
{
1515
title: "Documentation",
16+
path: "resources/documentation",
17+
isSubNavigation: true,
1618
children: [
17-
{ title: "Getting Started", path: "resources/documentation/getting-started", },
18-
{ title: "Foundations", path: "resources/documentation/foundations", },
19+
{ title: "Getting Started", path: "resources/documentation/getting-started" },
20+
{ title: "Foundations", path: "resources/documentation/foundations" },
1921
],
2022
},
2123
],
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
layout: "@template/layouts/default"
3+
title: "Documentation"
4+
toc: false
5+
---
6+
7+
XXX
Lines changed: 43 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,50 @@
1-
2-
import {
3-
DBNavigationItem,
4-
DBNavigationItemGroup,
5-
} from "@db-ux/react-core-components";
1+
import { DBNavigationItem, DBNavigationItemGroup } from "@db-ux/react-core-components";
62
import { getAriaCurrent } from "@template/utils/client.utils.ts";
3+
import { covers, getFirstChildPath } from "@template/utils/navigation.utils.ts";
4+
5+
const NavItem = ({ path, title, icon, iconTrailing, children, isSubNavigation }: NavigationItem) => {
6+
// if sub-navigation node, do not render children here
7+
if (isSubNavigation) {
8+
const target = path ?? getFirstChildPath(children);
9+
const currentPath = typeof window !== "undefined" ? window.location.pathname : "/";
10+
const isActive = covers({ path, title, icon, iconTrailing, children, isSubNavigation }, currentPath);
11+
12+
return (
13+
<DBNavigationItem icon={icon} key={`router-leaf-${target ?? title}`}>
14+
<a
15+
href={target}
16+
aria-current={isActive ? "page" : undefined}
17+
data-icon-trailing={iconTrailing}
18+
>
19+
{title}
20+
</a>
21+
</DBNavigationItem>
22+
);
23+
}
724

8-
const NavItem = ({
9-
path,
10-
title,
11-
icon,iconTrailing,
12-
children,
13-
}: NavigationItem) => {
14-
if (children) {
25+
// node with children
26+
if (children && children.length > 0) {
27+
return (
28+
<DBNavigationItemGroup text={title} key={`router-group-${path ?? title}`}>
29+
{children.map((subItem) => (
30+
<NavItem key={`router-sub-path-${subItem.path ?? subItem.title}`} {...subItem} />
31+
))}
32+
</DBNavigationItemGroup>
33+
);
34+
}
35+
36+
// leaf-node, no children
1537
return (
16-
<DBNavigationItemGroup text={title} key={`router-path-${path}-${title}`}>
17-
{children.map((subItem) => (
18-
<NavItem key={`router-sub-path-${subItem.path}`} {...subItem} />
19-
))}
20-
</DBNavigationItemGroup>
38+
<DBNavigationItem icon={icon} key={`router-leaf-${path ?? title}`}>
39+
<a
40+
href={path}
41+
aria-current={path ? getAriaCurrent(path) : undefined}
42+
data-icon-trailing={iconTrailing}
43+
>
44+
{title}
45+
</a>
46+
</DBNavigationItem>
2147
);
22-
}
23-
24-
return (
25-
<DBNavigationItem icon={icon} key={`router-path-${path}-${title}`}>
26-
<a
27-
href={path}
28-
aria-current={getAriaCurrent(path)}
29-
data-icon-trailing={iconTrailing}
30-
>
31-
{title}
32-
</a>
33-
</DBNavigationItem>
34-
);
3548
};
3649

3750
export default NavItem;

template/types/global.d.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,11 @@ declare interface NavigationItem {
4343
* The title of the navigation item.
4444
*/
4545
title: string;
46-
/*
46+
/**
4747
* An optional icon that can be used to represent the navigation item.
4848
*/
4949
icon?: string;
50-
/*
50+
/**
5151
* An optional icon that can be used to represent the navigation item.
5252
*/
5353
iconTrailing?: string;
@@ -62,9 +62,9 @@ declare interface NavigationItem {
6262
*/
6363
children?: NavigationItem[];
6464
/**
65-
* An optional sub navigation.
65+
* If true, the children will be rendered as sub-navigation items.
6666
*/
67-
subNavigation?: NavigationItem[];
67+
isSubNavigation?: boolean;
6868
}
6969

7070
/**

template/utils/navigation.utils.ts

Lines changed: 92 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,56 +2,106 @@ import { appConfig } from "@root/app.config";
22
import { appNavigation } from "@root/app.navigation";
33

44
/**
5-
* Returns the parent of a navigation item based on the given pathname.
6-
* @param pathname - The pathname to find the parent for.
7-
* @returns The parent navigation item or undefined if none is found.
5+
* Normalizes a path by removing trailing slashes.
6+
*
7+
* @param path - The path to normalize
8+
* @returns The normalized path without trailing slashes
89
*/
9-
export function getNavigationItemParent(
10-
pathname: string,
11-
): NavigationItem | undefined {
12-
const { basePath } = appConfig;
13-
const _pathname = pathname.replace(/\/+$/, "");
14-
15-
return appNavigation.filter((item) => {
16-
const children = [...(item.children ?? []), ...(item.subNavigation ?? [])];
17-
if (children.length > 0) {
18-
return children.some((child) => {
19-
const fullPath = `${basePath}${child.path}`.replace(/\/+$/, "");
20-
return fullPath === _pathname;
21-
});
22-
}
23-
return false;
24-
})[0];
10+
const norm = (path: string) => path.replace(/\/+$/, "");
11+
12+
/**
13+
* Prepends the base path to a relative path and normalizes the result.
14+
*
15+
* @param relPath - The relative path to prepend the base path to
16+
* @returns The normalized absolute path including the base path
17+
*/
18+
function withBase(relPath: string) {
19+
const base = norm(appConfig.basePath || "/");
20+
const rel = relPath.startsWith("/") ? relPath : `/${relPath}`;
21+
return norm(`${base}${rel}`);
2522
}
2623

2724
/**
28-
* Recursively searches for a subNavigation array with at least one entry for the given pathname.
29-
* @param pathname - The pathname to check.
30-
* @returns The subNavigation array if found, otherwise undefined.
25+
* Returns the path of the first child navigation item, if any.
26+
* @param children - An array of navigation items.
27+
* @returns The path of the first child navigation item or undefined if there are no children.
3128
*/
32-
export function findSubNavigation(
33-
pathname: string,
34-
): NavigationItem[] | undefined {
35-
const { basePath } = appConfig;
36-
const _pathname = pathname.replace(/\/+$/, "");
29+
export function getFirstChildPath(children?: NavigationItem[]): string | undefined {
30+
return children && children.length > 0 ? children[0]?.path : undefined;
31+
}
32+
33+
/**
34+
* Checks if a navigation item or any of its descendants covers the current path.
35+
* A path is considered "covered" if it either exactly matches or is a sub-path of the navigation item's path.
36+
*
37+
* @param item - The navigation item to check
38+
* @param currentPath - The current path to compare against
39+
* @returns {boolean} True if the item or its descendants cover the current path, false otherwise
40+
*/
41+
export function covers(item: NavigationItem, currentPath: string): boolean {
42+
if (item.path) {
43+
const full = withBase(item.path);
44+
if (currentPath === full || currentPath.startsWith(full + "/")) return true;
45+
}
46+
for (const child of item.children ?? []) {
47+
if (covers(child, currentPath)) return true;
48+
}
49+
return false;
50+
}
3751

38-
function checkRecursive(item: NavigationItem | undefined): NavigationItem[] | undefined {
39-
if (!item) return undefined;
40-
const fullPath = `${basePath}${item.path}`.replace(/\/+$/, "");
41-
if (fullPath === _pathname && Array.isArray(item.subNavigation) && item.subNavigation.length > 0) {
42-
return item.subNavigation;
52+
/**
53+
* Finds the children of the deepest sub-navigation item that covers the current pathname.
54+
*
55+
* @param currentPathname - The current pathname to find sub-navigation for
56+
* @returns An array of navigation items if a matching sub-navigation is found, undefined otherwise
57+
*/
58+
export function findSubNavigation(currentPathname: string): NavigationItem[] | undefined {
59+
const current = norm(currentPathname);
60+
let match: { node: NavigationItem; depth: number } | undefined;
61+
62+
const walk = (node: NavigationItem, depth: number) => {
63+
if (node.isSubNavigation && covers(node, current)) {
64+
if (!match || depth > match.depth) match = { node, depth };
4365
}
44-
const children = [...(item.children ?? []), ...(item.subNavigation ?? [])];
45-
for (const child of children) {
46-
const result = checkRecursive(child);
47-
if (result) return result;
66+
for (const child of node.children ?? []) walk(child, depth + 1);
67+
};
68+
69+
for (const top of appNavigation) walk(top, 0);
70+
return match?.node.children;
71+
}
72+
73+
/**
74+
* Finds the parent navigation item for a given pathname.
75+
*
76+
* @param pathname - The pathname to find the parent for
77+
* @returns The parent navigation item if found, undefined otherwise
78+
*/
79+
export function getNavigationItemParent(pathname: string): NavigationItem | undefined {
80+
const { basePath } = appConfig;
81+
const norm = (p: string) => p.replace(/\/+$/, "");
82+
const _pathname = norm(pathname);
83+
84+
const withBase = (relPath?: string) => {
85+
if (!relPath) return undefined;
86+
const base = norm(basePath || "/");
87+
const rel = relPath.startsWith("/") ? relPath : `/${relPath}`;
88+
return norm(`${base}${rel}`);
89+
};
90+
91+
const findParent = (nodes: NavigationItem[]): NavigationItem | undefined => {
92+
for (const node of nodes) {
93+
const children = node.children ?? [];
94+
for (const child of children) {
95+
const full = withBase(child.path);
96+
if (full && full === _pathname) {
97+
return node;
98+
}
99+
}
100+
const found = findParent(children);
101+
if (found) return found;
48102
}
49103
return undefined;
50-
}
104+
};
51105

52-
for (const navItem of appNavigation) {
53-
const result = checkRecursive(navItem);
54-
if (result) return result;
55-
}
56-
return undefined;
106+
return findParent(appNavigation);
57107
}

0 commit comments

Comments
 (0)