From 97ee5c79e14bb9f20ad670b6d5148bbaf3c4e039 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olle=20M=C3=A5nsson?= <31876997+ollema@users.noreply.github.com> Date: Thu, 26 Oct 2023 11:32:49 +0200 Subject: [PATCH] refactor mode.ts to use a single persistent store --- README.md | 2 +- src/lib/mode-watcher.svelte | 8 +- src/lib/mode.ts | 141 +++++++++++------------------------- src/tests/mode.spec.ts | 10 ++- 4 files changed, 55 insertions(+), 106 deletions(-) diff --git a/README.md b/README.md index 41c0684..1bb7469 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ A function that resets the mode to system preferences. ### mode -A readable store that contains the current mode. It can be `light` or `dark`. +A readable store that contains the current mode. It can be `light`, `dark` or `system`. ```svelte @@ -12,5 +12,5 @@ - {@html `<\u{73}cript nonce="%sveltekit.nonce%">(${setInitialClassState.toString()})();`} + {@html `<\u{73}cript nonce="%sveltekit.nonce%">(${setInitialMode.toString()})();`} diff --git a/src/lib/mode.ts b/src/lib/mode.ts index cc87bb5..1a34101 100644 --- a/src/lib/mode.ts +++ b/src/lib/mode.ts @@ -1,145 +1,86 @@ // Modified version of the light switch by: https://skeleton.dev -import { derived, get } from 'svelte/store'; import { persisted } from 'svelte-persisted-store'; +import { get } from 'svelte/store'; -/** - * Stores - * ---- - * TRUE: Light mode | FALSE: Dark mode - */ - -const modeOsPrefers = persisted('modeOsPrefers', false); -const modeUserPrefers = persisted('modeUserPrefers', undefined); -const modeCurrent = persisted('modeCurrent', false); - -/** Derived store with either `"light"` or `"dark"` depending on the current mode */ -export const mode = derived(modeCurrent, ($modeCurrent) => ($modeCurrent ? 'light' : 'dark')); +/** Persistent store with either `"light"`, `"dark"` or `"system"` depending on the current mode */ +export const mode = persisted<'light' | 'dark' | 'system'>('mode', 'system'); /** * Getters */ -/** Get the OS preference */ -export function getModeOsPrefers(): boolean { - const prefersLightMode = window.matchMedia('(prefers-color-scheme: light)').matches; - modeOsPrefers.set(prefersLightMode); - return prefersLightMode; +/** Get the current mode */ +function getCurrentMode(): 'light' | 'dark' | 'system' { + return get(mode); } -/** Get the User preference */ -function getModeUserPrefers(): boolean | undefined { - return get(modeUserPrefers); -} - -/** Get the automatic preference */ -export function getModeAutoPrefers(): boolean { - const os = getModeOsPrefers(); - const user = getModeUserPrefers(); - return user !== undefined ? user : os; +/** Get the operating system preference */ +export function getSystemMode(): 'light' | 'dark' { + return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; } /** * Setters */ -/** Set the user preference */ -function setModeUserPrefers(value: boolean | undefined): void { - modeUserPrefers.set(value); -} - -/** Set the current mode */ -export function setModeCurrent(value: boolean): void { +/** Set class & color scheme for the `html` element */ +function setHtmlClassColorScheme(nextMode: 'light' | 'dark' | 'system'): void { const htmlEl = document.documentElement; - const classDark = 'dark'; - if (value === true) { - htmlEl.classList.remove(classDark); + const systemMode = getSystemMode(); + + if (nextMode === 'light' || (nextMode === 'system' && systemMode === 'light')) { + htmlEl.classList.remove('dark'); htmlEl.style.colorScheme = 'light'; - } else { - htmlEl.classList.add(classDark); - htmlEl.style.colorScheme = 'dark'; } - modeCurrent.set(value); -} - -/** - * Lightswitch Utility - */ -/** Set the visible light/dark mode on page load */ -export function setInitialClassState() { - const htmlEl = document.documentElement; - - const condLocalStorageUserPrefs = localStorage.getItem('modeUserPrefers') === 'false'; - const condLocalStorageUserPrefsExist = !('modeUserPrefers' in localStorage); - const condMatchMedia = window.matchMedia('(prefers-color-scheme: dark)').matches; - - if (condLocalStorageUserPrefs || (condLocalStorageUserPrefsExist && condMatchMedia)) { + if (nextMode === 'dark' || (nextMode === 'system' && systemMode === 'dark')) { htmlEl.classList.add('dark'); htmlEl.style.colorScheme = 'dark'; - } else { - htmlEl.classList.remove('dark'); - htmlEl.style.colorScheme = 'light'; } } -/** - * Auto Mode Watcher - */ +/** Set the current mode */ +export function setCurrentMode(nextMode: 'light' | 'dark' | 'system'): void { + mode.set(nextMode); + setHtmlClassColorScheme(nextMode); +} -/** Automatically set the visible light/dark updates on change */ -export function autoModeWatcher(): void { - const mql = window.matchMedia('(prefers-color-scheme: dark)'); - function setMode(value: boolean) { - const htmlEl = document.documentElement; - const classDark = 'dark'; - if (value === true) { - htmlEl.classList.remove(classDark); - htmlEl.style.colorScheme = 'light'; - } else { - htmlEl.classList.add(classDark); - htmlEl.style.colorScheme = 'dark'; - } - } - setMode(mql.matches); - mql.onchange = () => { - setMode(mql.matches); - }; +/** Set the visible light/dark mode on page load */ +export function setInitialMode(): void { + const currentMode = getCurrentMode(); + setCurrentMode(currentMode); } /** * Toggle between light and dark mode */ export function toggleMode(): void { - modeCurrent.update((curr) => { - const next = !curr; - setModeUserPrefers(next); - setModeCurrent(next); - return next; - }); + const currentMode = getCurrentMode(); + const systemMode = getSystemMode(); + + const nextMode = + currentMode === 'system' + ? systemMode === 'light' + ? 'dark' + : 'light' + : currentMode === 'light' + ? 'dark' + : 'light'; + + setCurrentMode(nextMode); } /** * Set the mode to light or dark */ export function setMode(mode: 'light' | 'dark'): void { - modeCurrent.update((curr) => { - const next = mode === 'light'; - if (curr === next) return curr; - setModeUserPrefers(next); - setModeCurrent(next); - return next; - }); + setCurrentMode(mode); } /** - * Reset the mode to OS preference + * Reset the mode to system preference */ export function resetMode(): void { - modeCurrent.update(() => { - setModeUserPrefers(undefined); - const next = getModeOsPrefers(); - setModeCurrent(next); - return next; - }); + setCurrentMode('system'); } diff --git a/src/tests/mode.spec.ts b/src/tests/mode.spec.ts index 443a47a..b0d4709 100644 --- a/src/tests/mode.spec.ts +++ b/src/tests/mode.spec.ts @@ -85,6 +85,7 @@ it('resets the mode to OS preferences (dark?)', async () => { const rootEl = container.parentElement; const light = getByTestId('light'); const reset = getByTestId('reset'); + const toggle = getByTestId('toggle'); const mode = getByTestId('mode'); const classes = getClasses(rootEl); const colorScheme = getColorScheme(rootEl); @@ -104,7 +105,14 @@ it('resets the mode to OS preferences (dark?)', async () => { const colorScheme3 = getColorScheme(rootEl); expect(classes3).toContain('dark'); expect(colorScheme3).toBe('dark'); - expect(mode.textContent).toBe('dark'); + expect(mode.textContent).toBe('system'); + + await userEvent.click(toggle); + const classes4 = getClasses(rootEl); + const colorScheme4 = getColorScheme(rootEl); + expect(classes4).not.toContain('dark'); + expect(colorScheme4).toBe('light'); + expect(mode.textContent).toBe('light'); }); function getClasses(element: HTMLElement | null): string[] {