Skip to content

Commit 97a0416

Browse files
Add Navbar slots
1 parent 35aea60 commit 97a0416

File tree

5 files changed

+251
-70
lines changed

5 files changed

+251
-70
lines changed

changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ SciReactUI Changelog
1414
### Changed
1515
- Breadcrumbs component takes optional linkComponent prop for page routing.
1616
- Navbar, NavLink and FooterLink will use routing library for links if provided with linkComponent and to props.
17+
- Navbar uses slots for positioning elements. Breaking change: elements must now use rightSlot for positioning to the far right.
1718

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

readme.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,27 +105,32 @@ function App() {
105105
}
106106
export default App;
107107
```
108+
The Navbar component supports multiple slot props for flexible layout customization: leftSlot, centreSlot, rightSlot.
109+
If a logo is defined (either via the logo prop or from the theme), the layout will arrange elements in the following order from left to right: logo, leftSlot, rightSlot.
110+
The centreSlot is absolutely positioned at 50% horizontally, which means it stays centered regardless of the content on the left or right. However, if the content in the left or right slots is too wide, it may overlap with the centre slot.
108111

109-
There are various other components, here's an example of how to use the NavBar:
112+
Any children passed to the Navbar (NavLinks in the following example) will be placed in a horizontal Stack after the leftSlot.
110113

114+
To control the width of the Navbar contents, containerWidth can be passed which sets the maxWidth property of a Container. This can be set to false to not constrain the contents or to a Breakpoint e.g. 'sm' where 'lg' is the default.
111115
```js
112116
import { Container, Typography } from "@mui/material";
113117
import { Navbar, NavLink, NavLinks } from "@diamondlightsource/sci-react-ui";
114118

115119
function App() {
116120
return (
117121
<>
118-
<Navbar>
122+
<Navbar
123+
leftSlot={<Typography>left</Typography>}
124+
centreSlot={<Typography>centre</Typography>}
125+
rightSlot={<Typography>right</Typography>}
126+
containerWidth={false}
127+
>
119128
<NavLinks key="links">
120129
<NavLink href="#" key="first">
121130
A link
122131
</NavLink>
123132
</NavLinks>
124133
</Navbar>
125-
<Container>
126-
<Typography variant="h2">Scientific UI Collection</Typography>
127-
<Typography>A collection of science based React components.</Typography>
128-
</Container>
129134
</>
130135
);
131136
}

src/components/navigation/Navbar.stories.tsx

Lines changed: 113 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -19,43 +19,43 @@ type Story = StoryObj<typeof meta>;
1919

2020
export const All: Story = {
2121
args: {
22-
children: [
23-
<NavLinks key="links">
24-
<NavLink href="#Mercury" key="mercury">
25-
Mercury
26-
</NavLink>
27-
<NavLink href="#Venus" key="venus">
28-
Venus
29-
</NavLink>
30-
<NavLink href="#Earth" key="earth">
31-
Earth
32-
</NavLink>
33-
<NavLink href="#Mars" key="mars">
34-
Mars
35-
</NavLink>
36-
</NavLinks>,
37-
<User
38-
key="user"
39-
onLogin={() => {}}
40-
onLogout={() => {}}
41-
user={{ name: "Name", fedid: "FedID" }}
42-
color={"white"}
43-
/>,
44-
<ColourSchemeButton key="colourScheme" />,
45-
],
22+
rightSlot: (
23+
<>
24+
<User
25+
key="user"
26+
onLogin={() => {}}
27+
onLogout={() => {}}
28+
user={{ name: "Name", fedid: "FedID" }}
29+
color={"white"}
30+
/>
31+
<ColourSchemeButton key="colourScheme" />,
32+
</>
33+
),
34+
children: (
35+
<>
36+
<NavLinks key="links">
37+
<NavLink href="#Mercury" key="mercury">
38+
Mercury
39+
</NavLink>
40+
<NavLink href="#Venus" key="venus">
41+
Venus
42+
</NavLink>
43+
<NavLink href="#Earth" key="earth">
44+
Earth
45+
</NavLink>
46+
<NavLink href="#Mars" key="mars">
47+
Mars
48+
</NavLink>
49+
</NavLinks>
50+
</>
51+
),
4652
logo: "theme",
4753
},
4854
};
4955

