Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e4b62fe
First approach to new Sidenav implementation
Mil4n0r Oct 10, 2025
e4b7043
First version of the new SideNav
Mil4n0r Oct 16, 2025
40a4fe1
Merge branch 'master' of github.com:dxc-technology/halstack-react int…
Mil4n0r Oct 16, 2025
9a887b3
Added responsiveness behavior
Mil4n0r Oct 17, 2025
83f634b
Made context props optional to prevent typing warnings
Mil4n0r Oct 20, 2025
323a29b
Cleaned types definition file
Mil4n0r Oct 20, 2025
d90b881
Added tests and stories and fixed types
Mil4n0r Oct 20, 2025
9713d2c
Made changes in sidenav API to fit our documentation website
Mil4n0r Oct 23, 2025
16eba2e
Created new TreeNavigation
Mil4n0r Oct 23, 2025
57645fa
Restored contextualmenu
Mil4n0r Oct 23, 2025
b5819f4
Fixed problem in test
Mil4n0r Oct 23, 2025
9e49a38
Fixed problem in test
Mil4n0r Oct 23, 2025
d5153b8
Fixed circular dependency
Mil4n0r Oct 23, 2025
5bacd9c
Fixed accessibility issues
Mil4n0r Oct 23, 2025
adafdbd
Fixed accessibility problems and cleared code
Mil4n0r Oct 23, 2025
b9a7a73
Fixed accessibility problems for icons
Mil4n0r Oct 23, 2025
5b31f4f
Fixed accessibility problems for collapsed view
Mil4n0r Oct 23, 2025
1b25619
Added custom hook for groupitems
Mil4n0r Oct 24, 2025
5a4811f
Simplified all the common logic for ContextualMenu and NavigationMenu
Mil4n0r Oct 24, 2025
69eb0b9
Added TODO
Mil4n0r Oct 24, 2025
8f618fc
Fixed typings
Mil4n0r Oct 24, 2025
f54fce4
Fixed types export
Mil4n0r Oct 24, 2025
5250054
Fixed some styles problems in ItemAction for ContextualMenu
Mil4n0r Oct 24, 2025
e214f0c
Restored label centering
Mil4n0r Oct 24, 2025
76ac354
Fixed uncentered text with no badge/icon
Mil4n0r Oct 24, 2025
e34e4cc
Merge branch 'master' into Mil4n0r/tree_navigation
Mil4n0r Nov 3, 2025
a263be4
Used navItems instead of items for the API
Mil4n0r Nov 4, 2025
27907dd
Replaced items in test
Mil4n0r Nov 4, 2025
aea806b
Merge branch 'Mil4n0r/tree_navigation' of github.com:dxc-technology/h…
Mil4n0r Nov 4, 2025
95c892f
Fixed app according to new API
Mil4n0r Nov 4, 2025
0836347
Added tests, docsite sidenav and new API improvements
Mil4n0r Nov 6, 2025
e133d79
Fixed problem with build
Mil4n0r Nov 6, 2025
39ab15f
Added section support for responsive mode
Mil4n0r Nov 7, 2025
04b1de3
Added documentation and sorted props
Mil4n0r Nov 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 99 additions & 59 deletions apps/website/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { ReactElement, ReactNode, useMemo, useState } from "react";
import { ReactElement, ReactNode, useEffect, useMemo, useState } from "react";
import type { NextPage } from "next";
import type { AppProps } from "next/app";
import Head from "next/head";
import { DxcApplicationLayout, DxcTextInput, DxcToastsQueue } from "@dxc-technology/halstack-react";
import SidenavLogo from "@/common/sidenav/SidenavLogo";
import MainContent from "@/common/MainContent";
import { useRouter } from "next/router";
import { LinksSectionDetails, LinksSections } from "@/common/pagesList";
import Link from "next/link";
import StatusBadge from "@/common/StatusBadge";
import "../global-styles.css";
import createCache, { EmotionCache } from "@emotion/cache";
import { CacheProvider } from "@emotion/react";
import { usePathname } from "next/navigation";
import Link from "next/link";
import { GroupItem, Item, Section } from "../../../packages/lib/src/base-menu/types";
import { isGroupItem } from "../../../packages/lib/src/base-menu/utils";
import SidenavLogo from "@/common/sidenav/SidenavLogo";

