Skip to content

Commit

Permalink
feat: Set selected theme via query string param (#2204)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
bmingles committed Aug 30, 2024
1 parent 0a924cd commit 89ede66
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 92 deletions.
1 change: 1 addition & 0 deletions packages/components/src/theme/ThemeModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export type ThemeIconsRequiringManualColorChanges =

export const DEFAULT_DARK_THEME_KEY = 'default-dark' satisfies BaseThemeKey;
export const DEFAULT_LIGHT_THEME_KEY = 'default-light' satisfies BaseThemeKey;
export const THEME_KEY_OVERRIDE_QUERY_PARAM = 'theme';

// Hex versions of some of the default dark theme color palette needed for
// preload defaults.
Expand Down
60 changes: 24 additions & 36 deletions packages/components/src/theme/ThemeProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,12 @@ import React from 'react';
import { act, render } from '@testing-library/react';
import { assertNotNull, TestUtils } from '@deephaven/utils';
import { ThemeContextValue, ThemeProvider } from './ThemeProvider';
import {
DEFAULT_DARK_THEME_KEY,
DEFAULT_LIGHT_THEME_KEY,
ThemeData,
ThemePreloadData,
} from './ThemeModel';
import { DEFAULT_LIGHT_THEME_KEY, ThemeData } from './ThemeModel';
import {
calculatePreloadStyleContent,
getActiveThemes,
getDefaultBaseThemes,
getThemePreloadData,
getDefaultSelectedThemeKey,
setThemePreloadData,
} from './ThemeUtils';
import { useTheme } from './useTheme';
Expand All @@ -24,13 +19,17 @@ jest.mock('./ThemeUtils', () => {
return {
...actual,
calculatePreloadStyleContent: jest.fn(),
getThemePreloadData: jest.fn(actual.getThemePreloadData),
getDefaultSelectedThemeKey: jest.fn(),
getThemeKeyOverride: jest.fn(),
setThemePreloadData: jest.fn(),
};
});

const customThemes = [{ themeKey: 'themeA' }] as [ThemeData];
const preloadA: ThemePreloadData = { themeKey: 'themeA' };
const customThemes = [
{ themeKey: 'themeA' },
{ themeKey: 'mockDefaultSelectedThemeKey' },
] as ThemeData[];
const defaultSelectedThemeKey = 'mockDefaultSelectedThemeKey';

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

asMock(getThemePreloadData).mockName('getThemePreloadData');
asMock(getDefaultSelectedThemeKey)
.mockName('getDefaultSelectedThemeKey')
.mockReturnValue(defaultSelectedThemeKey);

asMock(setThemePreloadData).mockName('setThemePreloadData');
});

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

