Skip to content

Commit

Permalink
feat: Custom Class Names (#80)
Browse files Browse the repository at this point in the history
  • Loading branch information
huntabyte authored Jul 14, 2024
1 parent d3fa031 commit aa1759c
Show file tree
Hide file tree
Showing 14 changed files with 5,257 additions and 4,208 deletions.
5 changes: 5 additions & 0 deletions .changeset/calm-dots-shake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"mode-watcher": minor
---

feat: Custom ClassNames
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
with:
version: 8.6.3
run_install: true
- run: pnpm run lint
- run: pnpm build:packages && pnpm lint

Check:
runs-on: ubuntu-latest
Expand All @@ -33,7 +33,7 @@ jobs:
with:
version: 8.6.3
run_install: true
- run: pnpm run build && pnpm run check
- run: pnpm check

Tests:
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"ci:publish": "pnpm build:packages && changeset publish",
"lint": "pnpm -r lint",
"format": "pnpm -r format",
"check": "pnpm -r check"
"check": "pnpm build:packages && pnpm -r check"
},
"license": "MIT",
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion packages/mode-watcher/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"test": "vitest",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --plugin-search-dir . --check . && eslint .",
"lint": "eslint .",
"format": "prettier --plugin-search-dir . --write .",
"watch": "svelte-kit sync && svelte-package --watch"
},
Expand Down
19 changes: 17 additions & 2 deletions packages/mode-watcher/src/lib/mode-watcher.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,30 @@
} from './mode.js';
import type { Mode, ModeWatcherProps, ThemeColors } from './types.js';
import { isValidMode } from './stores.js';
import {
darkClassNames as darkClassNamesStore,
isValidMode,
lightClassNames as lightClassNamesStore,
} from './stores.js';
type $$Props = ModeWatcherProps;
export let track = true;
export let defaultMode: Mode = 'system';
export let themeColors: ThemeColors = undefined;
export let disableTransitions = true;
export let darkClassNames: string[] = ['dark'];
export let lightClassNames: string[] = [];
themeColorsStore.set(themeColors);
disableTransitionsStore.set(disableTransitions);
darkClassNamesStore.set(darkClassNames);
lightClassNamesStore.set(lightClassNames);
$: disableTransitionsStore.set(disableTransitions);
$: themeColorsStore.set(themeColors);
$: darkClassNamesStore.set(darkClassNames);
$: lightClassNamesStore.set(lightClassNames);
onMount(() => {
const unsubscriber = mode.subscribe(() => {});
Expand All @@ -35,7 +48,9 @@
};
});
const args = `"${defaultMode}"${themeColors ? `, ${JSON.stringify(themeColors)}` : ''}`;
const args = `"${defaultMode}"${
themeColors ? `, ${JSON.stringify(themeColors)}` : ', undefined'
}, ${JSON.stringify(darkClassNames)}, ${JSON.stringify(lightClassNames)}`;
</script>

<svelte:head>
Expand Down
19 changes: 16 additions & 3 deletions packages/mode-watcher/src/lib/mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
themeColors,
disableTransitions,
} from './stores.js';
import { sanitizeClassNames } from './utils.js';
import type { Mode, ThemeColors } from './types.js';

/** Toggle between light and dark mode */
Expand All @@ -25,14 +26,26 @@ export function resetMode(): void {
}

