Skip to content
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

refactor mode.ts to use a single persistent store #9

Closed
wants to merge 1 commit into from
Closed
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<script lang="ts">
Expand Down
8 changes: 4 additions & 4 deletions src/lib/mode-watcher.svelte
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
<script lang="ts">
import { onMount } from 'svelte';
import { getModeOsPrefers, setInitialClassState, setModeCurrent } from './mode.js';
import { getSystemMode, setInitialMode, setCurrentMode } from './mode.js';

onMount(() => {
if (!('modeCurrent' in localStorage)) {
setModeCurrent(getModeOsPrefers());
if (!('mode' in localStorage)) {
setCurrentMode(getSystemMode());
}
});
</script>

<svelte:head>
<!-- This causes the new eslint-plugin-svelte: https://github.com/sveltejs/eslint-plugin-svelte/issues/492 -->
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html `<\u{73}cript nonce="%sveltekit.nonce%">(${setInitialClassState.toString()})();</script>`}
{@html `<\u{73}cript nonce="%sveltekit.nonce%">(${setInitialMode.toString()})();</script>`}
</svelte:head>
141 changes: 41 additions & 100 deletions src/lib/mode.ts
Original file line number Diff line number Diff line change
@@ -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<boolean>('modeOsPrefers', false);
const modeUserPrefers = persisted<boolean | undefined>('modeUserPrefers', undefined);
const modeCurrent = persisted<boolean>('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');
}
10 changes: 9 additions & 1 deletion src/tests/mode.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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[] {
Expand Down