50-
export const WithLogin: Story = {
51-
args: {
52-
children: <User onLogin={() => {}} onLogout={() => {}} user={null} />,
53-
},
54-
};
55-
5656
export const WithUser: Story = {
5757
args: {
58-
children: (
58+
rightSlot: (
5959
<User
6060
key="user"
6161
onLogin={() => {}}
@@ -99,23 +99,27 @@ export const RouterLinks: Story = {
9999

100100
export const LinksAndUser: Story = {
101101
args: {
102-
children: [
103-
<NavLinks key="links">
104-
<NavLink href="#" key="first">
105-
First
106-
</NavLink>
107-
<NavLink href="#" key="second">
108-
Second
109-
</NavLink>
110-
</NavLinks>,
102+
rightSlot: (
111103
<User
112104
key="user"
113105
onLogin={() => {}}
114106
onLogout={() => {}}
115107
user={{ name: "Name", fedid: "FedID" }}
116108
color={"white"}
117-
/>,
118-
],
109+
/>
110+
),
111+
children: (
112+
<>
113+
<NavLinks key="links">
114+
<NavLink href="#" key="first">
115+
First
116+
</NavLink>
117+
<NavLink href="#" key="second">
118+
Second
119+
</NavLink>
120+
</NavLinks>
121+
</>
122+
),
119123
},
120124
};
121125

@@ -177,6 +181,73 @@ export const CustomChildElement: Story = {
177181
},
178182
};
179183

184+
export const LinksInSlot: Story = {
185+
args: {
186+
rightSlot: (
187+
<>
188+
<User
189+
key="user"
190+
onLogin={() => {}}
191+
onLogout={() => {}}
192+
user={{ name: "Name", fedid: "FedID" }}
193+
color={"white"}
194+
/>
195+
<ColourSchemeButton key="colourScheme" />,
196+
</>
197+
),
198+
leftSlot: (
199+
<>
200+
<NavLinks key="links">
201+
<NavLink href="#Mercury" key="mercury">
202+
Mercury
203+
</NavLink>
204+
<NavLink href="#Venus" key="venus">
205+
Venus
206+
</NavLink>
207+
<NavLink href="#Earth" key="earth">
208+
Earth
209+
</NavLink>
210+
<NavLink href="#Mars" key="mars">
211+
Mars
212+
</NavLink>
213+
</NavLinks>
214+
</>
215+
),
216+
logo: "theme",
217+
},
218+
};
219+
220+
export const AllSlots: Story = {
221+
args: {
222+
leftSlot: (
223+
<NavLink to="left" linkComponent={MockLink}>
224+
Left
225+
</NavLink>
226+
),
227+
centreSlot: (
228+
<NavLink to="centre" linkComponent={MockLink}>
229+
Centre
230+
</NavLink>
231+
),
232+
rightSlot: (
233+
<NavLink to="right" linkComponent={MockLink}>
234+
Right
235+
</NavLink>
236+
),
237+
children: (
238+
<NavLink to="children" linkComponent={MockLink}>
239+
Children
240+
</NavLink>
241+
),
242+
logo: "theme",
243+
},
244+
};
245+
export const WithLogin: Story = {
246+
args: {
247+
children: <User onLogin={() => {}} onLogout={() => {}} user={null} />,
248+
},
249+
};
250+
180251
export const Empty: Story = {
181252
args: {},
182253
};

src/components/navigation/Navbar.test.tsx

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,3 +274,73 @@ it("should use 'to' when both 'href' and 'to' are provided with linkComponent",
274274
expect(link).toBeInTheDocument();
275275
expect(link).toHaveAttribute("href", "/about");
276276
});
277+
278+
it("renders leftSlot", () => {
279+
renderWithProviders(
280+
<Navbar leftSlot={<div data-testid="left-slot">Right Slot</div>} />,
281+
);
282+
expect(screen.getByTestId("left-slot")).toBeInTheDocument();
283+
});
284+
285+
it("renders centreSlot", () => {
286+
renderWithProviders(
287+
<Navbar centreSlot={<div data-testid="centre-slot">Centre Slot</div>} />,
288+
);
289+
expect(screen.getByTestId("centre-slot")).toBeInTheDocument();
290+
});
291+
292+
it("renders rightSlot", () => {
293+
renderWithProviders(
294+
<Navbar rightSlot={<div data-testid="right-slot">Right Slot</div>} />,
295+
);
296+
expect(screen.getByTestId("right-slot")).toBeInTheDocument();
297+
});
298+
299+
it("renders all slots together", () => {
300+
renderWithProviders(
301+
<Navbar
302+
leftSlot={<div data-testid="left-slot">Right</div>}
303+
centreSlot={<div data-testid="centre-slot">Centre</div>}
304+
rightSlot={<div data-testid="right-slot">Right Slot</div>}
305+
/>,
306+
);
307+
expect(screen.getByTestId("left-slot")).toBeInTheDocument();
308+
expect(screen.getByTestId("centre-slot")).toBeInTheDocument();
309+
expect(screen.getByTestId("right-slot")).toBeInTheDocument();
310+
});
311+
describe("Navbar Slot Positioning", () => {
312+
it("centreSlot should be centred", () => {
313+
renderWithProviders(
314+
<Navbar centreSlot={<div data-testid="centre-slot">Centre</div>} />,
315+
);
316+
const centreSlot = screen.getByTestId("centre-slot");
317+
const parent = centreSlot.parentElement;
318+
expect(parent).toHaveStyle({
319+
position: "absolute",
320+
left: "50%",
321+
transform: "translateX(-50%)",
322+
});
323+
});
324+
325+
it("rightSlot should be aligned to the end of the row", () => {
326+
renderWithProviders(
327+
<Navbar rightSlot={<div data-testid="right-slot">Right</div>} />,
328+
);
329+
const rightSlot = screen.getByTestId("right-slot");
330+
const stack = rightSlot.closest(".MuiStack-root");
331+
expect(stack).toHaveStyle("justify-content: space-between");
332+
});
333+
334+
it("logo should be vertically centred", () => {
335+
renderWithProviders(
336+
<Navbar
337+
logo={{ src: "/logo.svg", alt: "Home" }}
338+
leftSlot={<div data-testid="left-slot">Left</div>}
339+
/>,
340+
);
341+
const logo = screen.getByRole("link");
342+
const logoBox = logo.parentElement;
343+
expect(logoBox).toHaveStyle("display: flex");
344+
expect(logoBox).toHaveStyle("align-items: center");
345+
});
346+
});

0 commit comments

Comments
 (0)