Skip to content

Fix flicker in SSR when using dark mode #36

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 19 additions & 0 deletions src/__test-utils__/helpers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ThemeProvider } from "@mui/material/styles";
import { DiamondTheme } from "../themes/DiamondTheme";
import { render, RenderResult } from "@testing-library/react";
import { ThemeProviderProps } from "@mui/material/styles/ThemeProvider";

export const renderWithProviders = (
ui: React.ReactNode,
themeOptions?: Omit<ThemeProviderProps, "theme">,
): RenderResult => {
const Wrapper = ({ children }: { children: React.ReactNode }) => {
return (
<ThemeProvider {...themeOptions} theme={DiamondTheme}>
{children}
</ThemeProvider>
);
};

return render(ui, { wrapper: Wrapper });
};
20 changes: 8 additions & 12 deletions src/components/Footer.test.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,34 @@
import { render, screen, waitFor } from "@testing-library/react";
import { screen, waitFor } from "@testing-library/react";
import "@testing-library/jest-dom";

import dlsLogo from "../public/generic/logo-short.svg";
import { Footer, FooterLink, FooterLinks } from "./Footer";
import { ImageColorSchemeSwitch } from "./ImageColorSchemeSwitch";
import { ThemeProvider } from "../themes/ThemeProvider";
import { renderWithProviders } from "../__test-utils__/helpers";

jest.mock("./ImageColorSchemeSwitch");
// @ts-expect-error: doesn't find mockImplementation outside of testing.
ImageColorSchemeSwitch.mockImplementation(() => <img src="src" alt="alt" />);

