Skip to content

Commit d53af68

Browse files
Merge pull request #2319 from dxc-technology/Mil4n0r/tree_navigation
NavigationTree internal component implementation + SideNav reimplementation
2 parents fb44418 + 6e49199 commit d53af68

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+2224
-1374
lines changed

apps/website/pages/_app.tsx

Lines changed: 99 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
1-
import { ReactElement, ReactNode, useMemo, useState } from "react";
1+
import { ReactElement, ReactNode, useEffect, useMemo, useState } from "react";
22
import type { NextPage } from "next";
33
import type { AppProps } from "next/app";
44
import Head from "next/head";
55
import { DxcApplicationLayout, DxcTextInput, DxcToastsQueue } from "@dxc-technology/halstack-react";
6-
import SidenavLogo from "@/common/sidenav/SidenavLogo";
76
import MainContent from "@/common/MainContent";
87
import { useRouter } from "next/router";
98
import { LinksSectionDetails, LinksSections } from "@/common/pagesList";
10-
import Link from "next/link";
119
import StatusBadge from "@/common/StatusBadge";
1210
import "../global-styles.css";
1311
import createCache, { EmotionCache } from "@emotion/cache";
1412
import { CacheProvider } from "@emotion/react";
13+
import { usePathname } from "next/navigation";
14+
import Link from "next/link";
15+
import { GroupItem, Item, Section } from "../../../packages/lib/src/base-menu/types";
16+
import { isGroupItem } from "../../../packages/lib/src/base-menu/utils";
17+
import SidenavLogo from "@/common/sidenav/SidenavLogo";
1518

