Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Set selected theme via query string param #2204

Merged
merged 7 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading