Skip to content

Commit 75d3aa2

Browse files
authored
History: Stub out support for theming (#2066)
* Add theme, themeVariant and onThemeUpdate types * Add ThemeProvider and theme variant support * Add history theme integration tests * Fix theme not being updated when media query changes
1 parent 9561b9c commit 75d3aa2

File tree

13 files changed

+256
-13
lines changed

13 files changed

+256
-13
lines changed

special-pages/pages/history/app/components/App.jsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { h } from 'preact';
22
import cn from 'classnames';
33
import styles from './App.module.css';
4-
import { useEnv } from '../../../../shared/components/EnvironmentProvider.js';
54
import { Header } from './Header.js';
65
import { ResultsContainer } from './Results.js';
76
import { useEffect, useRef } from 'preact/hooks';
@@ -20,11 +19,12 @@ import { useRangesData } from '../global/Providers/HistoryServiceProvider.js';
2019
import { usePlatformName } from '../types.js';
2120
import { useLayoutMode } from '../global/hooks/useLayoutMode.js';
2221
import { useClickAnywhereElse } from '../global/hooks/useClickAnywhereElse.jsx';
22+
import { useTheme } from '../global/Providers/ThemeProvider.js';
2323

2424
export function App() {
2525
const platformName = usePlatformName();
2626
const mainRef = useRef(/** @type {HTMLElement|null} */ (null));
27-
const { isDarkMode } = useEnv();
27+
const { theme, themeVariant } = useTheme();
2828
const ranges = useRangesData();
2929
const query = useQueryContext();
3030
const mode = useLayoutMode();
@@ -66,7 +66,8 @@ export function App() {
6666
return (
6767
<div
6868
class={styles.layout}
69-
data-theme={isDarkMode ? 'dark' : 'light'}
69+
data-theme={theme}
70+
data-theme-variant={themeVariant}
7071
data-platform={platformName}
7172
data-layout-mode={mode}
7273
onClick={clickAnywhere}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { createContext, h } from 'preact';
2+
import { useContext, useEffect, useState } from 'preact/hooks';
3+
import { useMessaging } from '../../types.js';
4+
import { useEnv } from '../../../../../shared/components/EnvironmentProvider.js';
5+
6+
/**
7+
* @typedef {import('../../../types/history').BrowserTheme} BrowserTheme
8+
* @typedef {import('../../../types/history').ThemeVariant} ThemeVariant
9+
*/
10+
11+
const ThemeContext = createContext({
12+
/** @type {BrowserTheme} */
13+
theme: 'light',
14+
/** @type {ThemeVariant} */
15+
themeVariant: 'default',
16+
});
17+
18+
/**
19+
* @param {object} props
20+
* @param {import('preact').ComponentChild} props.children
21+
* @param {BrowserTheme | undefined} props.initialTheme
22+
* @param {ThemeVariant | undefined} props.initialThemeVariant
23+
*/
24+
export function ThemeProvider({ children, initialTheme, initialThemeVariant }) {
25+
const { isDarkMode } = useEnv();
26+
const history = useMessaging();
27+
28+
// Track explicit theme updates from onThemeUpdate subscription
29+
const [explicitTheme, setExplicitTheme] = useState(/** @type {BrowserTheme | undefined} */ (undefined));
30+
const [explicitThemeVariant, setExplicitThemeVariant] = useState(/** @type {ThemeVariant | undefined} */ (undefined));
31+
32+
useEffect(() => {
33+
const unsubscribe = history.messaging.subscribe('onThemeUpdate', (data) => {
34+
setExplicitTheme(data.theme);
35+
setExplicitThemeVariant(data.themeVariant);
36+
});
37+
return unsubscribe;
38+
}, [history]);
39+
40+
// Derive theme from explicit updates, initial theme, or system preference (in that order)
41+
const theme = explicitTheme ?? initialTheme ?? (isDarkMode ? 'dark' : 'light');
42+
const themeVariant = explicitThemeVariant ?? initialThemeVariant ?? 'default';
43+
44+
return <ThemeContext.Provider value={{ theme, themeVariant }}>{children}</ThemeContext.Provider>;
45+
}
46+
47+
export function useTheme() {
48+
return useContext(ThemeContext);
49+
}

special-pages/pages/history/app/index.js

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { Settings } from './Settings.js';
1515
import { SelectionProvider } from './global/Providers/SelectionProvider.js';
1616
import { QueryProvider } from './global/Providers/QueryProvider.js';
1717
import { InlineErrorBoundary } from '../../../shared/components/InlineErrorBoundary.js';
18+
import { ThemeProvider } from './global/Providers/ThemeProvider.js';
1819

1920
/**
2021
* @param {Element} root
@@ -84,15 +85,17 @@ export async function init(root, messaging, baseEnvironment) {
8485
<UpdateEnvironment search={window.location.search} />
8586
<TranslationProvider translationObject={strings} fallback={enStrings} textLength={environment.textLength}>
8687
<MessagingContext.Provider value={messaging}>
87-
<SettingsContext.Provider value={settings}>
88-
<QueryProvider query={query.query}>
89-
<HistoryServiceProvider service={service} initial={initial}>
90-
<SelectionProvider>
91-
<App />
92-
</SelectionProvider>
93-
</HistoryServiceProvider>
94-
</QueryProvider>
95-
</SettingsContext.Provider>
88+
<ThemeProvider initialTheme={init.theme} initialThemeVariant={init.themeVariant}>
89+
<SettingsContext.Provider value={settings}>
90+
<QueryProvider query={query.query}>
91+
<HistoryServiceProvider service={service} initial={initial}>
92+
<SelectionProvider>
93+
<App />
94+
</SelectionProvider>
95+
</HistoryServiceProvider>
96+
</QueryProvider>
97+
</SettingsContext.Provider>
98+
</ThemeProvider>
9699
</MessagingContext.Provider>
97100
</TranslationProvider>
98101
</EnvironmentProvider>
@@ -117,6 +120,9 @@ export async function init(root, messaging, baseEnvironment) {
117120
* @param {import("../types/history.ts").DefaultStyles | null | undefined} defaultStyles
118121
*/
119122
function applyDefaultStyles(defaultStyles) {
123+
if (defaultStyles?.lightBackgroundColor || defaultStyles?.darkBackgroundColor) {
124+
console.warn('defaultStyles is deprecated. Use themeVariant instead. This will override theme variant colors.', defaultStyles);
125+
}
120126
if (defaultStyles?.lightBackgroundColor) {
121127
document.body.style.setProperty('--default-light-background-color', defaultStyles.lightBackgroundColor);
122128
}

special-pages/pages/history/app/mocks/mock-transport.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,23 @@ export function mockTransport() {
163163
},
164164
};
165165

166+
// Allow theme override via URL params
167+
if (url.searchParams.has('theme')) {
168+
const value = url.searchParams.get('theme');
169+
if (value === 'light' || value === 'dark') {
170+
initial.theme = /** @type {import('../../types/history.ts').BrowserTheme} */ (value);
171+
}
172+
}
173+
174+
// Allow themeVariant override via URL params
175+
if (url.searchParams.has('themeVariant')) {
176+
const value = url.searchParams.get('themeVariant');
177+
const validVariants = ['default', 'coolGray', 'slateBlue', 'green', 'violet', 'rose', 'orange', 'desert'];
178+
if (value && validVariants.includes(value)) {
179+
initial.themeVariant = /** @type {import('../../types/history.ts').ThemeVariant} */ (value);
180+
}
181+
}
182+
166183
return Promise.resolve(initial);
167184
}
168185

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { test } from '@playwright/test';
2+
import { HistoryTestPage } from './history.page.js';
3+
4+
test.describe('history theme and theme variants', () => {
5+
test('setting theme = dark and themeVariant via initialSetup', async ({ page }, workerInfo) => {
6+
const hp = HistoryTestPage.create(page, workerInfo);
7+
await hp.openPage({ additional: { theme: 'dark', themeVariant: 'violet' } });
8+
await hp.hasTheme('dark', 'violet');
9+
await hp.hasBackgroundColor({ hex: '#2e2158' });
10+
});
11+
12+
test('setting theme = light and themeVariant via initialSetup', async ({ page }, workerInfo) => {
13+
const hp = HistoryTestPage.create(page, workerInfo);
14+
await hp.openPage({ additional: { theme: 'light', themeVariant: 'coolGray' } });
15+
await hp.hasTheme('light', 'coolGray');
16+
await hp.hasBackgroundColor({ hex: '#d2d5e3' });
17+
});
18+
19+
test('light theme and default themeVariant when unspecified', async ({ page }, workerInfo) => {
20+
const hp = HistoryTestPage.create(page, workerInfo);
21+
await hp.openPage();
22+
await hp.hasTheme('light', 'default');
23+
await hp.hasBackgroundColor({ hex: '#fafafa' });
24+
});
25+
26+
test('dark theme and default themeVariant when unspecified', async ({ page }, workerInfo) => {
27+
const hp = HistoryTestPage.create(page, workerInfo);
28+
await hp.darkMode();
29+
await hp.openPage();
30+
await hp.hasTheme('dark', 'default');
31+
await hp.hasBackgroundColor({ hex: '#333333' });
32+
});
33+
34+
test('changing theme to dark and themeVariant using onThemeUpdate', async ({ page }, workerInfo) => {
35+
const hp = HistoryTestPage.create(page, workerInfo);
36+
await hp.openPage({ additional: { theme: 'light', themeVariant: 'desert' } });
37+
await hp.hasTheme('light', 'desert');
38+
await hp.hasBackgroundColor({ hex: '#eee9e1' });
39+
await hp.acceptsThemeUpdate('dark', 'slateBlue');
40+
await hp.hasTheme('dark', 'slateBlue');
41+
await hp.hasBackgroundColor({ hex: '#1e3347' });
42+
});
43+
44+
test('changing theme to light and themeVariant using onThemeUpdate', async ({ page }, workerInfo) => {
45+
const hp = HistoryTestPage.create(page, workerInfo);
46+
await hp.openPage({ additional: { theme: 'dark', themeVariant: 'rose' } });
47+
await hp.hasTheme('dark', 'rose');
48+
await hp.hasBackgroundColor({ hex: '#5b194b' });
49+
await hp.acceptsThemeUpdate('light', 'green');
50+
await hp.hasTheme('light', 'green');
51+
await hp.hasBackgroundColor({ hex: '#e3eee1' });
52+
});
53+
});

special-pages/pages/history/integration-tests/history.page.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,23 @@ export class HistoryTestPage {
610610
});
611611
expect(borderTopColor).toBe(rgb);
612612
}
613+
614+
/**
615+
* @param {import('../types/history.ts').BrowserTheme} theme
616+
* @param {import('../types/history.ts').ThemeVariant} themeVariant
617+
*/
618+
async acceptsThemeUpdate(theme, themeVariant) {
619+
await this.mocks.simulateSubscriptionMessage('onThemeUpdate', { theme, themeVariant });
620+
}
621+
622+
/**
623+
* @param {import('../types/history.ts').BrowserTheme} theme
624+
* @param {import('../types/history.ts').ThemeVariant} themeVariant
625+
*/
626+
async hasTheme(theme, themeVariant) {
627+
await expect(this.page.locator('[data-layout-mode]')).toHaveAttribute('data-theme', theme);
628+
await expect(this.page.locator('[data-layout-mode]')).toHaveAttribute('data-theme-variant', themeVariant);
629+
}
613630
}
614631

615632
/**

special-pages/pages/history/messages/initialSetup.response.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,17 @@
2020
}
2121
}
2222
},
23+
"theme": {
24+
"$ref": "./types/browser-theme.json"
25+
},
26+
"themeVariant": {
27+
"$ref": "./types/theme-variant.json"
28+
},
2329
"customizer": {
2430
"type": "object",
2531
"properties": {
2632
"defaultStyles": {
33+
"deprecated": true,
2734
"oneOf": [
2835
{
2936
"type": "null"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"type": "object",
4+
"required": ["theme", "themeVariant"],
5+
"properties": {
6+
"theme": {
7+
"$ref": "./types/browser-theme.json"
8+
},
9+
"themeVariant": {
10+
"$ref": "./types/theme-variant.json"
11+
}
12+
}
13+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"title": "Browser Theme",
4+
"enum": [
5+
"light",
6+
"dark"
7+
]
8+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"title": "Theme Variant",
4+
"enum": [
5+
"default",
6+
"coolGray",
7+
"slateBlue",
8+
"green",
9+
"violet",
10+
"rose",
11+
"orange",
12+
"desert"
13+
]
14+
}

0 commit comments

Comments
 (0)