Skip to content

Commit c1eff0a

Browse files
authored
Add support for expandable nav groups in route definition (#96)
1 parent 8782bc6 commit c1eff0a

File tree

4 files changed

+88
-10
lines changed

4 files changed

+88
-10
lines changed

src/app/AppLayout/AppLayout.tsx

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import * as React from 'react';
2-
import { NavLink } from 'react-router-dom';
2+
import { NavLink, useLocation } from 'react-router-dom';
33
import {
44
Nav,
55
NavList,
66
NavItem,
7+
NavExpandable,
78
Page,
89
PageHeader,
910
PageSidebar,
1011
SkipToContent
1112
} from '@patternfly/react-core';
12-
import { routes } from '@app/routes';
13+
import { routes, IAppRoute, IAppRouteGroup } from '@app/routes';
1314

1415
interface IAppLayout {
1516
children: React.ReactNode;
@@ -42,14 +43,33 @@ const AppLayout: React.FunctionComponent<IAppLayout> = ({children}) => {
4243
/>
4344
);
4445

46+
const location = useLocation();
47+
48+
const renderNavItem = (route: IAppRoute, index: number) => (
49+
<NavItem key={`${route.label}-${index}`} id={`${route.label}-${index}`}>
50+
<NavLink exact to={route.path} activeClassName="pf-m-current">
51+
{route.label}
52+
</NavLink>
53+
</NavItem>
54+
);
55+
56+
const renderNavGroup = (group: IAppRouteGroup, groupIndex: number) => (
57+
<NavExpandable
58+
key={`${group.label}-${groupIndex}`}
59+
id={`${group.label}-${groupIndex}`}
60+
title={group.label}
61+
isActive={group.routes.some((route) => route.path === location.pathname)}
62+
>
63+
{group.routes.map((route, idx) => route.label && renderNavItem(route, idx))}
64+
</NavExpandable>
65+
);
66+
4567
const Navigation = (
4668
<Nav id="nav-primary-simple" theme="dark">
4769
<NavList id="nav-list-simple">
48-
{routes.map((route, idx) => route.label && (
49-
<NavItem key={`${route.label}-${idx}`} id={`${route.label}-${idx}`}>
50-
<NavLink exact to={route.path} activeClassName="pf-m-current">{route.label}</NavLink>
51-
</NavItem>
52-
))}
70+
{routes.map(
71+
(route, idx) => route.label && (!route.routes ? renderNavItem(route, idx) : renderNavGroup(route, idx))
72+
)}
5373
</NavList>
5474
</Nav>
5575
);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import * as React from 'react';
2+
import { PageSection, Title } from '@patternfly/react-core';
3+
4+
const GeneralSettings: React.FunctionComponent = () => (
5+
<PageSection>
6+
<Title headingLevel="h1" size="lg">
7+
General Settings Page Title
8+
</Title>
9+
</PageSection>
10+
);
11+
12+
export { GeneralSettings };
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import * as React from 'react';
2+
import { PageSection, Title } from '@patternfly/react-core';
3+
4+
const ProfileSettings: React.FunctionComponent = () => (
5+
<PageSection>
6+
<Title headingLevel="h1" size="lg">
7+
Profile Settings Page Title
8+
</Title>
9+
</PageSection>
10+
);
11+
12+
export { ProfileSettings };

src/app/routes.tsx

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { Alert, PageSection } from '@patternfly/react-core';
44
import { DynamicImport } from '@app/DynamicImport';
55
import { accessibleRouteChangeHandler } from '@app/utils/utils';
66
import { Dashboard } from '@app/Dashboard/Dashboard';
7+
import { GeneralSettings } from '@app/Settings/General/GeneralSettings';
8+
import { ProfileSettings } from '@app/Settings/Profile/ProfileSettings';
79
import { NotFound } from '@app/NotFound/NotFound';
810
import { useDocumentTitle } from '@app/utils/useDocumentTitle';
911
import { LastLocationProvider, useLastLocation } from 'react-router-last-location';
@@ -38,17 +40,25 @@ const Support = (routeProps: RouteComponentProps): React.ReactElement => {
3840
};
3941

4042
export interface IAppRoute {
41-
label?: string;
43+
label?: string; // Excluding the label will exclude the route from the nav sidebar in AppLayout
4244
/* eslint-disable @typescript-eslint/no-explicit-any */
4345
component: React.ComponentType<RouteComponentProps<any>> | React.ComponentType<any>;
4446
/* eslint-enable @typescript-eslint/no-explicit-any */
4547
exact?: boolean;
4648
path: string;
4749
title: string;
4850
isAsync?: boolean;
51+
routes?: undefined;
4952
}
5053

51-
const routes: IAppRoute[] = [
54+
export interface IAppRouteGroup {
55+
label: string;
56+
routes: IAppRoute[];
57+
}
58+
59+
export type AppRouteConfig = IAppRoute | IAppRouteGroup;
60+
61+
const routes: AppRouteConfig[] = [
5262
{
5363
component: Dashboard,
5464
exact: true,
@@ -64,6 +74,25 @@ const routes: IAppRoute[] = [
6474
path: '/support',
6575
title: 'PatternFly Seed | Support Page',
6676
},
77+
{
78+
label: 'Settings',
79+
routes: [
80+
{
81+
component: GeneralSettings,
82+
exact: true,
83+
label: 'General',
84+
path: '/settings/general',
85+
title: 'PatternFly Seed | General Settings',
86+
},
87+
{
88+
component: ProfileSettings,
89+
exact: true,
90+
label: 'Profile',
91+
path: '/settings/profile',
92+
title: 'PatternFly Seed | Profile Settings',
93+
},
94+
],
95+
},
6796
];
6897

6998
// a custom hook for sending focus to the primary content container
@@ -97,10 +126,15 @@ const PageNotFound = ({ title }: { title: string }) => {
97126
return <Route component={NotFound} />;
98127
};
99128

129+
const flattenedRoutes: IAppRoute[] = routes.reduce(
130+
(flattened, route) => [...flattened, ...(route.routes ? route.routes : [route])],
131+
[] as IAppRoute[]
132+
);
133+
100134
const AppRoutes = (): React.ReactElement => (
101135
<LastLocationProvider>
102136
<Switch>
103-
{routes.map(({ path, exact, component, title, isAsync }, idx) => (
137+
{flattenedRoutes.map(({ path, exact, component, title, isAsync }, idx) => (
104138
<RouteWithTitleUpdates
105139
path={path}
106140
exact={exact}

0 commit comments

Comments
 (0)