Skip to content

Commit 9050cf7

Browse files
Add routing library support to FooterLink, NavLink & Navbar
1 parent 24bc737 commit 9050cf7

File tree

10 files changed

+452
-41
lines changed

10 files changed

+452
-41
lines changed

changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ SciReactUI Changelog
1313

1414
### Changed
1515
- Breadcrumbs component takes optional linkComponent prop for page routing.
16+
- Navbar, NavLink and FooterLink will use routing library for links if provided with linkComponent and to props.
1617

1718
[v0.1.0] - 2025-04-10
1819
---------------------

readme.md

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,34 @@ root.render(
3232

3333
There are currently two themes, `GenericTheme` or `DiamondTheme`, but you can - and should - adapt your own.
3434

35-
The Breadcrumbs supports either static links or the use of a routing library.
36-
To use static links, omit the linkComponent prop and Breadcrumbs will use a Link component with standard href attributes.
35+
Navigation components support either static links (with href) or the use of a routing library (with linkComponent and to).
36+
For NavLink and FooterLink, if both linkComponent and to are provided, it will use linkComponent. If not, it falls back to using href.
37+
38+
An example with static links
39+
40+
```js
41+
<Navbar>
42+
<NavLinks>
43+
<NavLink href="/about">About</NavLink>
44+
</NavLinks>
45+
</Navbar>
46+
```
47+
48+
An example using react-router:
49+
50+
```js
51+
import { NavLink } from "react-router-dom";
52+
53+
<Navbar linkComponent={NavLink}>
54+
<NavLinks>
55+
<NavLink linkComponent={NavLink} to="/about">
56+
About
57+
</NavLink>
58+
</NavLinks>
59+
</Navbar>
60+
```
61+
62+
For Breadcrumbs, to use static links, omit the linkComponent prop and Breadcrumbs will use a Link component with standard href attributes.
3763

3864
```js
3965
import { Breadcrumbs } from "@diamondlightsource/sci-react-ui";
@@ -71,11 +97,11 @@ root.render(
7197
Then pass your library's corresponding Link component to Breadcrumbs in the linkComponent prop, for example:
7298

7399
```js
74-
import { Link } from "react-router-dom";
100+
import { NavLink } from "react-router-dom";
75101
import { Breadcrumbs } from "@diamondlightsource/sci-react-ui";
76102

77103
function App() {
78-
return <Breadcrumbs path={window.location.pathname} linkComponent={Link} />;
104+
return <Breadcrumbs path={window.location.pathname} linkComponent={NavLink} />;
79105
}
80106
export default App;
81107
```

src/components/navigation/Breadcrumbs.stories.tsx

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,6 @@
11
import { Meta, StoryObj } from "@storybook/react";
22
import { Breadcrumbs } from "./Breadcrumbs";
3-
import { Link as MuiLink } from "@mui/material";
4-
5-
interface MockLinkProps extends React.HTMLAttributes<HTMLAnchorElement> {
6-
to: string;
7-
children: React.ReactNode;
8-
[key: string]: unknown;
9-
}
10-
11-
const MockLink: React.FC<MockLinkProps> = ({ to, children, ...props }) => {
12-
const handleClick = (e: React.MouseEvent) => {
13-
e.preventDefault();
14-
console.log(`Mock navigation to: ${to}`);
15-
};
16-
17-
return (
18-
<MuiLink
19-
href={typeof to === "string" ? to : "#"}
20-
onClick={handleClick}
21-
underline="hover"
22-
color="inherit"
23-
{...props}
24-
>
25-
{children}
26-
</MuiLink>
27-
);
28-
};
3+
import { MockLink } from "../../utils/MockLink";
294

305
const meta: Meta<typeof Breadcrumbs> = {
316
title: "SciReactUI/Navigation/Breadcrumbs",

src/components/navigation/Footer.stories.tsx

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Meta, StoryObj } from "@storybook/react/*";
22
import { Footer, FooterLink, FooterLinks } from "./Footer";
3+
import { MockLink } from "../../utils/MockLink";
34

45
const meta: Meta<typeof Footer> = {
56
title: "SciReactUI/Navigation/Footer",
@@ -11,7 +12,24 @@ const meta: Meta<typeof Footer> = {
1112
export default meta;
1213
type Story = StoryObj<typeof meta>;
1314

14-
const footerLinks = [
15+
const routerFooterLinks = [
16+
<FooterLinks key="footer-links">
17+
<FooterLink to="home/TheMoon" key="the-moon" linkComponent={MockLink}>
18+
The Moon
19+
</FooterLink>
20+
<FooterLink to="home/Phobos" key="phobos" linkComponent={MockLink}>
21+
Phobos
22+
</FooterLink>
23+
<FooterLink to="home/Ganymede" key="ganymede" linkComponent={MockLink}>
24+
Ganymede
25+
</FooterLink>
26+
<FooterLink to="home/Titan" key="titan" linkComponent={MockLink}>
27+
Titan
28+
</FooterLink>
29+
</FooterLinks>,
30+
];
31+
32+
const staticFooterLinks = [
1533
<FooterLinks key="footer-links">
1634
<FooterLink href="#TheMoon" key="the-moon">
1735
The Moon
@@ -32,7 +50,15 @@ export const All: Story = {
3250
args: {
3351
logo: "theme",
3452
copyright: "Company",
35-
children: footerLinks,
53+
children: staticFooterLinks,
54+
},
55+
};
56+
57+
export const RouterLinks: Story = {
58+
args: {
59+
logo: "theme",
60+
copyright: "Company",
61+
children: routerFooterLinks,
3662
},
3763
};
3864

@@ -59,13 +85,13 @@ export const CopyrightAndLogo: Story = {
5985
export const LinksAndCopyright: Story = {
6086
args: {
6187
copyright: "Company",
62-
children: footerLinks,
88+
children: staticFooterLinks,
6389
},
6490
};
6591

6692
export const LinksOnly: Story = {
6793
args: {
68-
children: footerLinks,
94+
children: staticFooterLinks,
6995
},
7096
};
7197

src/components/navigation/Footer.test.tsx

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import "@testing-library/jest-dom";
1616
import dlsLogo from "../public/generic/logo-short.svg";
1717
import { Footer, FooterLink, FooterLinks } from "./Footer";
1818
import { renderWithProviders } from "../../__test-utils__/helpers";
19+
import { MemoryRouter, Link } from "react-router-dom";
1920

2021
describe("Footer logo and copyright", () => {
2122
test("Should render", async () => {
@@ -99,3 +100,88 @@ describe("Footer Links", () => {
99100
expect(link).toHaveAttribute("href", "link-two-href");
100101
});
101102
});
103+
104+
test("Should render FooterLink with linkComponent and 'to' prop", async () => {
105+
renderWithProviders(
106+
<MemoryRouter initialEntries={["/"]}>
107+
<Footer>
108+
<FooterLinks>
109+
<FooterLink linkComponent={Link} to="/about">
110+
About
111+
</FooterLink>
112+
</FooterLinks>
113+
</Footer>
114+
</MemoryRouter>,
115+
);
116+
117+
const link = await screen.findByText("About");
118+
expect(link).toBeInTheDocument();
119+
expect(link).toHaveAttribute("href", "/about");
120+
});
121+
122+
test("Should not render a valid link when only 'to' is provided without linkComponent", () => {
123+
renderWithProviders(
124+
<Footer>
125+
<FooterLinks>
126+
<FooterLink to="/about">About</FooterLink>
127+
</FooterLinks>
128+
</Footer>,
129+
);
130+
131+
const link = screen.getByText("About");
132+
expect(link).toBeInTheDocument();
133+
expect(link).not.toHaveAttribute("href", "/about");
134+
});
135+
136+
test("Should fall back to href when linkComponent is provided without 'to'", () => {
137+
renderWithProviders(
138+
<MemoryRouter>
139+
<Footer>
140+
<FooterLinks>
141+
<FooterLink linkComponent={Link} href="/about">
142+
About
143+
</FooterLink>
144+
</FooterLinks>
145+
</Footer>
146+
</MemoryRouter>,
147+
);
148+
149+
const link = screen.getByText("About");
150+
expect(link).toBeInTheDocument();
151+
expect(link.tagName).toBe("A");
152+
expect(link).toHaveAttribute("href", "/about");
153+
});
154+
155+
test("Should use href when both 'href' and 'to' are provided without linkComponent", () => {
156+
renderWithProviders(
157+
<Footer>
158+
<FooterLinks>
159+
<FooterLink href="/about" to="/somewhereElse">
160+
About
161+
</FooterLink>
162+
</FooterLinks>
163+
</Footer>,
164+
);
165+
166+
const link = screen.getByText("About");
167+
expect(link).toBeInTheDocument();
168+
expect(link).toHaveAttribute("href", "/about");
169+
});
170+
171+
test("Should use 'to' when both 'href' and 'to' are provided with linkComponent", () => {
172+
renderWithProviders(
173+
<MemoryRouter>
174+
<Footer>
175+
<FooterLinks>
176+
<FooterLink linkComponent={Link} href="/somewhereElse" to="/about">
177+
About
178+
</FooterLink>
179+
</FooterLinks>
180+
</Footer>
181+
</MemoryRouter>,
182+
);
183+
184+
const link = screen.getByText("About");
185+
expect(link).toBeInTheDocument();
186+
expect(link).toHaveAttribute("href", "/about");
187+
});

src/components/navigation/Footer.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,36 @@ const FooterLinks = ({ children, ...props }: FooterLinksProps) => {
4242
);
4343
};
4444

45-
const FooterLink = ({ children, ...props }: LinkProps) => {
45+
interface FooterLinkProps extends LinkProps {
46+
children: React.ReactNode;
47+
linkComponent?: React.ElementType;
48+
to?: string;
49+
href?: string;
50+
}
51+
52+
const FooterLink = ({
53+
children,
54+
linkComponent,
55+
to,
56+
href,
57+
...props
58+
}: FooterLinkProps) => {
4659
const theme = useTheme();
4760

61+
const shouldUseLinkComponent = linkComponent && to;
62+
63+
const linkProps = shouldUseLinkComponent
64+
? { component: linkComponent, to }
65+
: { href };
66+
4867
return (
4968
<Link
69+
{...linkProps}
5070
sx={{
5171
"&:hover": {
5272
color: theme.vars.palette.secondary.main,
5373
borderBottom: "solid 4px",
74+
textDecoration: "none",
5475
},
5576
textDecoration: "none",
5677
color: theme.palette.primary.contrastText,

src/components/navigation/Navbar.stories.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import logoImageDark from "../../public/generic/logo-dark.svg";
66
import logoImageLight from "../../public/generic/logo-light.svg";
77
import { ColourSchemeButton } from "../controls/ColourSchemeButton";
88
import { User } from "../controls/User";
9+
import { MockLink } from "../../utils/MockLink";
910

1011
const meta: Meta<typeof Navbar> = {
1112
title: "SciReactUI/Navigation/Navbar",
@@ -81,6 +82,21 @@ export const Links: Story = {
8182
},
8283
};
8384

85+
export const RouterLinks: Story = {
86+
args: {
87+
children: (
88+
<NavLinks key="links">
89+
<NavLink to="/home/first" key="first" linkComponent={MockLink}>
90+
First
91+
</NavLink>
92+
<NavLink to="/home/second" key="second" linkComponent={MockLink}>
93+
Second
94+
</NavLink>
95+
</NavLinks>
96+
),
97+
},
98+
};
99+
84100
export const LinksAndUser: Story = {
85101
args: {
86102
children: [

0 commit comments

Comments
 (0)