From 8084242c3021f144cd08826039cd44aec23efdca Mon Sep 17 00:00:00 2001 From: qwqcode Date: Tue, 17 Sep 2024 16:15:12 +0800 Subject: [PATCH 1/4] fix(dark-mode): avoid localStorage overwrite by system dark mode status --- .../src/useDarkMode/useDarkMode.test.ts | 25 +++++++ .../src/useDarkMode/useDarkMode.ts | 4 +- packages/usehooks-ts/tests/mocks.ts | 66 +++++++++++++++---- 3 files changed, 80 insertions(+), 15 deletions(-) diff --git a/packages/usehooks-ts/src/useDarkMode/useDarkMode.test.ts b/packages/usehooks-ts/src/useDarkMode/useDarkMode.test.ts index 82bb08bc..513bbcb0 100644 --- a/packages/usehooks-ts/src/useDarkMode/useDarkMode.test.ts +++ b/packages/usehooks-ts/src/useDarkMode/useDarkMode.test.ts @@ -124,4 +124,29 @@ describe('useDarkMode()', () => { expect(result.current.isDarkMode).toBe(false) }) + + it('should update dark mode when OS preference changes', () => { + const { updateMatches } = mockMatchMedia(false) + const { result, rerender } = renderHook(() => useDarkMode()) + expect(result.current.isDarkMode).toBe(false) + updateMatches(true) + rerender() // trigger `useIsomorphicLayoutEffect` + expect(result.current.isDarkMode).toBe(true) + }) + + it('should prioritize localStorage value over OS dark mode on page load', () => { + window.localStorage.setItem('custom-key', JSON.stringify(false)) + + mockMatchMedia(true) + const { result } = renderHook(() => + useDarkMode({ localStorageKey: 'custom-key', initializeWithValue: true }), + ) + expect(result.current.isDarkMode).toBe(false) + + act(() => { + result.current.toggle() + }) + expect(result.current.isDarkMode).toBe(true) + expect(window.localStorage.getItem('custom-key')).toBe(JSON.stringify(true)) + }) }) diff --git a/packages/usehooks-ts/src/useDarkMode/useDarkMode.ts b/packages/usehooks-ts/src/useDarkMode/useDarkMode.ts index 8f908daf..46f64aab 100644 --- a/packages/usehooks-ts/src/useDarkMode/useDarkMode.ts +++ b/packages/usehooks-ts/src/useDarkMode/useDarkMode.ts @@ -1,3 +1,4 @@ +import { useIsMounted } from '../useIsMounted' import { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect' import { useLocalStorage } from '../useLocalStorage' import { useMediaQuery } from '../useMediaQuery' @@ -68,8 +69,9 @@ export function useDarkMode(options: DarkModeOptions = {}): DarkModeReturn { ) // Update darkMode if os prefers changes + const allowDarkOSChange = useIsMounted() useIsomorphicLayoutEffect(() => { - if (isDarkOS !== isDarkMode) { + if (allowDarkOSChange() && isDarkOS !== isDarkMode) { setDarkMode(isDarkOS) } }, [isDarkOS]) diff --git a/packages/usehooks-ts/tests/mocks.ts b/packages/usehooks-ts/tests/mocks.ts index ed12aaa8..18d2b117 100644 --- a/packages/usehooks-ts/tests/mocks.ts +++ b/packages/usehooks-ts/tests/mocks.ts @@ -1,26 +1,63 @@ /** - * Mocks the matchMedia API - * @param {boolean} matches - True for dark, false for light + * Mocks the matchMedia API. + * @param {boolean} matches - True for dark, false for light. + * @returns {object} An object with a function to change the matches value. * @example * mockMatchMedia(false) */ -export const mockMatchMedia = (matches: boolean): void => { +export const mockMatchMedia = (matches: boolean) => { + type EventListener = (event: Event) => void + const eventListeners: Record = {} + + const matchMedia = (query: string) => ({ + get matches() { + return matches + }, + media: query, + onchange: null, + addEventListener: vitest + .fn() + .mockImplementation((type: string, listener: EventListener) => { + if (!eventListeners[type]) eventListeners[type] = [] + eventListeners[type].push(listener) + }), + removeEventListener: vitest + .fn() + .mockImplementation((type: string, listener: EventListener) => { + eventListeners[type] = eventListeners[type]?.filter(l => l !== listener) + }), + dispatchEvent: vitest.fn().mockImplementation((event: Event) => { + eventListeners[event.type]?.forEach(listener => { + listener(event) + }) + return true + }), + }) + Object.defineProperty(window, 'matchMedia', { writable: true, - value: vitest.fn().mockImplementation(query => ({ - matches, - media: query, - onchange: null, - addEventListener: vitest.fn(), - removeEventListener: vitest.fn(), - dispatchEvent: vitest.fn(), - })), + value: vitest.fn().mockImplementation(matchMedia), }) + + return { + /** + * Updates the matches value. This will trigger the change event. + * @param m - The new value for matches. + * @example + * mockMatchMedia(false).updateMatches(true) + */ + updateMatches: (m: boolean) => { + matches = m + eventListeners.change?.forEach(listener => { + listener(new Event('change')) + }) + }, + } } /** - * Mocks the Storage API - * @param {'localStorage' | 'sessionStorage'} name - The name of the storage to mock + * Mocks the Storage API. + * @param {'localStorage' | 'sessionStorage'} name - The name of the storage to mock. * @example * mockStorage('localStorage') * // Then use window.localStorage as usual (it will be mocked) @@ -38,10 +75,11 @@ export const mockStorage = (name: 'localStorage' | 'sessionStorage'): void => { } setItem(key: string, value: unknown) { - this.store[key] = value + '' + this.store[key] = String(value) } removeItem(key: string) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete this.store[key] } } From bd7d0f018848063f984dbc4d85bb1ce4e875a9e6 Mon Sep 17 00:00:00 2001 From: qwqcode Date: Fri, 11 Oct 2024 11:13:17 +0800 Subject: [PATCH 2/4] chore: add changeset --- .changeset/old-cows-mix.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/old-cows-mix.md diff --git a/.changeset/old-cows-mix.md b/.changeset/old-cows-mix.md new file mode 100644 index 00000000..b11a986d --- /dev/null +++ b/.changeset/old-cows-mix.md @@ -0,0 +1,5 @@ +--- +"usehooks-ts": patch +--- + +fix(dark-mode): avoid localStorage overwrite by system dark mode status From 2d6124f9cdaf863debd5b21c29ef81435b482b04 Mon Sep 17 00:00:00 2001 From: qwqcode Date: Fri, 11 Oct 2024 11:17:16 +0800 Subject: [PATCH 3/4] chore: revise changeset --- .changeset/old-cows-mix.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/old-cows-mix.md b/.changeset/old-cows-mix.md index b11a986d..5ec64852 100644 --- a/.changeset/old-cows-mix.md +++ b/.changeset/old-cows-mix.md @@ -2,4 +2,4 @@ "usehooks-ts": patch --- -fix(dark-mode): avoid localStorage overwrite by system dark mode status +Fix `useDarkMode` to avoid localStorage being overwritten by system status From bd1cd0c370ec484b0611b8f5e99539de4d2fea7c Mon Sep 17 00:00:00 2001 From: qwqcode Date: Fri, 11 Oct 2024 11:24:54 +0800 Subject: [PATCH 4/4] chore: revise changeset --- .changeset/old-cows-mix.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/old-cows-mix.md b/.changeset/old-cows-mix.md index 5ec64852..81c11545 100644 --- a/.changeset/old-cows-mix.md +++ b/.changeset/old-cows-mix.md @@ -2,4 +2,4 @@ "usehooks-ts": patch --- -Fix `useDarkMode` to avoid localStorage being overwritten by system status +Fix `useDarkMode` to avoid os dark mode overwriting localStorage