Skip to content

Commit 816c64d

Browse files
committed
[site] Preserve theme across navigation
1 parent f69af27 commit 816c64d

3 files changed

Lines changed: 68 additions & 14 deletions

File tree

packages/lit-dev-content/src/global/theme-worker.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ type ColorMode = 'light' | 'dark';
88

99
type ThemeWorkerMessage =
1010
| {
11-
type: 'connect' | 'system-mode';
11+
type: 'connect';
1212
mode: ColorMode;
13+
overridden: boolean;
1314
}
1415
| {
15-
type: 'set-mode';
16+
type: 'system-mode' | 'set-mode';
1617
mode: ColorMode;
1718
};
1819

@@ -22,6 +23,7 @@ interface SharedWorkerScope {
2223

2324
const ports = new Set<MessagePort>();
2425
let mode: ColorMode | undefined;
26+
let overridden = false;
2527

2628
const isColorMode = (value: unknown): value is ColorMode =>
2729
value === 'light' || value === 'dark';
@@ -33,7 +35,7 @@ const publishMode = () => {
3335

3436
for (const port of ports) {
3537
try {
36-
port.postMessage({type: 'mode', mode});
38+
port.postMessage({type: 'mode', mode, overridden});
3739
} catch {
3840
ports.delete(port);
3941
}
@@ -57,13 +59,16 @@ const publishMode = () => {
5759
case 'connect':
5860
if (mode === undefined) {
5961
mode = data.mode;
62+
overridden = data.overridden === true;
6063
}
6164
break;
6265
case 'system-mode':
6366
mode = data.mode;
67+
overridden = false;
6468
break;
6569
case 'set-mode':
6670
mode = data.mode;
71+
overridden = true;
6772
break;
6873
}
6974

packages/lit-dev-content/src/global/theme.ts

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const COLOR_MODE_CHANGE_EVENT = 'litdev-color-mode-change';
1212

1313
interface ThemeState {
1414
mode: ColorMode;
15+
overridden: boolean;
1516
mediaQuery: MediaQueryList;
1617
worker?: SharedWorker;
1718
port?: MessagePort;
@@ -20,10 +21,12 @@ interface ThemeState {
2021
interface ThemeWorkerModeMessage {
2122
type: 'mode';
2223
mode: ColorMode;
24+
overridden: boolean;
2325
}
2426

2527
const THEME_STATE_KEY = Symbol.for('lit.dev.theme-state');
2628
const THEME_WORKER_PATH = '/js/global/theme-worker.js';
29+
const COLOR_MODE_STORAGE_KEY = 'color-mode';
2730

2831
const isColorMode = (value: unknown): value is ColorMode =>
2932
value === 'light' || value === 'dark';
@@ -43,14 +46,41 @@ const setThemeState = (state: ThemeState) => {
4346
)[THEME_STATE_KEY] = state;
4447
};
4548

49+
const getStoredColorMode = (): ColorMode | undefined => {
50+
try {
51+
const mode = sessionStorage.getItem(COLOR_MODE_STORAGE_KEY);
52+
return isColorMode(mode) ? mode : undefined;
53+
} catch {
54+
return undefined;
55+
}
56+
};
57+
58+
const storeColorMode = (mode: ColorMode | undefined) => {
59+
try {
60+
if (mode) {
61+
sessionStorage.setItem(COLOR_MODE_STORAGE_KEY, mode);
62+
} else {
63+
sessionStorage.removeItem(COLOR_MODE_STORAGE_KEY);
64+
}
65+
} catch {
66+
// Theme switching still works within this document when storage is blocked.
67+
}
68+
};
69+
4670
/**
4771
* Sets the theme on the page given a color mode.
4872
*
4973
* @param mode The source color to generate the theme.
5074
*/
51-
function applyColorMode(state: ThemeState, mode: ColorMode) {
75+
function applyColorMode(
76+
state: ThemeState,
77+
mode: ColorMode,
78+
overridden: boolean
79+
) {
5280
const modeChanged = state.mode !== mode;
5381
state.mode = mode;
82+
state.overridden = overridden;
83+
storeColorMode(overridden ? mode : undefined);
5484
document.body.classList.remove('light', 'dark', 'auto');
5585
document.body.classList.add(mode);
5686
updateMetaColor(mode);
@@ -118,20 +148,19 @@ export function initializeTheme() {
118148
}
119149

120150
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
151+
const storedMode = getStoredColorMode();
121152
const state: ThemeState = {
122-
mode: mediaQuery.matches ? 'dark' : 'light',
153+
mode: storedMode ?? (mediaQuery.matches ? 'dark' : 'light'),
154+
overridden: storedMode !== undefined,
123155
mediaQuery,
124156
};
125157
setThemeState(state);
126-
applyColorMode(state, state.mode);
158+
applyColorMode(state, state.mode, state.overridden);
127159

128160
mediaQuery.addEventListener('change', ({matches}) => {
129161
const mode = matches ? 'dark' : 'light';
130-
if (state.port) {
131-
state.port.postMessage({type: 'system-mode', mode});
132-
} else {
133-
applyColorMode(state, mode);
134-
}
162+
applyColorMode(state, mode, false);
163+
state.port?.postMessage({type: 'system-mode', mode});
135164
});
136165

137166
if (!('SharedWorker' in window)) {
@@ -160,10 +189,14 @@ export function initializeTheme() {
160189
return;
161190
}
162191

163-
applyColorMode(state, message.mode);
192+
applyColorMode(state, message.mode, message.overridden === true);
164193
});
165194
port.start();
166-
port.postMessage({type: 'connect', mode: state.mode});
195+
port.postMessage({
196+
type: 'connect',
197+
mode: state.mode,
198+
overridden: state.overridden,
199+
});
167200
} catch {
168201
state.worker = undefined;
169202
state.port = undefined;
@@ -180,6 +213,6 @@ export function setColorMode(mode: ColorMode) {
180213
return;
181214
}
182215

183-
applyColorMode(state, mode);
216+
applyColorMode(state, mode, true);
184217
state.port?.postMessage({type: 'set-mode', mode});
185218
}

packages/lit-dev-tests/src/playwright/home.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,16 @@ test.describe('Home page', () => {
102102
await expect(firstBody).toHaveClass(/\blight\b/);
103103
await expect(firstToggle).toHaveAttribute('aria-pressed', 'false');
104104
await expect(firstToggle).toHaveAttribute('aria-label', 'Dark mode');
105+
expect(
106+
await page.evaluate(() => sessionStorage.getItem('color-mode'))
107+
).toBe('light');
108+
109+
await page.goto('/docs/');
110+
await expect(firstBody).toHaveClass(/\blight\b/);
111+
await expect(firstToggle).toHaveAttribute('aria-pressed', 'false');
112+
expect(
113+
await page.evaluate(() => sessionStorage.getItem('color-mode'))
114+
).toBe('light');
105115

106116
const secondPage = await context.newPage();
107117
await secondPage.emulateMedia({colorScheme: 'dark'});
@@ -150,6 +160,9 @@ test.describe('Home page', () => {
150160
await waitForMediaChangeCount(2);
151161
await expect(firstBody).toHaveClass(/\bdark\b/);
152162
await expect(secondBody).toHaveClass(/\bdark\b/);
163+
expect(
164+
await secondPage.evaluate(() => sessionStorage.getItem('color-mode'))
165+
).toBe(null);
153166

154167
await secondToggle.click();
155168
await expect(firstBody).toHaveClass(/\blight\b/);
@@ -162,6 +175,9 @@ test.describe('Home page', () => {
162175
await resetPage.emulateMedia({colorScheme: 'dark'});
163176
await resetPage.goto('/');
164177
await expect(resetPage.locator('body')).toHaveClass(/\bdark\b/);
178+
expect(
179+
await resetPage.evaluate(() => sessionStorage.getItem('color-mode'))
180+
).toBe(null);
165181
await expect(
166182
resetPage.locator(
167183
'#desktopNav theme-switcher button[aria-label="Dark mode"]'

0 commit comments

Comments
 (0)