type NextPageWithLayout = NextPage & {
getLayout?: (_page: ReactElement) => ReactNode;
Expand All @@ -26,73 +29,110 @@ const clientSideEmotionCache = createCache({ key: "css", prepend: true });
export default function App({ Component, pageProps, emotionCache = clientSideEmotionCache }: AppPropsWithLayout) {
const getLayout = Component.getLayout || ((page) => page);
const componentWithLayout = getLayout(<Component {...pageProps} />);
const router = useRouter();
const pathname = usePathname();
const [filter, setFilter] = useState("");
const { asPath: currentPath } = useRouter();
const filteredLinks = useMemo(() => {
const filtered: LinksSectionDetails[] = [];
LinksSections.map((section) => {
const sectionFilteredLinks = section?.links.filter((link) =>
link.label.toLowerCase().includes(filter.toLowerCase())
);
if (sectionFilteredLinks.length) {
filtered.push({ label: section.label, links: sectionFilteredLinks });
}
});
return filtered;
}, [filter]);
const [isExpanded, setIsExpanded] = useState(true);

const filterSections = (sections: Section[], query: string): Section[] => {
const q = query.trim().toLowerCase();
if (!q) return sections;

const filterItem = (item: Item | GroupItem): Item | GroupItem | null => {
const labelMatches = item.label.toLowerCase().includes(q);

if (!isGroupItem(item)) return labelMatches ? item : null;

const items = item.items.reduce<(Item | GroupItem)[]>((acc, child) => {
const filtered = filterItem(child);
if (filtered) acc.push(filtered);
return acc;
}, []);

const matchPaths = (linkPath: string) => {
const desiredPaths = [linkPath, `${linkPath}/code`];
const pathToBeMatched = currentPath?.split("#")[0]?.slice(0, -1);
return pathToBeMatched ? desiredPaths.includes(pathToBeMatched) : false;
return labelMatches || items.length ? { ...item, items } : null;
};

return sections.reduce<Section[]>((acc, section) => {
const items = section.items.reduce<(Item | GroupItem)[]>((acc, item) => {
const filtered = filterItem(item);
if (filtered) acc.push(filtered);
return acc;
}, []);
if (items.length) acc.push({ ...section, items });
return acc;
}, []);
};

const mapLinksToGroupItems = (sections: LinksSectionDetails[]): Section[] => {
const matchPaths = (linkPath: string) => {
const desiredPaths = [linkPath, `${linkPath}/code`];
const pathToBeMatched = pathname?.split("#")[0]?.slice(0, -1);
return pathToBeMatched ? desiredPaths.includes(pathToBeMatched) : false;
};

return sections.map((section) => ({
title: section.label,
items: section.links.map((link) => ({
label: link.label,
href: link.path,
selected: matchPaths(link.path),
...(link.status && {
badge: link.status !== "stable" ? <StatusBadge hasTitle status={link.status} /> : undefined,
}),
renderItem: ({ children }: { children: ReactNode }) => (
<Link key={link.path} href={link.path} passHref legacyBehavior>
{children}
</Link>
),
})),
}));
};

useEffect(() => {
const paths = [...new Set(LinksSections.flatMap((s) => s.links.map((l) => l.path)))];
const prefetchPaths = async () => {
for (const path of paths) {
await router.prefetch(path);
}
};
void prefetchPaths();
}, []);

// TODO: ADD NEW CATEGORIZATION

const filteredSections = useMemo(() => {
const sections = mapLinksToGroupItems(LinksSections);
return filterSections(sections, filter);
}, [filter]);

return (
<CacheProvider value={emotionCache}>
<Head>
<link rel="icon" type="image/png" sizes="32x32" href="/favicon.png" />
</Head>
<DxcApplicationLayout
visibilityToggleLabel="Menu"
sidenav={
<DxcApplicationLayout.SideNav title={<SidenavLogo />}>
<DxcApplicationLayout.SideNav.Section>
<DxcTextInput
placeholder="Search docs"
value={filter}
onChange={({ value }: { value: string }) => {
setFilter(value);
}}
size="fillParent"
clearable
margin={{
top: "large",
bottom: "large",
right: "medium",
left: "medium",
}}
/>
</DxcApplicationLayout.SideNav.Section>
{filteredLinks?.map(({ label, links }) => (
<DxcApplicationLayout.SideNav.Section key={label}>
<DxcApplicationLayout.SideNav.Group title={label}>
{links.map(({ label, path, status }) => (
<Link key={`${label}-${path}`} href={path} passHref legacyBehavior>
<DxcApplicationLayout.SideNav.Link selected={matchPaths(path)}>
{label}
{status && status !== "stable" && <StatusBadge hasTitle status={status} />}
</DxcApplicationLayout.SideNav.Link>
</Link>
))}
</DxcApplicationLayout.SideNav.Group>
</DxcApplicationLayout.SideNav.Section>
))}
<DxcApplicationLayout.SideNav.Section>
<DxcApplicationLayout.SideNav.Link href="https://github.com/dxc-technology/halstack-react" newWindow>
GitHub
</DxcApplicationLayout.SideNav.Link>
</DxcApplicationLayout.SideNav.Section>
</DxcApplicationLayout.SideNav>
<DxcApplicationLayout.Sidenav
navItems={filteredSections}
branding={<SidenavLogo expanded={isExpanded} />}
topContent={
isExpanded && (
<DxcTextInput
placeholder="Search docs"
value={filter}
onChange={({ value }: { value: string }) => {
setFilter(value);
}}
size="fillParent"
clearable
/>
)
}
expanded={isExpanded}
onExpandedChange={() => {
setIsExpanded((currentlyExpanded) => !currentlyExpanded);
}}
/>
}
>
<DxcApplicationLayout.Main>
Expand Down
40 changes: 32 additions & 8 deletions apps/website/screens/common/StatusBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ComponentStatus } from "./pagesList";
type StatusBadgeProps = {
hasTitle?: boolean;
status: ComponentStatus | "required";
// reduced?: boolean;
};

const getBadgeColor = (status: StatusBadgeProps["status"]) => {
Expand Down Expand Up @@ -40,13 +41,36 @@ const getBadgeTitle = (status: StatusBadgeProps["status"]) => {
}
};

const StatusBadge = ({ hasTitle = false, status }: StatusBadgeProps) => (
<DxcBadge
label={status[0]?.toUpperCase() + status.slice(1)}
color={getBadgeColor(status)}
title={hasTitle ? getBadgeTitle(status) : undefined}
size="small"
/>
);
// TODO: enable icon when the status badge supports reduced version
// const getBadgeIcon = (status: StatusBadgeProps["status"]) => {
// switch (status) {
// case "required":
// return "warning_amber";
// case "experimental":
// return "science";
// case "new":
// return "new_releases";
// case "stable":
// return "check_circle";
// case "legacy":
// return "history";
// case "deprecated":
// return "highlight_off";
// default:
// return "";
// }
// };

const StatusBadge = ({ hasTitle = false, status }: StatusBadgeProps) => {
return (
<DxcBadge
label={status[0]?.toUpperCase() + status.slice(1)}
color={getBadgeColor(status)}
title={hasTitle ? getBadgeTitle(status) : undefined}
size="small"
// icon={reduced ? getBadgeIcon(status) : undefined}
/>
);
};

export default StatusBadge;
12 changes: 10 additions & 2 deletions apps/website/screens/common/sidenav/SidenavLogo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ const Subtitle = styled.div`
font-family: var(--typography-font-family);
`;

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

return (
return expanded ? (
<DxcFlex alignItems="center">
<LogoContainer>
<DxcFlex alignItems="center" gap="var(--spacing-gap-s)">
Expand All @@ -47,6 +47,14 @@ const SidenavLogo = ({ subtitle = "Design System" }: { subtitle?: string }) => {
size="small"
/>
</DxcFlex>
) : (
<Image
alt="Halstack logo"
height={32}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
src={halstackLogo}
width={32}
/>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,6 @@ const ApplicationLayoutPropsTable = () => (
</td>
<td>-</td>
</tr>
<tr>
<td>visibilityToggleLabel</td>
<td>
<TableCode>string</TableCode>
</td>
<td>Text to be placed next to the hamburger button that toggles the visibility of the sidenav.</td>
<td>-</td>
</tr>
</tbody>
</DxcTable>
);
Expand All @@ -100,16 +92,6 @@ const sections = [
</DxcParagraph>
),
},
{
title: "DxcApplicationLayout.useResponsiveSidenavVisibility",
content: (
<DxcParagraph>
Custom hook that returns a function to manually change the visibility of the sidenav in responsive mode. This
can be very useful for cases where a custom sidenav is being used and some of its inner elements can close it
(for example, a navigation link).
</DxcParagraph>
),
},
{
title: "Examples",
subSections: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const itemTypeString = `{
icon?: string | SVG;
label: string;
onSelect?: () => void;
selectedByDefault?: boolean;
selected?: boolean;
}`;

const groupItemTypeString = `{
Expand Down Expand Up @@ -80,6 +80,7 @@ const sections = [
title: "Action menu",
content: <Example example={actionMenu} defaultIsVisible />,
},
// TODO: We should remove this example as it is not the intended usage right? (Navigation is handled inside ApplicationLayout)
{
title: "Navigation menu",
content: <Example example={navigationMenu} defaultIsVisible />,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const SidenavPageHeading = ({ children }: { children: ReactNode }) => {
{ label: "Overview", path: "/components/sidenav" },
{ label: "Code", path: "/components/sidenav/code" },
];

// TODO: UPDATE DESCRIPTION
return (
<DxcFlex direction="column" gap="var(--spacing-gap-xxl)">
<PageHeading>
Expand Down
Loading
Loading