1619
type NextPageWithLayout = NextPage & {
1720
getLayout?: (_page: ReactElement) => ReactNode;
@@ -26,73 +29,110 @@ const clientSideEmotionCache = createCache({ key: "css", prepend: true });
2629
export default function App({ Component, pageProps, emotionCache = clientSideEmotionCache }: AppPropsWithLayout) {
2730
const getLayout = Component.getLayout || ((page) => page);
2831
const componentWithLayout = getLayout(<Component {...pageProps} />);
32+
const router = useRouter();
33+
const pathname = usePathname();
2934
const [filter, setFilter] = useState("");
30-
const { asPath: currentPath } = useRouter();
31-
const filteredLinks = useMemo(() => {
32-
const filtered: LinksSectionDetails[] = [];
33-
LinksSections.map((section) => {
34-
const sectionFilteredLinks = section?.links.filter((link) =>
35-
link.label.toLowerCase().includes(filter.toLowerCase())
36-
);
37-
if (sectionFilteredLinks.length) {
38-
filtered.push({ label: section.label, links: sectionFilteredLinks });
39-
}
40-
});
41-
return filtered;
42-
}, [filter]);
35+
const [isExpanded, setIsExpanded] = useState(true);
36+
37+
const filterSections = (sections: Section[], query: string): Section[] => {
38+
const q = query.trim().toLowerCase();
39+
if (!q) return sections;
40+
41+
const filterItem = (item: Item | GroupItem): Item | GroupItem | null => {
42+
const labelMatches = item.label.toLowerCase().includes(q);
43+
44+
if (!isGroupItem(item)) return labelMatches ? item : null;
45+
46+
const items = item.items.reduce<(Item | GroupItem)[]>((acc, child) => {
47+
const filtered = filterItem(child);
48+
if (filtered) acc.push(filtered);
49+
return acc;
50+
}, []);
4351

44-
const matchPaths = (linkPath: string) => {
45-
const desiredPaths = [linkPath, `${linkPath}/code`];
46-
const pathToBeMatched = currentPath?.split("#")[0]?.slice(0, -1);
47-
return pathToBeMatched ? desiredPaths.includes(pathToBeMatched) : false;
52+
return labelMatches || items.length ? { ...item, items } : null;
53+
};
54+
55+
return sections.reduce<Section[]>((acc, section) => {
56+
const items = section.items.reduce<(Item | GroupItem)[]>((acc, item) => {
57+
const filtered = filterItem(item);
58+
if (filtered) acc.push(filtered);
59+
return acc;
60+
}, []);
61+
if (items.length) acc.push({ ...section, items });
62+
return acc;
63+
}, []);
4864
};
4965

66+
const mapLinksToGroupItems = (sections: LinksSectionDetails[]): Section[] => {
67+
const matchPaths = (linkPath: string) => {
68+
const desiredPaths = [linkPath, `${linkPath}/code`];
69+
const pathToBeMatched = pathname?.split("#")[0]?.slice(0, -1);
70+
return pathToBeMatched ? desiredPaths.includes(pathToBeMatched) : false;
71+
};
72+
73+
return sections.map((section) => ({
74+
title: section.label,
75+
items: section.links.map((link) => ({
76+
label: link.label,
77+
href: link.path,
78+
selected: matchPaths(link.path),
79+
...(link.status && {
80+
badge: link.status !== "stable" ? <StatusBadge hasTitle status={link.status} /> : undefined,
81+
}),
82+
renderItem: ({ children }: { children: ReactNode }) => (
83+
<Link key={link.path} href={link.path} passHref legacyBehavior>
84+
{children}
85+
</Link>
86+
),
87+
})),
88+
}));
89+
};
90+
91+
useEffect(() => {
92+
const paths = [...new Set(LinksSections.flatMap((s) => s.links.map((l) => l.path)))];
93+
const prefetchPaths = async () => {
94+
for (const path of paths) {
95+
await router.prefetch(path);
96+
}
97+
};
98+
void prefetchPaths();
99+
}, []);
100+
101+
// TODO: ADD NEW CATEGORIZATION
102+
103+
const filteredSections = useMemo(() => {
104+
const sections = mapLinksToGroupItems(LinksSections);
105+
return filterSections(sections, filter);
106+
}, [filter]);
107+
50108
return (
51109
<CacheProvider value={emotionCache}>
52110
<Head>
53111
<link rel="icon" type="image/png" sizes="32x32" href="/favicon.png" />
54112
</Head>
55113
<DxcApplicationLayout
56-
visibilityToggleLabel="Menu"
57114
sidenav={
58-
<DxcApplicationLayout.SideNav title={<SidenavLogo />}>
59-
<DxcApplicationLayout.SideNav.Section>
60-
<DxcTextInput
61-
placeholder="Search docs"
62-
value={filter}
63-
onChange={({ value }: { value: string }) => {
64-
setFilter(value);
65-
}}
66-
size="fillParent"
67-
clearable
68-
margin={{
69-
top: "large",
70-
bottom: "large",
71-
right: "medium",
72-
left: "medium",
73-
}}
74-
/>
75-
</DxcApplicationLayout.SideNav.Section>
76-
{filteredLinks?.map(({ label, links }) => (
77-
<DxcApplicationLayout.SideNav.Section key={label}>
78-
<DxcApplicationLayout.SideNav.Group title={label}>
79-
{links.map(({ label, path, status }) => (
80-
<Link key={`${label}-${path}`} href={path} passHref legacyBehavior>
81-
<DxcApplicationLayout.SideNav.Link selected={matchPaths(path)}>
82-
{label}
83-
{status && status !== "stable" && <StatusBadge hasTitle status={status} />}
84-
</DxcApplicationLayout.SideNav.Link>
85-
</Link>
86-
))}
87-
</DxcApplicationLayout.SideNav.Group>
88-
</DxcApplicationLayout.SideNav.Section>
89-
))}
90-
<DxcApplicationLayout.SideNav.Section>
91-
<DxcApplicationLayout.SideNav.Link href="https://github.com/dxc-technology/halstack-react" newWindow>
92-
GitHub
93-
</DxcApplicationLayout.SideNav.Link>
94-
</DxcApplicationLayout.SideNav.Section>
95-
</DxcApplicationLayout.SideNav>
115+
<DxcApplicationLayout.Sidenav
116+
navItems={filteredSections}
117+
branding={<SidenavLogo expanded={isExpanded} />}
118+
topContent={
119+
isExpanded && (
120+
<DxcTextInput
121+
placeholder="Search docs"
122+
value={filter}
123+
onChange={({ value }: { value: string }) => {
124+
setFilter(value);
125+
}}
126+
size="fillParent"
127+
clearable
128+
/>
129+
)
130+
}
131+
expanded={isExpanded}
132+
onExpandedChange={() => {
133+
setIsExpanded((currentlyExpanded) => !currentlyExpanded);
134+
}}
135+
/>
96136
}
97137
>
98138
<DxcApplicationLayout.Main>

apps/website/pages/components/sidenav/code.tsx

Lines changed: 0 additions & 17 deletions
This file was deleted.
Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,36 @@
1+
// import Head from "next/head";
2+
// import type { ReactElement } from "react";
3+
// import SidenavPageLayout from "screens/components/sidenav/SidenavPageLayout";
4+
// import SidenavOverviewPage from "screens/components/sidenav/overview/SidenavOverviewPage";
5+
6+
// const Index = () => (
7+
// <>
8+
// <Head>
9+
// <title>Sidenav — Halstack Design System</title>
10+
// </Head>
11+
// {/* <SidenavOverviewPage /> */}
12+
// <SidenavOverviewPage />
13+
// </>
14+
// );
15+
16+
// Index.getLayout = (page: ReactElement) => <SidenavPageLayout>{page}</SidenavPageLayout>;
17+
18+
// export default Index;
19+
120
import Head from "next/head";
221
import type { ReactElement } from "react";
322
import SidenavPageLayout from "screens/components/sidenav/SidenavPageLayout";
4-
import SidenavOverviewPage from "screens/components/sidenav/overview/SidenavOverviewPage";
23+
import SidenavCodePage from "screens/components/sidenav/code/SidenavCodePage";
524

6-
const Index = () => (
25+
const Code = () => (
726
<>
827
<Head>
9-
<title>Sidenav — Halstack Design System</title>
28+
<title>Sidenav code — Halstack Design System</title>
1029
</Head>
11-
<SidenavOverviewPage />
30+
<SidenavCodePage />
1231
</>
1332
);
1433

15-
Index.getLayout = (page: ReactElement) => <SidenavPageLayout>{page}</SidenavPageLayout>;
34+
Code.getLayout = (page: ReactElement) => <SidenavPageLayout>{page}</SidenavPageLayout>;
1635

17-
export default Index;
36+
export default Code;

apps/website/screens/common/StatusBadge.tsx

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ComponentStatus } from "./pagesList";
44
type StatusBadgeProps = {
55
hasTitle?: boolean;
66
status: ComponentStatus | "required";
7+
// reduced?: boolean;
78
};
89

910
const getBadgeColor = (status: StatusBadgeProps["status"]) => {
@@ -40,13 +41,36 @@ const getBadgeTitle = (status: StatusBadgeProps["status"]) => {
4041
}
4142
};
4243

43-
const StatusBadge = ({ hasTitle = false, status }: StatusBadgeProps) => (
44-
<DxcBadge
45-
label={status[0]?.toUpperCase() + status.slice(1)}
46-
color={getBadgeColor(status)}
47-
title={hasTitle ? getBadgeTitle(status) : undefined}
48-
size="small"
49-
/>
50-
);
44+
// TODO: enable icon when the status badge supports reduced version
45+
// const getBadgeIcon = (status: StatusBadgeProps["status"]) => {
46+
// switch (status) {
47+
// case "required":
48+
// return "warning_amber";
49+
// case "experimental":
50+
// return "science";
51+
// case "new":
52+
// return "new_releases";
53+
// case "stable":
54+
// return "check_circle";
55+
// case "legacy":
56+
// return "history";
57+
// case "deprecated":
58+
// return "highlight_off";
59+
// default:
60+
// return "";
61+
// }
62+
// };
63+
64+
const StatusBadge = ({ hasTitle = false, status }: StatusBadgeProps) => {
65+
return (
66+
<DxcBadge
67+
label={status[0]?.toUpperCase() + status.slice(1)}
68+
color={getBadgeColor(status)}
69+
title={hasTitle ? getBadgeTitle(status) : undefined}
70+
size="small"
71+
// icon={reduced ? getBadgeIcon(status) : undefined}
72+
/>
73+
);
74+
};
5175

5276
export default StatusBadge;

apps/website/screens/common/sidenav/SidenavLogo.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ const Subtitle = styled.div`
2222
font-family: var(--typography-font-family);
2323
`;
2424

25-
const SidenavLogo = ({ subtitle = "Design System" }: { subtitle?: string }) => {
25+
const SidenavLogo = ({ subtitle = "Design System", expanded }: { subtitle?: string; expanded: boolean }) => {
2626
const pathVersion = process.env.NEXT_PUBLIC_SITE_VERSION;
2727
const isDev = process.env.NODE_ENV === "development";
2828

29-
return (
29+
return expanded ? (
3030
<DxcFlex alignItems="center">
3131
<LogoContainer>
3232
<DxcFlex alignItems="center" gap="var(--spacing-gap-s)">
@@ -47,6 +47,14 @@ const SidenavLogo = ({ subtitle = "Design System" }: { subtitle?: string }) => {
4747
size="small"
4848
/>
4949
</DxcFlex>
50+
) : (
51+
<Image
52+
alt="Halstack logo"
53+
height={32}
54+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
55+
src={halstackLogo}
56+
width={32}
57+
/>
5058
);
5159
};
5260

apps/website/screens/components/application-layout/code/ApplicationLayoutCodePage.tsx

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,6 @@ const ApplicationLayoutPropsTable = () => (
7575
</td>
7676
<td>-</td>
7777
</tr>
78-
<tr>
79-
<td>visibilityToggleLabel</td>
80-
<td>
81-
<TableCode>string</TableCode>
82-
</td>
83-
<td>Text to be placed next to the hamburger button that toggles the visibility of the sidenav.</td>
84-
<td>-</td>
85-
</tr>
8678
</tbody>
8779
</DxcTable>
8880
);
@@ -100,16 +92,6 @@ const sections = [
10092
</DxcParagraph>
10193
),
10294
},
103-
{
104-
title: "DxcApplicationLayout.useResponsiveSidenavVisibility",
105-
content: (
106-
<DxcParagraph>
107-
Custom hook that returns a function to manually change the visibility of the sidenav in responsive mode. This
108-
can be very useful for cases where a custom sidenav is being used and some of its inner elements can close it
109-
(for example, a navigation link).
110-
</DxcParagraph>
111-
),
112-
},
11395
{
11496
title: "Examples",
11597
subSections: [

apps/website/screens/components/contextual-menu/code/ContextualMenuCodePage.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const itemTypeString = `{
1212
icon?: string | SVG;
1313
label: string;
1414
onSelect?: () => void;
15-
selectedByDefault?: boolean;
15+
selected?: boolean;
1616
}`;
1717

1818
const groupItemTypeString = `{
@@ -80,6 +80,7 @@ const sections = [
8080
title: "Action menu",
8181
content: <Example example={actionMenu} defaultIsVisible />,
8282
},
83+
// TODO: We should remove this example as it is not the intended usage right? (Navigation is handled inside ApplicationLayout)
8384
{
8485
title: "Navigation menu",
8586
content: <Example example={navigationMenu} defaultIsVisible />,

apps/website/screens/components/sidenav/SidenavPageLayout.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ import { ReactNode } from "react";
77

88
const SidenavPageHeading = ({ children }: { children: ReactNode }) => {
99
const tabs = [
10-
{ label: "Overview", path: "/components/sidenav" },
11-
{ label: "Code", path: "/components/sidenav/code" },
10+
// { label: "Overview", path: "/components/sidenav" },
11+
// { label: "Code", path: "/components/sidenav/code" },
12+
{ label: "Code", path: "/components/sidenav" },
1213
];
13-
14+
// TODO: UPDATE DESCRIPTION WHEN OVERVIEW IS ADDED
1415
return (
1516
<DxcFlex direction="column" gap="var(--spacing-gap-xxl)">
1617
<PageHeading>

0 commit comments

Comments
 (0)