/** Used to set the mode on initial page load to prevent FOUC */
export function setInitialMode(defaultMode: Mode, themeColors?: ThemeColors) {
export function setInitialMode(
defaultMode: Mode,
themeColors?: ThemeColors,
darkClassNames: string[] = ['dark'],
lightClassNames: string[] = []
) {
const rootEl = document.documentElement;
const mode = localStorage.getItem('mode-watcher-mode') || defaultMode;
const light =
mode === 'light' ||
(mode === 'system' && window.matchMedia('(prefers-color-scheme: light)').matches);

rootEl.classList[light ? 'remove' : 'add']('dark');
const sanitizedDarkClassNames = sanitizeClassNames(darkClassNames);
const sanitizedLightClassNames = sanitizeClassNames(lightClassNames);
if (light) {
if (sanitizedDarkClassNames.length) rootEl.classList.remove(...sanitizedDarkClassNames);
if (sanitizedLightClassNames.length) rootEl.classList.add(...sanitizedLightClassNames);
} else {
if (sanitizedLightClassNames.length) rootEl.classList.remove(...sanitizedLightClassNames);
if (sanitizedDarkClassNames.length) rootEl.classList.add(...sanitizedDarkClassNames);
}
rootEl.style.colorScheme = light ? 'light' : 'dark';

if (themeColors) {
Expand Down
37 changes: 33 additions & 4 deletions packages/mode-watcher/src/lib/stores.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { writable, derived } from 'svelte/store';
import { withoutTransition } from './without-transition.js';
import type { Mode, ThemeColors } from './types.js';
import { sanitizeClassNames } from './utils.js';

// saves having to branch for server vs client
const noopStorage = {
Expand Down Expand Up @@ -38,6 +39,16 @@ export const themeColors = writable<ThemeColors>(undefined);
*/
export const disableTransitions = writable(true);

/**
* The classnames to add to the root `html` element when the mode is dark.
*/
export const darkClassNames = writable<string[]>([]);

/**
* The classnames to add to the root `html` element when the mode is light.
*/
export const lightClassNames = writable<string[]>([]);

/**
* Derived store that represents the current mode (`"dark"`, `"light"` or `undefined`)
*/
Expand Down Expand Up @@ -120,23 +131,41 @@ function createSystemMode() {

function createDerivedMode() {
const { subscribe } = derived(
[userPrefersMode, systemPrefersMode, themeColors, disableTransitions],
([$userPrefersMode, $systemPrefersMode, $themeColors, $disableTransitions]) => {
[
userPrefersMode,
systemPrefersMode,
themeColors,
disableTransitions,
darkClassNames,
lightClassNames,
],
([
$userPrefersMode,
$systemPrefersMode,
$themeColors,
$disableTransitions,
$darkClassNames,
$lightClassNames,
]) => {
if (!isBrowser) return undefined;

const derivedMode = $userPrefersMode === 'system' ? $systemPrefersMode : $userPrefersMode;
const sanitizedDarkClassNames = sanitizeClassNames($darkClassNames);
const sanitizedLightClassNames = sanitizeClassNames($lightClassNames);

function update() {
const htmlEl = document.documentElement;
const themeColorEl = document.querySelector('meta[name="theme-color"]');
if (derivedMode === 'light') {
htmlEl.classList.remove('dark');
if (sanitizedDarkClassNames.length) htmlEl.classList.remove(...sanitizedDarkClassNames);
if (sanitizedLightClassNames.length) htmlEl.classList.add(...sanitizedLightClassNames);
htmlEl.style.colorScheme = 'light';
if (themeColorEl && $themeColors) {
themeColorEl.setAttribute('content', $themeColors.light);
}
} else {
htmlEl.classList.add('dark');
if (sanitizedLightClassNames.length) htmlEl.classList.remove(...sanitizedLightClassNames);
if (sanitizedDarkClassNames.length) htmlEl.classList.add(...sanitizedDarkClassNames);
htmlEl.style.colorScheme = 'dark';
if (themeColorEl && $themeColors) {
themeColorEl.setAttribute('content', $themeColors.dark);
Expand Down
14 changes: 14 additions & 0 deletions packages/mode-watcher/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,18 @@ export type ModeWatcherProps = {
* Whether to disable transitions when updating the mode.
*/
disableTransitions?: boolean;

/**
* The classname to add to the root `html` element when the mode is dark.
*
* @defaultValue `["dark"]`
*/
darkClassNames?: string[];

/**
* The classname to add to the root `html` element when the mode is light.
*
* @defaultValue `[]`
*/
lightClassNames?: string[];
};
6 changes: 6 additions & 0 deletions packages/mode-watcher/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Santizes an array of classnames by removing any empty strings.
*/
export function sanitizeClassNames(classNames: string[]): string[] {
return classNames.filter((className) => className.length > 0);
}
9 changes: 8 additions & 1 deletion packages/mode-watcher/src/tests/Mode.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@
import { ModeWatcher, toggleMode, setMode, resetMode, mode } from '$lib/index.js';
export let track = true;
export let darkClassNames: string[] = ['dark'];
export let lightClassNames: string[] = [];
</script>

<ModeWatcher {track} themeColors={{ dark: 'black', light: 'white' }} />
<ModeWatcher
{track}
{darkClassNames}
{lightClassNames}
themeColors={{ dark: 'black', light: 'white' }}
/>
<span data-testid="mode">{$mode}</span>
<button on:click={toggleMode} data-testid="toggle"> toggle </button>
<button on:click={() => setMode('light')} data-testid="light">light</button>
Expand Down
40 changes: 23 additions & 17 deletions packages/mode-watcher/src/tests/mode.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import StealthMode from './StealthMode.svelte';
import { userEvent } from '@testing-library/user-event';
import { mediaQueryState } from '../../scripts/setupTest.js';
import { tick } from 'svelte';
import type { ModeWatcherProps } from '$lib/types.js';

function setup() {
function setup(props: Partial<ModeWatcherProps> = {}) {
const user = userEvent.setup();
const returned = render(Mode);
const returned = render(Mode, { props });
return { user, ...returned };
}

Expand Down Expand Up @@ -318,35 +319,40 @@ it('also works when $mode is not used in the current page', async () => {
expect(themeColor3).toBe('black');
});

it('allows the user to apply custom classnames to the root html element', async () => {
const { container, getByTestId, user } = setup({
darkClassNames: ['custom-d-class'],
lightClassNames: ['custom-l-class'],
});
const rootEl = container.parentElement;

const classes = getClasses(rootEl);
expect(classes).toContain('custom-d-class');
const toggle = getByTestId('toggle');
await user.click(toggle);
const classes2 = getClasses(rootEl);
expect(classes2).toContain('custom-l-class');
});

function getClasses(element: HTMLElement | null): string[] {
if (element === null) {
return [];
}
if (element === null) return [];
const classes = element.className.split(' ').filter((c) => c.length > 0);
return classes;
}

function getColorScheme(element: HTMLElement | null) {
if (element === null) {
return '';
}
if (element === null) return '';
return element.style.colorScheme;
}

function getThemeColor(element: HTMLElement | null) {
if (element === null) {
return '';
}
if (element === null) return '';

const themeMetaEl = element.querySelector('meta[name="theme-color"]');
if (themeMetaEl === null) {
return '';
}
if (themeMetaEl === null) return '';

const content = themeMetaEl.getAttribute('content');
if (content === null) {
return '';
}
if (content === null) return '';

return content;
}
Loading

0 comments on commit aa1759c

Please sign in to comment.