describe("Footer", () => {
test("Should render logo only", () => {
render(<Footer logo={{ src: dlsLogo, alt: "t" }} />);
renderWithProviders(<Footer logo={{ src: dlsLogo, alt: "t" }} />);

expect(screen.getByRole("img")).toBeInTheDocument();
// No copyright text
expect(screen.queryByRole("paragraph")).not.toBeInTheDocument();
});

test("Should render logo via theme", () => {
render(
<ThemeProvider>
<Footer logo="theme" />
</ThemeProvider>,
);
renderWithProviders(<Footer logo="theme" />);

expect(screen.getByRole("img")).toBeInTheDocument();
});

test("Should render copyright only", async () => {
const copyrightText = "add text here";
const currentYear = new Date().getFullYear();
render(<Footer logo={null} copyright={copyrightText} />);
renderWithProviders(<Footer logo={null} copyright={copyrightText} />);

await waitFor(() => {
expect(screen.queryByRole("paragraph")).toBeInTheDocument();
Expand All @@ -47,7 +43,7 @@ describe("Footer", () => {
test("Should render logo and copyright", async () => {
const copyrightText = "add text here";
const currentYear = new Date().getFullYear();
render(
renderWithProviders(
<Footer logo={{ src: dlsLogo, alt: "" }} copyright={copyrightText} />,
);

Expand All @@ -65,7 +61,7 @@ describe("Footer", () => {
const lineOneText = "Link one";
const linkOneName = "link-one-href";

render(
renderWithProviders(
<Footer>
<FooterLinks>
<FooterLink href={linkOneName}>{lineOneText}</FooterLink>
Expand All @@ -87,7 +83,7 @@ describe("Footer", () => {
const linkTwoText = "Link two";
const linkOneName = "link-one-href";
const linkTwoName = "link-two-href";
render(
renderWithProviders(
<Footer>
<FooterLinks>
<FooterLink href={linkOneName}>{linkOneText}</FooterLink>
Expand Down
4 changes: 2 additions & 2 deletions src/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const FooterLink = ({ children, ...props }: LinkProps) => {
<Link
sx={{
"&:hover": {
color: theme.palette.secondary.main,
color: theme.vars.palette.secondary.main,
borderBottom: "solid 4px",
},
textDecoration: "none",
Expand Down Expand Up @@ -74,7 +74,7 @@ const Footer = ({ logo, copyright, children, ...props }: FooterProps) => {
bottom: 0,
marginTop: "auto",
minHeight: 50,
backgroundColor: theme.palette.primary.light,
backgroundColor: theme.vars.palette.primary.light,
}}
{...props}
>
Expand Down
61 changes: 11 additions & 50 deletions src/components/ImageColorSchemeSwitch.test.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import "@testing-library/jest-dom";
import { render } from "@testing-library/react";

import { ImageColorSchemeSwitch, getLogoSrc } from "./ImageColorSchemeSwitch";

jest.mock("@mui/material", () => {
return {
useColorScheme: jest.fn().mockReturnValue({ mode: "dark" }),
};
});
import { ImageColorSchemeSwitch } from "./ImageColorSchemeSwitch";
import { renderWithProviders } from "../__test-utils__/helpers";
import { screen } from "@testing-library/react";

describe("ImageColorSchemeSwitch", () => {
const testVals = {
Expand All @@ -22,17 +17,17 @@ describe("ImageColorSchemeSwitch", () => {
width?: string;
height?: string;
}) {
const { getByAltText } = render(
const { getByTestId } = renderWithProviders(
<ImageColorSchemeSwitch image={{ ...testVals, ...image }} />,
);

const img = getByAltText(testVals.alt);
const img = getByTestId("image-light");
expect(img).toBeInTheDocument();
return img;
}

it("should render without errors", () => {
render(<ImageColorSchemeSwitch image={{ ...testVals }} />);
renderWithProviders(<ImageColorSchemeSwitch image={{ ...testVals }} />);
});

it("should have src and alt by default", () => {
Expand Down Expand Up @@ -66,46 +61,12 @@ describe("ImageColorSchemeSwitch", () => {
it("should have alternate src", () => {
const srcDark = "src/dark";

const img = getRenderImg({
srcDark,
});

expect(img).toHaveAttribute("src", srcDark);
});
});

describe("getLogoSrc", () => {
const srcLight = "src/light",
srcDark = "src/dark";

it("should be null if no image", () => {
// @ts-expect-error: invalid input
expect(getLogoSrc(null, "")).toStrictEqual(undefined);
// @ts-expect-error: invalid input, calm down ts
expect(getLogoSrc()).toStrictEqual(undefined);
});

it("should be srcLight if no srcDark", () => {
expect(getLogoSrc({ src: srcLight, alt: "" }, "light")).toStrictEqual(
srcLight,
renderWithProviders(
<ImageColorSchemeSwitch image={{ ...testVals, srcDark }} />,
{ defaultMode: "dark" },
);
});
const img = screen.getByTestId("image-dark");

it("should be srcLight if mode is dark but no srcDark", () => {
expect(getLogoSrc({ src: srcLight, alt: "" }, "dark")).toStrictEqual(
srcLight,
);
});

it("should be srcLight if srcDark but mode light", () => {
expect(
getLogoSrc({ src: srcLight, srcDark: srcDark, alt: "" }, "light"),
).toStrictEqual(srcLight);
});

it("should be srcDark if mode dark", () => {
expect(
getLogoSrc({ src: "src/light", srcDark: srcDark, alt: "" }, "dark"),
).toStrictEqual(srcDark);
expect(img).toHaveAttribute("src", srcDark);
});
});
58 changes: 35 additions & 23 deletions src/components/ImageColorSchemeSwitch.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useColorScheme } from "@mui/material";
import { styled } from "@mui/material";

type ImageColorSchemeSwitchType = {
src: string;
Expand All @@ -12,28 +12,40 @@ interface ImageColorSchemeSwitchProps {
image: ImageColorSchemeSwitchType;
}

export function getLogoSrc(image: ImageColorSchemeSwitchType, mode: string) {
if (!image) {
return undefined;
}

if (image.srcDark === undefined) {
return image.src;
}

return mode === "dark" ? image.srcDark : image.src;
}

const ImageColorSchemeSwitch = ({ image }: ImageColorSchemeSwitchProps) => {
const { mode } = useColorScheme();
if (!mode) return <></>;

const src: string | undefined = getLogoSrc(image, mode);

return (
<img src={src} alt={image.alt} width={image.width} height={image.height} />
);
};
/** Styled component which is only displayed in dark mode */
const ImageDark = styled("img")(({ theme }) => [
{ display: "none" },
theme.applyStyles("dark", {
display: "block",
}),
]);

/** Styled component which is only displayed in light mode */
const ImageLight = styled("img")(({ theme }) => [
{ display: "block" },
theme.applyStyles("dark", {
display: "none",
}),
]);

const ImageColorSchemeSwitch = ({ image }: ImageColorSchemeSwitchProps) => (
<>
<ImageLight
data-testid="image-light"
src={image.src}
alt={image.alt}
width={image.width}
height={image.height}
/>
<ImageDark
data-testid="image-dark"
src={image.srcDark}
alt={image.alt}
width={image.width}
height={image.height}
/>
</>
);

export { ImageColorSchemeSwitch };
export type { ImageColorSchemeSwitchProps, ImageColorSchemeSwitchType };
11 changes: 6 additions & 5 deletions src/components/Navbar.test.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import { fireEvent, screen, render } from "@testing-library/react";
import { fireEvent, screen } from "@testing-library/react";
import { Navbar, NavLinks, NavLink } from "./Navbar";
import "@testing-library/jest-dom";
import { renderWithProviders } from "../__test-utils__/helpers";

describe("Navbar", () => {
it("should not display logo if null", () => {
global.innerWidth = 600;
render(<Navbar logo={null} />);
renderWithProviders(<Navbar logo={null} />);
expect(screen.queryByAltText("Home")).not.toBeInTheDocument();
});
});

describe("Navbar Links", () => {
it("should display hamburger menu on narrow displays", () => {
global.innerWidth = 600;
render(
renderWithProviders(
<NavLinks>
<NavLink>Proposals</NavLink>
</NavLinks>,
Expand All @@ -25,7 +26,7 @@ describe("Navbar Links", () => {

it("should display menu items when hamburger menu is clicked", () => {
global.innerWidth = 600;
render(
renderWithProviders(
<NavLinks>
<NavLink>Proposals</NavLink>
</NavLinks>,
Expand All @@ -38,7 +39,7 @@ describe("Navbar Links", () => {

it("should render links properly", () => {
global.innerWidth = 600;
render(
renderWithProviders(
<NavLinks>
<NavLink>Proposals</NavLink>
</NavLinks>,
Expand Down
6 changes: 3 additions & 3 deletions src/components/Navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ const NavLinks = ({ children }: NavLinksProps) => {
onClose={onClose}
anchor="left"
PaperProps={{
sx: { backgroundColor: theme.palette.primary.main },
sx: { backgroundColor: theme.vars.palette.primary.main },
}}
>
<Box
Expand All @@ -101,7 +101,7 @@ const NavLinks = ({ children }: NavLinksProps) => {
display: "flex",
flexDirection: "column",
alignItems: "center",
backgroundColor: theme.palette.primary.main,
backgroundColor: theme.vars.palette.primary.main,
}}
>
{children}
Expand All @@ -126,7 +126,7 @@ const Navbar = ({ children, logo, ...props }: NavbarProps) => {
<Paper
sx={{
display: "flex",
backgroundColor: theme.palette.primary.main,
backgroundColor: theme.vars.palette.primary.main,
px: { xs: "1rem", md: "7.5vw" },
height: 50,
width: "100%",
Expand Down
Loading
Loading