Skip to content

Commit 89ede66

Browse files
authored
feat: Set selected theme via query string param (#2204)
Allow setting selected theme via a `themeKey` query string param. Can test by passing `themeKey=default-light` and `themeKey=default-dark` as query string params. resolves #2203
1 parent 0a924cd commit 89ede66

6 files changed

Lines changed: 111 additions & 92 deletions

File tree

packages/components/src/theme/ThemeModel.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export type ThemeIconsRequiringManualColorChanges =
4343

4444
export const DEFAULT_DARK_THEME_KEY = 'default-dark' satisfies BaseThemeKey;
4545
export const DEFAULT_LIGHT_THEME_KEY = 'default-light' satisfies BaseThemeKey;
46+
export const THEME_KEY_OVERRIDE_QUERY_PARAM = 'theme';
4647

4748
// Hex versions of some of the default dark theme color palette needed for
4849
// preload defaults.

packages/components/src/theme/ThemeProvider.test.tsx

Lines changed: 24 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,12 @@ import React from 'react';
22
import { act, render } from '@testing-library/react';
33
import { assertNotNull, TestUtils } from '@deephaven/utils';
44
import { ThemeContextValue, ThemeProvider } from './ThemeProvider';
5-
import {
6-
DEFAULT_DARK_THEME_KEY,
7-
DEFAULT_LIGHT_THEME_KEY,
8-
ThemeData,
9-
ThemePreloadData,
10-
} from './ThemeModel';
5+
import { DEFAULT_LIGHT_THEME_KEY, ThemeData } from './ThemeModel';
116
import {
127
calculatePreloadStyleContent,
138
getActiveThemes,
149
getDefaultBaseThemes,
15-
getThemePreloadData,
10+
getDefaultSelectedThemeKey,
1611
setThemePreloadData,
1712
} from './ThemeUtils';
1813
import { useTheme } from './useTheme';
@@ -24,13 +19,17 @@ jest.mock('./ThemeUtils', () => {
2419
return {
2520
...actual,
2621
calculatePreloadStyleContent: jest.fn(),
27-
getThemePreloadData: jest.fn(actual.getThemePreloadData),
22+
getDefaultSelectedThemeKey: jest.fn(),
23+
getThemeKeyOverride: jest.fn(),
2824
setThemePreloadData: jest.fn(),
2925
};
3026
});
3127

32-
const customThemes = [{ themeKey: 'themeA' }] as [ThemeData];
33-
const preloadA: ThemePreloadData = { themeKey: 'themeA' };
28+
const customThemes = [
29+
{ themeKey: 'themeA' },
30+
{ themeKey: 'mockDefaultSelectedThemeKey' },
31+
] as ThemeData[];
32+
const defaultSelectedThemeKey = 'mockDefaultSelectedThemeKey';
3433

3534
beforeEach(() => {
3635
jest.clearAllMocks();
@@ -41,7 +40,10 @@ beforeEach(() => {
4140
.mockName('calculatePreloadStyleContent')
4241
.mockReturnValue(':root{mock-preload-content}');
4342

44-
asMock(getThemePreloadData).mockName('getThemePreloadData');
43+
asMock(getDefaultSelectedThemeKey)
44+
.mockName('getDefaultSelectedThemeKey')
45+
.mockReturnValue(defaultSelectedThemeKey);
46+
4547
asMock(setThemePreloadData).mockName('setThemePreloadData');
4648
});
4749

@@ -57,16 +59,9 @@ describe('ThemeProvider', () => {
5759
themeContextValueRef.current = null;
5860
});
5961

60-
it.each([
61-
[null, null],
62-
[null, preloadA],
63-
[customThemes, null],
64-
[customThemes, preloadA],
65-
] as const)(
66-
'should load themes based on preload data or default: %s, %s',
67-
(themes, preloadData) => {
68-
asMock(getThemePreloadData).mockReturnValue(preloadData);
69-
62+
it.each([null, customThemes])(
63+
'should load themes based on default selected theme key. customThemes: %o',
64+
themes => {
7065
const component = render(
7166
<ThemeProvider themes={themes}>
7267
<MockChild />
@@ -79,31 +74,24 @@ describe('ThemeProvider', () => {
7974
expect(themeContextValueRef.current.activeThemes).toBeNull();
8075
} else {
8176
expect(themeContextValueRef.current.activeThemes).toEqual(
82-
getActiveThemes(preloadData?.themeKey ?? DEFAULT_DARK_THEME_KEY, {
77+
getActiveThemes(defaultSelectedThemeKey, {
8378
base: getDefaultBaseThemes(),
8479
custom: themes,
8580
})
8681
);
8782

8883
expect(themeContextValueRef.current.selectedThemeKey).toEqual(
89-
preloadData?.themeKey ?? DEFAULT_DARK_THEME_KEY
84+
defaultSelectedThemeKey
9085
);
9186
}
9287

9388
expect(component.baseElement).toMatchSnapshot();
9489
}
9590
);
9691

97-
it.each([
98-
[null, null],
99-
[null, preloadA],
100-
[customThemes, null],
101-
[customThemes, preloadA],
102-
] as const)(
103-
'should set preload data when active themes change: %s, %s',
104-
(themes, preloadData) => {
105-
asMock(getThemePreloadData).mockReturnValue(preloadData);
106-
92+
it.each([null, customThemes] as const)(
93+
'should set preload data when active themes change: %o',
94+
themes => {
10795
render(
10896
<ThemeProvider themes={themes}>
10997
<MockChild />
@@ -114,14 +102,14 @@ describe('ThemeProvider', () => {
114102
expect(setThemePreloadData).not.toHaveBeenCalled();
115103
} else {
116104
expect(setThemePreloadData).toHaveBeenCalledWith({
117-
themeKey: preloadData?.themeKey ?? DEFAULT_DARK_THEME_KEY,
118-
preloadStyleContent: calculatePreloadStyleContent(),
105+
themeKey: defaultSelectedThemeKey,
106+
preloadStyleContent: calculatePreloadStyleContent({}),
119107
});
120108
}
121109
}
122110
);
123111

124-
describe.each([null, customThemes])('setSelectedThemeKey: %s', themes => {
112+
describe.each([null, customThemes])('setSelectedThemeKey: %o', themes => {
125113
it.each([DEFAULT_LIGHT_THEME_KEY, customThemes[0].themeKey])(
126114
'should change selected theme: %s',
127115
themeKey => {

packages/components/src/theme/ThemeProvider.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,13 @@
11
import { createContext, ReactNode, useEffect, useMemo, useState } from 'react';
22
import Log from '@deephaven/log';
3-
import {
4-
DEFAULT_DARK_THEME_KEY,
5-
DEFAULT_PRELOAD_DATA_VARIABLES,
6-
ThemeData,
7-
} from './ThemeModel';
3+
import { DEFAULT_PRELOAD_DATA_VARIABLES, ThemeData } from './ThemeModel';
84
import {
95
calculatePreloadStyleContent,
106
getActiveThemes,
117
getDefaultBaseThemes,
12-
getThemePreloadData,
138
setThemePreloadData,
149
overrideSVGFillColors,
10+
getDefaultSelectedThemeKey,
1511
} from './ThemeUtils';
1612
import { SpectrumThemeProvider } from './SpectrumThemeProvider';
1713
import './theme-svg.scss';
@@ -48,7 +44,7 @@ export function ThemeProvider({
4844
const [value, setValue] = useState<ThemeContextValue | null>(null);
4945

5046
const [selectedThemeKey, setSelectedThemeKey] = useState<string>(
51-
() => getThemePreloadData()?.themeKey ?? DEFAULT_DARK_THEME_KEY
47+
getDefaultSelectedThemeKey
5248
);
5349

5450
// Calculate active themes once a non-null themes array is provided.

packages/components/src/theme/ThemeUtils.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ import {
1010
ThemePreloadColorVariable,
1111
ThemeRegistrationData,
1212
THEME_CACHE_LOCAL_STORAGE_KEY,
13+
THEME_KEY_OVERRIDE_QUERY_PARAM,
1314
} from './ThemeModel';
1415
import {
1516
calculatePreloadStyleContent,
1617
createCssVariableResolver,
1718
extractDistinctCssVariableExpressions,
1819
getActiveThemes,
1920
getDefaultBaseThemes,
21+
getDefaultSelectedThemeKey,
2022
getExpressionRanges,
2123
getThemeKey,
2224
getThemePreloadData,
@@ -239,6 +241,44 @@ describe('getActiveThemes', () => {
239241
});
240242
});
241243

244+
describe('getDefaultSelectedThemeKey', () => {
245+
const origLocation = window.location;
246+
247+
beforeEach(() => {
248+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
249+
// @ts-ignore
250+
delete window.location;
251+
window.location = {
252+
search: '',
253+
} as unknown as Location;
254+
});
255+
256+
afterEach(() => {
257+
window.location = origLocation;
258+
});
259+
260+
it.each([
261+
['overrideKey', 'preloadKey', 'overrideKey'],
262+
[undefined, 'preloadKey', 'preloadKey'],
263+
[undefined, undefined, DEFAULT_DARK_THEME_KEY],
264+
])(
265+
'should coalesce overide key -> preload key -> default key: %s, %s, %s',
266+
(overrideKey, preloadKey, expected) => {
267+
if (overrideKey != null) {
268+
window.location.search = `?${THEME_KEY_OVERRIDE_QUERY_PARAM}=${overrideKey}`;
269+
}
270+
271+
localStorage.setItem(
272+
THEME_CACHE_LOCAL_STORAGE_KEY,
273+
JSON.stringify({ themeKey: preloadKey })
274+
);
275+
276+
const actual = getDefaultSelectedThemeKey();
277+
expect(actual).toEqual(expected);
278+
}
279+
);
280+
});
281+
242282
describe('getExpressionRanges', () => {
243283
const testCases = [
244284
['Single expression', '#ffffff', [[0, 6]]],

packages/components/src/theme/ThemeUtils.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
SVG_ICON_MANUAL_COLOR_MAP,
1616
ThemeCssVariableName,
1717
ThemeIconsRequiringManualColorChanges,
18+
THEME_KEY_OVERRIDE_QUERY_PARAM,
1819
} from './ThemeModel';
1920

2021
const log = Log.module('ThemeUtils');
@@ -178,6 +179,31 @@ export function getDefaultBaseThemes(): ThemeData[] {
178179
];
179180
}
180181

182+
/**
183+
* Get the default selected theme key. Precedence is:
184+
* 1. Theme key override query parameter
185+
* 2. Theme key from preload data
186+
* 3. Default dark theme key
187+
* @returns The default selected theme key
188+
*/
189+
export function getDefaultSelectedThemeKey(): string {
190+
return (
191+
getThemeKeyOverride() ??
192+
getThemePreloadData()?.themeKey ??
193+
DEFAULT_DARK_THEME_KEY
194+
);
195+
}
196+
197+
/**
198+
* A theme key override can be set via a query parameter to force a specific
199+
* theme selection. Useful for embedded widget scenarios that don't expose the
200+
* theme selector.
201+
*/
202+
export function getThemeKeyOverride(): string | null {
203+
const searchParams = new URLSearchParams(window.location.search);
204+
return searchParams.get(THEME_KEY_OVERRIDE_QUERY_PARAM);
205+
}
206+
181207
/**
182208
* Get the preload data from local storage or null if it does not exist or is
183209
* invalid

packages/components/src/theme/__snapshots__/ThemeProvider.test.tsx.snap

Lines changed: 17 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`ThemeProvider setSelectedThemeKey: [ [Object] ] should change selected theme: default-light 1`] = `
3+
exports[`ThemeProvider setSelectedThemeKey: [
4+
{ themeKey: 'themeA' },
5+
{ themeKey: 'mockDefaultSelectedThemeKey' },
6+
[length]: 2
7+
] should change selected theme: default-light 1`] = `
48
<body>
59
<div>
610
<style
@@ -27,7 +31,11 @@ exports[`ThemeProvider setSelectedThemeKey: [ [Object] ] should change selected
2731
</body>
2832
`;
2933

30-
exports[`ThemeProvider setSelectedThemeKey: [ [Object] ] should change selected theme: themeA 1`] = `
34+
exports[`ThemeProvider setSelectedThemeKey: [
35+
{ themeKey: 'themeA' },
36+
{ themeKey: 'mockDefaultSelectedThemeKey' },
37+
[length]: 2
38+
] should change selected theme: themeA 1`] = `
3139
<body>
3240
<div>
3341
<style
@@ -91,7 +99,11 @@ exports[`ThemeProvider setSelectedThemeKey: null should change selected theme: t
9199
</body>
92100
`;
93101

94-
exports[`ThemeProvider should load themes based on preload data or default: [ [Object] ], { themeKey: 'themeA' } 1`] = `
102+
exports[`ThemeProvider should load themes based on default selected theme key. customThemes: [
103+
{ themeKey: 'themeA' },
104+
{ themeKey: 'mockDefaultSelectedThemeKey' },
105+
[length]: 2
106+
] 1`] = `
95107
<body>
96108
<div>
97109
<style
@@ -105,7 +117,7 @@ exports[`ThemeProvider should load themes based on preload data or default: [ [O
105117
./theme-dark-components.css?raw
106118
</style>
107119
<style
108-
data-theme-key="themeA"
120+
data-theme-key="mockDefaultSelectedThemeKey"
109121
/>
110122
<div
111123
class="spectrum-theme-provider JuTe6q_spectrum _5QszkG_spectrum _5QszkG_i18nFontFamily PFjRbG_spectrum--light theme-spectrum-palette theme-spectrum-alias HAZavG_spectrum--large zA6MfG_spectrum zA6MfG_spectrum--dark zA6MfG_spectrum--darkest zA6MfG_spectrum--large zA6MfG_spectrum--light zA6MfG_spectrum--lightest zA6MfG_spectrum--medium"
@@ -121,51 +133,7 @@ exports[`ThemeProvider should load themes based on preload data or default: [ [O
121133
</body>
122134
`;
123135

124-
exports[`ThemeProvider should load themes based on preload data or default: [ [Object] ], null 1`] = `
125-
<body>
126-
<div>
127-
<style
128-
data-theme-key="default-dark"
129-
>
130-
./theme-dark-palette.css?raw
131-
./theme-dark-semantic.css?raw
132-
./theme-dark-semantic-chart.css?raw
133-
./theme-dark-semantic-editor.css?raw
134-
./theme-dark-semantic-grid.css?raw
135-
./theme-dark-components.css?raw
136-
</style>
137-
<div
138-
class="spectrum-theme-provider JuTe6q_spectrum _5QszkG_spectrum _5QszkG_i18nFontFamily PFjRbG_spectrum--light theme-spectrum-palette theme-spectrum-alias HAZavG_spectrum--large zA6MfG_spectrum zA6MfG_spectrum--dark zA6MfG_spectrum--darkest zA6MfG_spectrum--large zA6MfG_spectrum--light zA6MfG_spectrum--lightest zA6MfG_spectrum--medium"
139-
dir="ltr"
140-
lang="en-US"
141-
style="isolation: isolate; color-scheme: light;"
142-
>
143-
<div>
144-
Child
145-
</div>
146-
</div>
147-
</div>
148-
</body>
149-
`;
150-
151-
exports[`ThemeProvider should load themes based on preload data or default: null, { themeKey: 'themeA' } 1`] = `
152-
<body>
153-
<div>
154-
<div
155-
class="spectrum-theme-provider JuTe6q_spectrum _5QszkG_spectrum _5QszkG_i18nFontFamily PFjRbG_spectrum--light theme-spectrum-palette theme-spectrum-alias HAZavG_spectrum--large zA6MfG_spectrum zA6MfG_spectrum--dark zA6MfG_spectrum--darkest zA6MfG_spectrum--large zA6MfG_spectrum--light zA6MfG_spectrum--lightest zA6MfG_spectrum--medium"
156-
dir="ltr"
157-
lang="en-US"
158-
style="isolation: isolate; color-scheme: light;"
159-
>
160-
<div>
161-
Child
162-
</div>
163-
</div>
164-
</div>
165-
</body>
166-
`;
167-
168-
exports[`ThemeProvider should load themes based on preload data or default: null, null 1`] = `
136+
exports[`ThemeProvider should load themes based on default selected theme key. customThemes: null 1`] = `
169137
<body>
170138
<div>
171139
<div

0 commit comments

Comments
 (0)