it.each([
[null, null],
[null, preloadA],
[customThemes, null],
[customThemes, preloadA],
] as const)(
'should load themes based on preload data or default: %s, %s',
(themes, preloadData) => {
asMock(getThemePreloadData).mockReturnValue(preloadData);

it.each([null, customThemes])(
'should load themes based on default selected theme key. customThemes: %o',
themes => {
const component = render(
<ThemeProvider themes={themes}>
<MockChild />
Expand All @@ -79,31 +74,24 @@ describe('ThemeProvider', () => {
expect(themeContextValueRef.current.activeThemes).toBeNull();
} else {
expect(themeContextValueRef.current.activeThemes).toEqual(
getActiveThemes(preloadData?.themeKey ?? DEFAULT_DARK_THEME_KEY, {
getActiveThemes(defaultSelectedThemeKey, {
base: getDefaultBaseThemes(),
custom: themes,
})
);

expect(themeContextValueRef.current.selectedThemeKey).toEqual(
preloadData?.themeKey ?? DEFAULT_DARK_THEME_KEY
defaultSelectedThemeKey
);
}

expect(component.baseElement).toMatchSnapshot();
}
);

it.each([
[null, null],
[null, preloadA],
[customThemes, null],
[customThemes, preloadA],
] as const)(
'should set preload data when active themes change: %s, %s',
(themes, preloadData) => {
asMock(getThemePreloadData).mockReturnValue(preloadData);

it.each([null, customThemes] as const)(
'should set preload data when active themes change: %o',
themes => {
render(
<ThemeProvider themes={themes}>
<MockChild />
Expand All @@ -114,14 +102,14 @@ describe('ThemeProvider', () => {
expect(setThemePreloadData).not.toHaveBeenCalled();
} else {
expect(setThemePreloadData).toHaveBeenCalledWith({
themeKey: preloadData?.themeKey ?? DEFAULT_DARK_THEME_KEY,
preloadStyleContent: calculatePreloadStyleContent(),
themeKey: defaultSelectedThemeKey,
preloadStyleContent: calculatePreloadStyleContent({}),
});
}
}
);

describe.each([null, customThemes])('setSelectedThemeKey: %s', themes => {
describe.each([null, customThemes])('setSelectedThemeKey: %o', themes => {
it.each([DEFAULT_LIGHT_THEME_KEY, customThemes[0].themeKey])(
'should change selected theme: %s',
themeKey => {
Expand Down
10 changes: 3 additions & 7 deletions packages/components/src/theme/ThemeProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import { createContext, ReactNode, useEffect, useMemo, useState } from 'react';
import Log from '@deephaven/log';
import {
DEFAULT_DARK_THEME_KEY,
DEFAULT_PRELOAD_DATA_VARIABLES,
ThemeData,
} from './ThemeModel';
import { DEFAULT_PRELOAD_DATA_VARIABLES, ThemeData } from './ThemeModel';
import {
calculatePreloadStyleContent,
getActiveThemes,
getDefaultBaseThemes,
getThemePreloadData,
setThemePreloadData,
overrideSVGFillColors,
getDefaultSelectedThemeKey,
} from './ThemeUtils';
import { SpectrumThemeProvider } from './SpectrumThemeProvider';
import './theme-svg.scss';
Expand Down Expand Up @@ -48,7 +44,7 @@ export function ThemeProvider({
const [value, setValue] = useState<ThemeContextValue | null>(null);

const [selectedThemeKey, setSelectedThemeKey] = useState<string>(
() => getThemePreloadData()?.themeKey ?? DEFAULT_DARK_THEME_KEY
getDefaultSelectedThemeKey
);

// Calculate active themes once a non-null themes array is provided.
Expand Down
40 changes: 40 additions & 0 deletions packages/components/src/theme/ThemeUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import {
ThemePreloadColorVariable,
ThemeRegistrationData,
THEME_CACHE_LOCAL_STORAGE_KEY,
THEME_KEY_OVERRIDE_QUERY_PARAM,
} from './ThemeModel';
import {
calculatePreloadStyleContent,
createCssVariableResolver,
extractDistinctCssVariableExpressions,
getActiveThemes,
getDefaultBaseThemes,
getDefaultSelectedThemeKey,
getExpressionRanges,
getThemeKey,
getThemePreloadData,
Expand Down Expand Up @@ -239,6 +241,44 @@ describe('getActiveThemes', () => {
});
});

describe('getDefaultSelectedThemeKey', () => {
const origLocation = window.location;

beforeEach(() => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
delete window.location;
window.location = {
search: '',
} as unknown as Location;
});

afterEach(() => {
window.location = origLocation;
});

it.each([
['overrideKey', 'preloadKey', 'overrideKey'],
[undefined, 'preloadKey', 'preloadKey'],
[undefined, undefined, DEFAULT_DARK_THEME_KEY],
])(
'should coalesce overide key -> preload key -> default key: %s, %s, %s',
(overrideKey, preloadKey, expected) => {
if (overrideKey != null) {
window.location.search = `?${THEME_KEY_OVERRIDE_QUERY_PARAM}=${overrideKey}`;
}

localStorage.setItem(
THEME_CACHE_LOCAL_STORAGE_KEY,
JSON.stringify({ themeKey: preloadKey })
);

const actual = getDefaultSelectedThemeKey();
expect(actual).toEqual(expected);
}
);
});

describe('getExpressionRanges', () => {
const testCases = [
['Single expression', '#ffffff', [[0, 6]]],
Expand Down
26 changes: 26 additions & 0 deletions packages/components/src/theme/ThemeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
SVG_ICON_MANUAL_COLOR_MAP,
ThemeCssVariableName,
ThemeIconsRequiringManualColorChanges,
THEME_KEY_OVERRIDE_QUERY_PARAM,
} from './ThemeModel';

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

/**
* Get the default selected theme key. Precedence is:
* 1. Theme key override query parameter
* 2. Theme key from preload data
* 3. Default dark theme key
* @returns The default selected theme key
*/
export function getDefaultSelectedThemeKey(): string {
return (
getThemeKeyOverride() ??
getThemePreloadData()?.themeKey ??
DEFAULT_DARK_THEME_KEY
);
}

/**
* A theme key override can be set via a query parameter to force a specific
* theme selection. Useful for embedded widget scenarios that don't expose the
* theme selector.
*/
export function getThemeKeyOverride(): string | null {
const searchParams = new URLSearchParams(window.location.search);
return searchParams.get(THEME_KEY_OVERRIDE_QUERY_PARAM);
}

/**
* Get the preload data from local storage or null if it does not exist or is
* invalid
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`ThemeProvider setSelectedThemeKey: [ [Object] ] should change selected theme: default-light 1`] = `
exports[`ThemeProvider setSelectedThemeKey: [
{ themeKey: 'themeA' },
{ themeKey: 'mockDefaultSelectedThemeKey' },
[length]: 2
] should change selected theme: default-light 1`] = `
<body>
<div>
<style
Expand All @@ -27,7 +31,11 @@ exports[`ThemeProvider setSelectedThemeKey: [ [Object] ] should change selected
</body>
`;

exports[`ThemeProvider setSelectedThemeKey: [ [Object] ] should change selected theme: themeA 1`] = `
exports[`ThemeProvider setSelectedThemeKey: [
{ themeKey: 'themeA' },
{ themeKey: 'mockDefaultSelectedThemeKey' },
[length]: 2
] should change selected theme: themeA 1`] = `
<body>
<div>
<style
Expand Down Expand Up @@ -91,7 +99,11 @@ exports[`ThemeProvider setSelectedThemeKey: null should change selected theme: t
</body>
`;

exports[`ThemeProvider should load themes based on preload data or default: [ [Object] ], { themeKey: 'themeA' } 1`] = `
exports[`ThemeProvider should load themes based on default selected theme key. customThemes: [
{ themeKey: 'themeA' },
{ themeKey: 'mockDefaultSelectedThemeKey' },
[length]: 2
] 1`] = `
<body>
<div>
<style
Expand All @@ -105,7 +117,7 @@ exports[`ThemeProvider should load themes based on preload data or default: [ [O
./theme-dark-components.css?raw
</style>
<style
data-theme-key="themeA"
data-theme-key="mockDefaultSelectedThemeKey"
/>
<div
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"
Expand All @@ -121,51 +133,7 @@ exports[`ThemeProvider should load themes based on preload data or default: [ [O
</body>
`;

exports[`ThemeProvider should load themes based on preload data or default: [ [Object] ], null 1`] = `
<body>
<div>
<style
data-theme-key="default-dark"
>
./theme-dark-palette.css?raw
./theme-dark-semantic.css?raw
./theme-dark-semantic-chart.css?raw
./theme-dark-semantic-editor.css?raw
./theme-dark-semantic-grid.css?raw
./theme-dark-components.css?raw
</style>
<div
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"
dir="ltr"
lang="en-US"
style="isolation: isolate; color-scheme: light;"
>
<div>
Child
</div>
</div>
</div>
</body>
`;

exports[`ThemeProvider should load themes based on preload data or default: null, { themeKey: 'themeA' } 1`] = `
<body>
<div>
<div
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"
dir="ltr"
lang="en-US"
style="isolation: isolate; color-scheme: light;"
>
<div>
Child
</div>
</div>
</div>
</body>
`;

exports[`ThemeProvider should load themes based on preload data or default: null, null 1`] = `
exports[`ThemeProvider should load themes based on default selected theme key. customThemes: null 1`] = `
<body>
<div>
<div
Expand Down

0 comments on commit 89ede66

Please sign in to comment.