Skip to content
2 changes: 2 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ SciReactUI Changelog
- Navbar, NavLink and FooterLink will use routing library for links if provided with linkComponent and to props.
- Navbar uses slots for positioning elements. Breaking change: elements must now use rightSlot for positioning to the far right.
- User can take additional menu items through the menuItems prop.
- Footer uses slots for positioning elements. Breaking change: elements must now use rightSlot for positioning to the far right.


[v0.1.0] - 2025-04-10
---------------------
Expand Down
7 changes: 7 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,13 @@ function App() {
export default App;
```

The Footer component supports multiple slot props, the same as Navbar (leftSlot, centreSlot, rightSlot).

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: leftSlot, rightSlot, logo.
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.

Any children passed to the Footer will be placed in a horizontal Stack after the leftSlot.

### Documentation

Documentation is created with Storybook.
Expand Down
89 changes: 75 additions & 14 deletions src/components/navigation/Footer.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const routerFooterLinks = [
</FooterLinks>,
];

const staticFooterLinks = [
const staticFooterLinks = (
<FooterLinks key="footer-links">
<FooterLink href="#TheMoon" key="the-moon">
The Moon
Expand All @@ -43,8 +43,8 @@ const staticFooterLinks = [
<FooterLink href="#Titan" key="titan">
Titan
</FooterLink>
</FooterLinks>,
];
</FooterLinks>
);

export const All: Story = {
args: {
Expand All @@ -54,6 +54,58 @@ export const All: Story = {
},
};

export const AllSlots: Story = {
args: {
logo: "theme",
copyright: "Company",
leftSlot: (
<FooterLinks key="footer-links-left">
<FooterLink href="#Left" key="left">
Left
</FooterLink>
</FooterLinks>
),
children: (
<FooterLinks key="footer-links-children">
<FooterLink href="#Children" key="children">
Children
</FooterLink>
</FooterLinks>
),
rightSlot: (
<FooterLinks key="footer-links-right">
<FooterLink href="#Right" key="right">
Right
</FooterLink>
</FooterLinks>
),
centreSlot: (
<FooterLinks key="footer-links-centre">
<FooterLink href="#Centre" key="centre">
Centre
</FooterLink>
</FooterLinks>
),
},
};

export const RightSlot: Story = {
args: {
logo: "theme",
copyright: "Company",
rightSlot: (
<FooterLinks key="footer-links">
<FooterLink href="#TheMoon" key="the-moon">
The Moon
</FooterLink>
<FooterLink href="#Phobos" key="phobos">
Phobos
</FooterLink>
</FooterLinks>
),
},
};

export const RouterLinks: Story = {
args: {
logo: "theme",
Expand Down Expand Up @@ -95,27 +147,36 @@ export const LinksOnly: Story = {
},
};

export const LinksOnlyCentred: Story = {
export const LinksOnlySlots: Story = {
args: {
children: [
<FooterLinks
key="footer-links"
style={{ float: "unset", textAlign: "center" }}
>
leftSlot: (
<FooterLinks key="footer-links-moon">
<FooterLink href="#TheMoon" key="the-moon">
The Moon
</FooterLink>
</FooterLinks>
),
centreSlot: (
<FooterLinks key="footer-links-phobos">
<FooterLink href="#Phobos" key="phobos">
Phobos
</FooterLink>
<FooterLink href="#Ganymede" key="ganymede">
Ganymede
</FooterLink>
</FooterLinks>
),
rightSlot: (
<FooterLinks key="footer-links-titan">
<FooterLink href="#Titan" key="titan">
Titan
</FooterLink>
</FooterLinks>,
],
</FooterLinks>
),
},
};
LinksOnlySlots.storyName = "Links Only, Slots";

export const LinksOnlyCentred: Story = {
args: {
centreSlot: staticFooterLinks,
},
};
LinksOnlyCentred.storyName = "Links Only, Centred";
61 changes: 61 additions & 0 deletions src/components/navigation/Footer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,64 @@ test("Should use 'to' when both 'href' and 'to' are provided with linkComponent"
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute("href", "/about");
});

test("renders leftSlot", () => {
renderWithProviders(
<Footer leftSlot={<div data-testid="left-slot">Left Slot</div>} />,
);
expect(screen.getByTestId("left-slot")).toBeInTheDocument();
});

test("renders centreSlot", () => {
renderWithProviders(
<Footer centreSlot={<div data-testid="centre-slot">Centre Slot</div>} />,
);
expect(screen.getByTestId("centre-slot")).toBeInTheDocument();
});

test("renders rightSlot", () => {
renderWithProviders(
<Footer rightSlot={<div data-testid="right-slot">Right Slot</div>} />,
);
expect(screen.getByTestId("right-slot")).toBeInTheDocument();
});

test("renders all slots together", () => {
renderWithProviders(
<Footer
leftSlot={<div data-testid="left-slot">Right</div>}
centreSlot={<div data-testid="centre-slot">Centre</div>}
rightSlot={<div data-testid="right-slot">Right Slot</div>}
/>,
);
expect(screen.getByTestId("left-slot")).toBeInTheDocument();
expect(screen.getByTestId("centre-slot")).toBeInTheDocument();
expect(screen.getByTestId("right-slot")).toBeInTheDocument();
});

describe("Footer Slot Positioning", () => {
test("centreSlot should be centred", () => {
renderWithProviders(
<Footer centreSlot={<div data-testid="centre-slot">Centre</div>} />,
);
const centreSlot = screen.getByTestId("centre-slot");
const parent = centreSlot.parentElement;
expect(parent).toHaveStyle({
position: "absolute",
left: "50%",
transform: "translateX(-50%)",
});
});

test("leftSlot should be aligned to the left end of the row", () => {
renderWithProviders(
<Footer leftSlot={<div data-testid="left-slot">Left</div>} />,
);
const leftSlot = screen.getByTestId("left-slot");
const parent = leftSlot.parentElement;
const grandparent = parent?.parentElement;
expect(grandparent).toHaveStyle("justify-content: space-between");
expect(parent?.firstChild).toBe(leftSlot);
expect(grandparent?.firstChild).toBe(parent);
});
});
56 changes: 46 additions & 10 deletions src/components/navigation/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Grid2 as Grid,
Link,
LinkProps,
Stack,
styled,
Typography,
useTheme,
Expand All @@ -22,6 +23,9 @@ interface FooterLinksProps extends React.HTMLProps<HTMLDivElement> {
interface FooterProps extends BoxProps, React.PropsWithChildren {
logo?: ImageColorSchemeSwitchType | "theme" | null;
copyright?: string | null;
centreSlot?: React.ReactElement<LinkProps>;
rightSlot?: React.ReactElement<LinkProps>;
leftSlot?: React.ReactElement<LinkProps>;
}

const FooterLinks = ({ children, ...props }: FooterLinksProps) => {
Expand Down Expand Up @@ -90,17 +94,27 @@ const FooterLink = ({
};

const BoxStyled = styled(Box)<BoxProps>(({ theme }) => ({
position: "relative",
bottom: 0,
marginTop: "auto",
minHeight: "50px",
backgroundColor: theme.vars.palette.primary.light,
alignItems: "center",
}));

/*
* Basic footer bar.
* Can be used with `FooterLinks` and `FooterLink` to display a list of links.
*/
const Footer = ({ logo, copyright, children, ...props }: FooterProps) => {
const Footer = ({
logo,
copyright,
children,
leftSlot,
rightSlot,
centreSlot,
...props
}: FooterProps) => {
const theme = useTheme();
let resolvedLogo: ImageColorSchemeSwitchType | null | undefined = null;

Expand All @@ -112,27 +126,49 @@ const Footer = ({ logo, copyright, children, ...props }: FooterProps) => {

return (
<BoxStyled role="contentinfo" {...props}>
<Grid container>
<Grid
container
sx={{ position: "relative", height: "100%", minHeight: 50 }}
>
<Grid
size={
resolvedLogo || copyright ? { xs: 6, md: 8 } : { xs: 12, md: 12 }
resolvedLogo || copyright ? { xs: 6, md: 9 } : { xs: 12, md: 12 }
}
style={{
alignContent: "center",
}}
>
<div
style={{
paddingTop: "10px",
paddingLeft: "15px",
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
height="100%"
width="100%"
sx={{
pr: resolvedLogo || copyright ? 0 : 2,
}}
>
{children}
</div>
<Stack direction="row" alignItems="center" spacing={1}>
{leftSlot}
{children}
</Stack>
<Stack direction="row" alignItems="center" spacing={1}>
{rightSlot}
</Stack>
<Box
sx={{
position: "absolute",
left: "50%",
transform: "translateX(-50%)",
}}
>
{centreSlot}
</Box>
</Stack>
</Grid>

{(resolvedLogo || copyright) && (
<Grid size={{ xs: 6, md: 4 }}>
<Grid size={{ xs: 6, md: 3 }}>
<div
style={{
float: "right",
Expand Down