From 28f0f521744a1577a51ce53c43ddb3d6fd365c58 Mon Sep 17 00:00:00 2001 From: Mayank Kumar Chaudhari Date: Thu, 7 Dec 2023 15:03:08 +0530 Subject: [PATCH] update tests --- README.md | 109 +++++++++++++++++- .../client/color-switch/color-switch.test.tsx | 19 +++ .../src/client/color-switch/color-switch.tsx | 1 + .../theme-switcher/theme-switcher.test.tsx | 35 +++--- .../server-side-wrapper.tsx | 2 +- packages/nextjs-themes/src/utils.ts | 8 ++ packages/nextjs-themes/vitest.config.ts | 4 +- 7 files changed, 150 insertions(+), 28 deletions(-) create mode 100644 packages/nextjs-themes/src/client/color-switch/color-switch.test.tsx diff --git a/README.md b/README.md index 95d4b803..68a21723 100644 --- a/README.md +++ b/README.md @@ -6,23 +6,24 @@ 🤟 👉 [Unleash the Power of React Server Components](https://medium.com/javascript-in-plain-english/unleash-the-power-of-react-server-components-eb3fe7201231) -This project is inspired by next-themes. Next-themes is an awesome package, however, it requires wrapping everything in a provider. The provider has to be a client component as it uses hooks. And thus, it takes away all the benefits of Server Components. +This project was originally inspired by next-themes. Next-themes is an awesome package, however, it requires wrapping everything in a provider. The provider has to be a client component as it uses hooks. And thus, it takes away all the benefits of Server Components. -`nextjs-themes` removes this limitation and enables you to unleash the full power of React 18 Server Components. In addition, more features are coming up soon... Stay tuned! +`nextjs-themes` removes this limitation and enables you to unleash the full power of React 18 Server Components. In addition, it adds more features and control over how you theme your app. Stay tuned! +- ✅ Perfect dark mode in 2 lines of code - ✅ Fully Treeshakable (`import from nextjs-themes/client/component`) - ✅ Designed for excellence - ✅ Full TypeScript Support - ✅ Unleash the full power of React18 Server components -- ✅ Works with all build systems/tools/frameworks for React18 - ✅ Perfect dark mode in 2 lines of code - ✅ System setting with prefers-color-scheme - ✅ Themed browser UI with color-scheme - ✅ Support for Next.js 13 & Next.js 14 `appDir` +- ✅ No flash on load (for all - SSG, SSR, ISG, Server Components) - ✅ Sync theme across tabs and windows - ✅ Disable flashing when changing themes - ✅ Force pages to specific themes -- ✅ Class or data attribute selector +- ✅ Class and data attribute selector - ✅ Manipulate theme via `useTheme` hook - ✅ Doccumented with [Typedoc](https://react18-tools.github.io/nextjs-themes) ([Docs](https://react18-tools.github.io/nextjs-themes)) @@ -161,6 +162,36 @@ That's it, your Next.js app fully supports dark mode, including System preferenc } ``` +## Images + +You can also show different images based on the current theme. + +```jsx +import Image from "next/image"; +import { getResolvedTheme } from "nextjs-themes/utils"; + +function ThemedImage() { + const resolvedTheme = getResolvedTheme(); + let src; + + switch (resolvedTheme) { + case "light": + src = "/light.png"; + break; + case "dark": + src = "/dark.png"; + break; + default: + src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"; + break; + } + + return ; +} + +export default ThemedImage; +``` + ### useTheme In case your components need to know the current theme and be able to change it. The `useTheme` hook provides theme information: @@ -221,6 +252,60 @@ In a similar way, you can also force color scheme. Forcing color scheme will apply your defaultDark or defaultLight theme, configurable via hooks. +### With Styled Components and any CSS-in-JS + +Next Themes is completely CSS independent, it will work with any library. For example, with Styled Components you just need to `createGlobalStyle` in your custom App: + +```js +// pages/_app.js +import { createGlobalStyle } from "styled-components"; +import { ThemeSwitcher } from "nextjs-themes"; + +// Your themeing variables +const GlobalStyle = createGlobalStyle` + :root { + --fg: #000; + --bg: #fff; + } + + [data-theme="dark"] { + --fg: #fff; + --bg: #000; + } +`; + +function MyApp({ Component, pageProps }) { + return ( + <> + + + + + ); +} +``` + +### With Tailwind + +In your `tailwind.config.js`, set the dark mode property to class: + +```js +// tailwind.config.js +module.exports = { + darkMode: "class", +}; +``` + +⚡🎉Boom! You are ready to use darkTheme in tailwind. + +> Caution! Your class must be set to `"dark"`, which is the default value we have used for this library. Tailwind, as of now, requires that class name must be `"dark"` for dark-theme. + +That's it! Now you can use dark-mode specific classes: + +```tsx +

+``` + ## Migrating from v1 to v2 #### Motivation: @@ -249,6 +334,22 @@ Take care of the following while migrating to `v2`. Want handson course for getting started with Turborepo? Check out [React and Next.js with TypeScript](https://www.udemy.com/course/react-and-next-js-with-typescript/?referralCode=7202184A1E57C3DCA8B2) +## FAQ + +**Do I need to use CSS variables with this library?** + +Nope. It's just a convenient way. You can hard code values for every class as follows. + +```css +.my-class { + color: #555; +} + +[data-theme="dark"] .my-class { + color: white; +} +``` + ## License Licensed as MIT open source. diff --git a/packages/nextjs-themes/src/client/color-switch/color-switch.test.tsx b/packages/nextjs-themes/src/client/color-switch/color-switch.test.tsx new file mode 100644 index 00000000..357d6e1b --- /dev/null +++ b/packages/nextjs-themes/src/client/color-switch/color-switch.test.tsx @@ -0,0 +1,19 @@ +import { act, cleanup, fireEvent, render, renderHook, screen } from "@testing-library/react"; +import { ColorSwitch } from "./color-switch"; +import { useTheme } from "../../store"; + +describe("color-switch", () => { + afterEach(cleanup); + + test("color-scheme-toggle", ({ expect }) => { + const hook = renderHook(() => useTheme()); + render(); + const element = screen.getByTestId("color-switch"); + act(() => fireEvent.click(element)); + expect(hook.result.current.colorSchemePref).toBe("dark"); + act(() => fireEvent.click(element)); + expect(hook.result.current.colorSchemePref).toBe("light"); + act(() => fireEvent.click(element)); + expect(hook.result.current.colorSchemePref).toBe("system"); + }); +}); diff --git a/packages/nextjs-themes/src/client/color-switch/color-switch.tsx b/packages/nextjs-themes/src/client/color-switch/color-switch.tsx index 63580dee..9a240af2 100644 --- a/packages/nextjs-themes/src/client/color-switch/color-switch.tsx +++ b/packages/nextjs-themes/src/client/color-switch/color-switch.tsx @@ -37,6 +37,7 @@ export function ColorSwitch({ size = 25 }: ColorSwitchProps) { return (
e.key === "Enter" && toggleColorScheme()} role="button" diff --git a/packages/nextjs-themes/src/client/theme-switcher/theme-switcher.test.tsx b/packages/nextjs-themes/src/client/theme-switcher/theme-switcher.test.tsx index 43effd09..7f11bd6e 100644 --- a/packages/nextjs-themes/src/client/theme-switcher/theme-switcher.test.tsx +++ b/packages/nextjs-themes/src/client/theme-switcher/theme-switcher.test.tsx @@ -2,32 +2,25 @@ import { act, cleanup, render, renderHook } from "@testing-library/react"; import { afterEach, describe, test } from "vitest"; import { useTheme } from "../../store"; import { ThemeSwitcher } from "./theme-switcher"; +import { getResolvedColorScheme, getResolvedTheme } from "../../utils"; -/** Test if proper data-theme is added to documentElement - * concurrency is not feasible because of global store conflicts +/** + * -> concurrency is not feasible because of global store conflicts */ describe("theme-switcher", () => { afterEach(cleanup); - test("Test first time load based on media query", async ({ expect }) => { - window.media = "dark"; - await act(() => render()); - expect(document.documentElement.getAttribute("data-theme")).toBe("dark"); - window.media = "light"; - await act(() => render()); - expect(document.documentElement.getAttribute("data-theme")).toBe(""); - }); - test("Test defaultDark and defaultLight themes", async ({ expect }) => { const { result } = renderHook(() => useTheme()); act(() => result.current.setDarkTheme("dark1")); act(() => result.current.setLightTheme("light1")); window.media = "dark"; await act(() => render()); - expect(document.documentElement.getAttribute("data-theme")).toBe("dark1"); + expect(getResolvedTheme()).toBe("dark1"); window.media = "light"; await act(() => render()); - expect(document.documentElement.getAttribute("data-theme")).toBe("light1"); + expect(getResolvedTheme()).toBe("light1"); + expect(getResolvedColorScheme()).toBe("system"); }); // colorScheme has higher preference @@ -36,7 +29,7 @@ describe("theme-switcher", () => { act(() => result.current.setColorSchemePref("")); act(() => result.current.setTheme("blue")); await act(() => render()); - expect(document.documentElement.getAttribute("data-theme")).toBe("blue"); + expect(getResolvedTheme()).toBe("blue"); }); test("test color scheme preference", async ({ expect }) => { @@ -45,11 +38,11 @@ describe("theme-switcher", () => { act(() => result.current.setLightTheme("yellow")); act(() => result.current.setTheme("blue")); await act(() => render()); - expect(document.documentElement.getAttribute("data-theme")).toBe("yellow"); + expect(getResolvedTheme()).toBe("yellow"); act(() => result.current.setDarkTheme("dark-blue")); act(() => result.current.setColorSchemePref("dark")); await act(() => render()); - expect(document.documentElement.getAttribute("data-theme")).toBe("dark-blue"); + expect(getResolvedTheme()).toBe("dark-blue"); }); test("test forcedTheme", async ({ expect }) => { @@ -59,7 +52,7 @@ describe("theme-switcher", () => { act(() => result.current.setColorSchemePref("light")); act(() => result.current.setTheme("f1")); await act(() => render()); - expect(document.documentElement.getAttribute("data-theme")).toBe("forced1"); + expect(getResolvedTheme()).toBe("forced1"); }); test("forced colorScheme only", async ({ expect }) => { @@ -71,19 +64,19 @@ describe("theme-switcher", () => { act(() => result.current.setLightTheme("yellow")); act(() => result.current.setDarkTheme("black")); await act(() => render()); - expect(document.documentElement.getAttribute("data-theme")).toBe(""); + expect(getResolvedTheme()).toBe(""); act(() => result.current.setForcedTheme(undefined)); - expect(document.documentElement.getAttribute("data-theme")).toBe("black"); + expect(getResolvedTheme()).toBe("black"); }); test("forced theme prop", async ({ expect }) => { await act(() => render()); - expect(document.documentElement.getAttribute("data-theme")).toBe("theme1"); + expect(getResolvedTheme()).toBe("theme1"); }); test("forced colorScheme prop", async ({ expect }) => { // global state is continuing from previous testss await act(() => render()); - expect(document.documentElement.getAttribute("data-theme")).toBe(""); + expect(getResolvedTheme()).toBe(""); }); }); diff --git a/packages/nextjs-themes/src/server/nextjs/server-side-wrapper/server-side-wrapper.tsx b/packages/nextjs-themes/src/server/nextjs/server-side-wrapper/server-side-wrapper.tsx index 55a8c0a1..7f7ce3a4 100644 --- a/packages/nextjs-themes/src/server/nextjs/server-side-wrapper/server-side-wrapper.tsx +++ b/packages/nextjs-themes/src/server/nextjs/server-side-wrapper/server-side-wrapper.tsx @@ -34,7 +34,7 @@ function sharedServerComponentRenderer( className: "", }; if (dataTheme !== undefined) { - dataProps["data-theme"] += dataTheme; + dataProps["data-theme"] = dataTheme; dataProps.className = dataTheme; } if (dataColorScheme !== undefined) { diff --git a/packages/nextjs-themes/src/utils.ts b/packages/nextjs-themes/src/utils.ts index ece14e9f..3a265240 100644 --- a/packages/nextjs-themes/src/utils.ts +++ b/packages/nextjs-themes/src/utils.ts @@ -14,3 +14,11 @@ export function resolveThemeFromColorScheme(state: ThemeStoreType, isSystemDark: return theme; } } + +export function getResolvedTheme() { + return document.documentElement.getAttribute("data-theme"); +} + +export function getResolvedColorScheme() { + return document.documentElement.getAttribute("data-color-scheme"); +} diff --git a/packages/nextjs-themes/vitest.config.ts b/packages/nextjs-themes/vitest.config.ts index a972bbfd..2415b17a 100644 --- a/packages/nextjs-themes/vitest.config.ts +++ b/packages/nextjs-themes/vitest.config.ts @@ -12,8 +12,8 @@ export default defineConfig({ setupFiles: ["vitest.setup.ts"], coverage: { reporter: ["text", "json", "clover", "html"], - exclude: ["__mocks__"], + exclude: ["__mocks__", "**/index.ts"], + include: ["src/**/*"], }, - threads: true, }, });