From 27683ba7531e1c8d9e742e0f8f6bd909d670dc2a Mon Sep 17 00:00:00 2001 From: Mayank Date: Thu, 13 Jun 2024 08:09:45 +0530 Subject: [PATCH 1/4] fix: FOUC --- lib/package.json | 2 +- lib/src/client/core/core.tsx | 10 ++-------- lib/src/constants.ts | 2 ++ lib/src/utils.ts | 12 ++++++++++-- lib/vitest.setup.ts | 5 +++-- packages/shared/package.json | 2 +- pnpm-lock.yaml | 24 ++++++++++++------------ 7 files changed, 31 insertions(+), 26 deletions(-) diff --git a/lib/package.json b/lib/package.json index 9e17a3eb..0379704b 100644 --- a/lib/package.json +++ b/lib/package.json @@ -67,7 +67,7 @@ "vitest": "^1.6.0" }, "dependencies": { - "r18gs": "^1.0.2" + "r18gs": "^1.1.0" }, "peerDependencies": { "@types/react": "16.8 - 19", diff --git a/lib/src/client/core/core.tsx b/lib/src/client/core/core.tsx index e5320b40..9c7f9136 100644 --- a/lib/src/client/core/core.tsx +++ b/lib/src/client/core/core.tsx @@ -1,4 +1,4 @@ -import { COOKIE_KEY, DARK, LIGHT, SYSTEM, modes } from "../../constants"; +import { COOKIE_KEY, DARK, LIGHT, MEDIA, SYSTEM, modes } from "../../constants"; import { ColorSchemePreference, Store, useStore } from "../../utils"; import { useEffect } from "react"; @@ -41,17 +41,11 @@ export const Core = ({ t, nonce }: CoreProps) => { const resolvedMode = mode === SYSTEM ? systemMode : mode; // resolvedMode is the actual mode that will be used useEffect(() => { - const media = matchMedia(`(prefers-color-scheme: ${DARK})`); + const media = matchMedia(MEDIA); /** Updating media: prefers-color-scheme*/ const updateSystemColorScheme = () => setThemeState(state => ({ ...state, s: media.matches ? DARK : LIGHT }) as Store); - updateSystemColorScheme(); media.addEventListener("change", updateSystemColorScheme); - - setThemeState(state => ({ - ...state, - m: (localStorage.getItem(COOKIE_KEY) ?? SYSTEM) as ColorSchemePreference, - })); /** Sync the tabs */ const storageListener = (e: StorageEvent): void => { if (e.key === COOKIE_KEY) diff --git a/lib/src/constants.ts b/lib/src/constants.ts index cd24dff9..75ee6b65 100644 --- a/lib/src/constants.ts +++ b/lib/src/constants.ts @@ -6,3 +6,5 @@ export const LIGHT: ColorSchemePreference = "light"; export const COOKIE_KEY = "gx"; export const modes: ColorSchemePreference[] = [SYSTEM, DARK, LIGHT]; + +export const MEDIA = `(prefers-color-scheme: ${DARK})`; diff --git a/lib/src/utils.ts b/lib/src/utils.ts index b9a12d53..06aad87f 100644 --- a/lib/src/utils.ts +++ b/lib/src/utils.ts @@ -1,5 +1,5 @@ import useRGS from "r18gs"; -import { DARK, SYSTEM } from "./constants"; +import { COOKIE_KEY, DARK, LIGHT, MEDIA, SYSTEM } from "./constants"; export type ColorSchemePreference = "system" | "dark" | "light"; export type ResolvedScheme = "dark" | "light"; @@ -14,4 +14,12 @@ const DEFAULT_STORE_VAL: Store = { }; /** local abstaction of RGS to avoid multiple imports */ -export const useStore = () => useRGS("ndm", DEFAULT_STORE_VAL); +export const useStore = () => + useRGS("ndm", () => + typeof localStorage === "undefined" + ? DEFAULT_STORE_VAL + : { + m: (localStorage.getItem(COOKIE_KEY) ?? SYSTEM) as ColorSchemePreference, + s: (matchMedia(MEDIA).matches ? DARK : LIGHT) as ResolvedScheme, + }, + ); diff --git a/lib/vitest.setup.ts b/lib/vitest.setup.ts index e6e76229..355e7d54 100644 --- a/lib/vitest.setup.ts +++ b/lib/vitest.setup.ts @@ -1,13 +1,14 @@ import { vi } from "vitest"; +const mediaListeners: (() => void)[] = []; // mock matchMedia Object.defineProperty(window, "matchMedia", { writable: true, value: vi.fn().mockImplementation((query: string) => ({ matches: query.includes(window.media), media: query, - onchange: null, - addEventListener: vi.fn(), + onchange: () => mediaListeners.forEach(listener => listener()), + addEventListener: (listener: () => void) => mediaListeners.push(listener), removeEventListener: vi.fn(), dispatchEvent: vi.fn(), })), diff --git a/packages/shared/package.json b/packages/shared/package.json index 93e347bb..bc022e6c 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -42,7 +42,7 @@ "@repo/scripts": "workspace:*", "nextjs-darkmode": "workspace:*", "nextjs-themes": "^3.1.1", - "r18gs": "^1.0.2", + "r18gs": "^1.1.0", "react-live": "^4.1.6", "react18-loaders": "^1.1.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 08012c12..43dc8487 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -208,8 +208,8 @@ importers: specifier: 10 - 14 version: 14.2.4(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.5) r18gs: - specifier: ^1.0.2 - version: 1.0.2(@types/react@18.3.3)(react@18.3.1) + specifier: ^1.1.0 + version: 1.1.0(@types/react@18.3.3)(react@18.3.1) devDependencies: '@repo/eslint-config': specifier: workspace:* @@ -341,8 +341,8 @@ importers: specifier: ^3.1.1 version: 3.1.1(@types/react@18.3.3)(next@14.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.5))(react@18.3.1) r18gs: - specifier: ^1.0.2 - version: 1.0.2(@types/react@18.3.3)(react@18.3.1) + specifier: ^1.1.0 + version: 1.1.0(@types/react@18.3.3)(react@18.3.1) react-live: specifier: ^4.1.6 version: 4.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -2348,8 +2348,8 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - electron-to-chromium@1.4.800: - resolution: {integrity: sha512-G8yyAReBP8m0XaW9BBH5NOJe4ZGYDDsPYkgLCG8xU6HwGKzrT0Jj51uAHkt1D+9ZxHPoGFSSZqqSN7HxAiP+0g==} + electron-to-chromium@1.4.801: + resolution: {integrity: sha512-PnlUz15ii38MZMD2/CEsAzyee8tv9vFntX5nhtd2/4tv4HqY7C5q2faUAjmkXS/UFpVooJ/5H6kayRKYWoGMXQ==} emittery@0.13.1: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} @@ -4458,8 +4458,8 @@ packages: '@types/react': 16.8 - 18 react: 16.8 - 18 - r18gs@1.0.2: - resolution: {integrity: sha512-DLTeoRpX70oo6/KkpB6vkY/oGxlI/SJJfOSL377+3R4f8Fi5MVQhJn92dzRH/lxZfQ6IjnqluJsQJ88AyQdu1w==} + r18gs@1.1.0: + resolution: {integrity: sha512-X6WzSCJtE8fn8/FA8Sjx8JKWC89tcjCP9mp365DTtfuqAzn+ab/5Ug+8uG4N8TIbcWaxH2veglvMDZE73CcxsQ==} peerDependencies: '@types/react': 16.8 - 18 react: 16.8 - 18 @@ -7429,7 +7429,7 @@ snapshots: browserslist@4.23.1: dependencies: caniuse-lite: 1.0.30001632 - electron-to-chromium: 1.4.800 + electron-to-chromium: 1.4.801 node-releases: 2.0.14 update-browserslist-db: 1.0.16(browserslist@4.23.1) @@ -7870,7 +7870,7 @@ snapshots: eastasianwidth@0.2.0: {} - electron-to-chromium@1.4.800: {} + electron-to-chromium@1.4.801: {} emittery@0.13.1: {} @@ -10635,7 +10635,7 @@ snapshots: '@types/react': 18.3.3 react: 18.3.1 - r18gs@1.0.2(@types/react@18.3.3)(react@18.3.1): + r18gs@1.1.0(@types/react@18.3.3)(react@18.3.1): dependencies: '@types/react': 18.3.3 react: 18.3.1 @@ -10679,7 +10679,7 @@ snapshots: react18-loaders@1.1.1(@types/react@18.3.3)(next@14.2.4(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.5))(react@18.3.1): dependencies: '@types/react': 18.3.3 - r18gs: 1.0.2(@types/react@18.3.3)(react@18.3.1) + r18gs: 1.1.0(@types/react@18.3.3)(react@18.3.1) react: 18.3.1 optionalDependencies: next: 14.2.4(@babel/core@7.24.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.77.5) From a97d162fa63eb2d76af39d8a28412712814d6de3 Mon Sep 17 00:00:00 2001 From: Mayank Date: Thu, 13 Jun 2024 09:08:04 +0530 Subject: [PATCH 2/4] Update tests --- lib/src/client/core/core.test.tsx | 13 ++++++++++--- lib/src/utils.ts | 3 ++- lib/vitest.setup.ts | 10 +++++++--- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/lib/src/client/core/core.test.tsx b/lib/src/client/core/core.test.tsx index dba79420..e05bb32a 100644 --- a/lib/src/client/core/core.test.tsx +++ b/lib/src/client/core/core.test.tsx @@ -1,9 +1,8 @@ import { act, cleanup, fireEvent, render, renderHook } from "@testing-library/react"; import { afterEach, beforeEach, describe, test } from "vitest"; -import { ServerTarget } from "../../server"; import { Core } from "./core"; import { useMode } from "../../hooks"; -import { COOKIE_KEY, DARK, LIGHT, SYSTEM } from "../../constants"; +import { COOKIE_KEY, DARK, LIGHT, MEDIA } from "../../constants"; describe("theme-switcher", () => { afterEach(cleanup); @@ -35,5 +34,13 @@ describe("theme-switcher", () => { expect(hook.result.current.mode).toBe(DARK); }); - test.todo("test media change event -- not supported by fireEvent"); + test("test media change event", async ({ expect }) => { + const hook = renderHook(() => useMode()); + await act(() => { + // globalThis.window.media = LIGHT as ResolvedScheme; + // @ts-expect-error -- ok + matchMedia(MEDIA).onchange?.(); + }); + expect(hook.result.current.mode).toBe(DARK); + }); }); diff --git a/lib/src/utils.ts b/lib/src/utils.ts index 06aad87f..87439b81 100644 --- a/lib/src/utils.ts +++ b/lib/src/utils.ts @@ -17,7 +17,8 @@ const DEFAULT_STORE_VAL: Store = { export const useStore = () => useRGS("ndm", () => typeof localStorage === "undefined" - ? DEFAULT_STORE_VAL + ? /* v8 ignore next */ + DEFAULT_STORE_VAL : { m: (localStorage.getItem(COOKIE_KEY) ?? SYSTEM) as ColorSchemePreference, s: (matchMedia(MEDIA).matches ? DARK : LIGHT) as ResolvedScheme, diff --git a/lib/vitest.setup.ts b/lib/vitest.setup.ts index 355e7d54..cde4b96f 100644 --- a/lib/vitest.setup.ts +++ b/lib/vitest.setup.ts @@ -7,9 +7,13 @@ Object.defineProperty(window, "matchMedia", { value: vi.fn().mockImplementation((query: string) => ({ matches: query.includes(window.media), media: query, - onchange: () => mediaListeners.forEach(listener => listener()), - addEventListener: (listener: () => void) => mediaListeners.push(listener), - removeEventListener: vi.fn(), + onchange() { + this.matches = query.includes(window.media); + mediaListeners.forEach(listener => listener()); + }, + addEventListener: (_: string, listener: () => void) => mediaListeners.push(listener), + removeEventListener: (_: string, listener: () => void) => + mediaListeners.splice(mediaListeners.indexOf(listener), 1), dispatchEvent: vi.fn(), })), }); From 70a22361482593147732665c2edf6833eabe140d Mon Sep 17 00:00:00 2001 From: Mayank Date: Thu, 13 Jun 2024 09:13:50 +0530 Subject: [PATCH 3/4] Fix shared ui tests --- .changeset/new-bugs-divide.md | 5 +++++ packages/shared/vitest.config.ts | 2 +- packages/shared/vitest.setup.ts | 37 ++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 .changeset/new-bugs-divide.md create mode 100644 packages/shared/vitest.setup.ts diff --git a/.changeset/new-bugs-divide.md b/.changeset/new-bugs-divide.md new file mode 100644 index 00000000..c788c2a3 --- /dev/null +++ b/.changeset/new-bugs-divide.md @@ -0,0 +1,5 @@ +--- +"nextjs-darkmode": patch +--- + +Fix: FOUC diff --git a/packages/shared/vitest.config.ts b/packages/shared/vitest.config.ts index 18196779..0476af26 100644 --- a/packages/shared/vitest.config.ts +++ b/packages/shared/vitest.config.ts @@ -8,7 +8,7 @@ export default defineConfig({ test: { environment: "jsdom", globals: true, - setupFiles: [], + setupFiles: ["vitest.setup.ts"], coverage: { include: ["src/**"], exclude: ["src/**/index.ts", "src/**/declaration.d.ts"], diff --git a/packages/shared/vitest.setup.ts b/packages/shared/vitest.setup.ts new file mode 100644 index 00000000..cde4b96f --- /dev/null +++ b/packages/shared/vitest.setup.ts @@ -0,0 +1,37 @@ +import { vi } from "vitest"; + +const mediaListeners: (() => void)[] = []; +// mock matchMedia +Object.defineProperty(window, "matchMedia", { + writable: true, + value: vi.fn().mockImplementation((query: string) => ({ + matches: query.includes(window.media), + media: query, + onchange() { + this.matches = query.includes(window.media); + mediaListeners.forEach(listener => listener()); + }, + addEventListener: (_: string, listener: () => void) => mediaListeners.push(listener), + removeEventListener: (_: string, listener: () => void) => + mediaListeners.splice(mediaListeners.indexOf(listener), 1), + dispatchEvent: vi.fn(), + })), +}); + +declare global { + interface Window { + media: "dark" | "light"; + } + // skipcq: JS-0102 + var cookies: Record; // eslint-disable-line no-var -- let is not supported in defining global due to block scope +} +Object.defineProperty(window, "media", { + writable: true, + value: "dark", +}); + +globalThis.cookies = {}; + +vi.mock("next/headers", () => ({ + cookies: () => ({ get: (cookieName: string) => globalThis.cookies[cookieName] }), +})); From 807465c5f22242d9c8204b39ab0f75bcce4abe98 Mon Sep 17 00:00:00 2001 From: Mayank Date: Thu, 13 Jun 2024 09:14:48 +0530 Subject: [PATCH 4/4] Apply changesets --- .changeset/new-bugs-divide.md | 5 ----- lib/CHANGELOG.md | 6 ++++++ lib/package.json | 2 +- packages/shared/CHANGELOG.md | 7 +++++++ packages/shared/package.json | 2 +- 5 files changed, 15 insertions(+), 7 deletions(-) delete mode 100644 .changeset/new-bugs-divide.md diff --git a/.changeset/new-bugs-divide.md b/.changeset/new-bugs-divide.md deleted file mode 100644 index c788c2a3..00000000 --- a/.changeset/new-bugs-divide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"nextjs-darkmode": patch ---- - -Fix: FOUC diff --git a/lib/CHANGELOG.md b/lib/CHANGELOG.md index 32ada8bc..eab1bad3 100644 --- a/lib/CHANGELOG.md +++ b/lib/CHANGELOG.md @@ -1,5 +1,11 @@ # nextjs-darkmode +## 0.1.1 + +### Patch Changes + +- 70a2236: Fix: FOUC + ## 0.1.0 ### Minor Changes diff --git a/lib/package.json b/lib/package.json index 0379704b..c662651d 100644 --- a/lib/package.json +++ b/lib/package.json @@ -2,7 +2,7 @@ "name": "nextjs-darkmode", "author": "Mayank Kumar Chaudhari ", "private": false, - "version": "0.1.0", + "version": "0.1.1", "description": "Unleash the Power of React Server Components! Use dark/light mode on your site with confidence, without losing any advantages of React Server Components", "license": "MPL-2.0", "main": "./dist/index.js", diff --git a/packages/shared/CHANGELOG.md b/packages/shared/CHANGELOG.md index 2c917232..ddd96c5c 100644 --- a/packages/shared/CHANGELOG.md +++ b/packages/shared/CHANGELOG.md @@ -1,5 +1,12 @@ # @repo/shared +## 0.0.5 + +### Patch Changes + +- Updated dependencies [70a2236] + - nextjs-darkmode@0.1.1 + ## 0.0.4 ### Patch Changes diff --git a/packages/shared/package.json b/packages/shared/package.json index bc022e6c..f1306b44 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@repo/shared", - "version": "0.0.4", + "version": "0.0.5", "private": true, "sideEffects": false, "main": "./dist/index.js",