Skip to content

Commit 275bf36

Browse files
committed
feat: optimize theme changing
1 parent 5edaab0 commit 275bf36

File tree

3 files changed

+79
-32
lines changed

3 files changed

+79
-32
lines changed

electron/renderer/context/theme.tsx

+3-7
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,7 @@
44
import type { EuiThemeColorMode } from '@elastic/eui';
55
import type { ReactNode } from 'react';
66
import { createContext, useEffect, useState } from 'react';
7-
import {
8-
enableTheme,
9-
getDefaultThemeName,
10-
getThemeName,
11-
} from '../lib/theme.js';
7+
import { enableTheme, getThemeName } from '../lib/theme.js';
128

139
/**
1410
* React context for storing theme-related data and callbacks.
@@ -35,9 +31,9 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = (
3531
) => {
3632
const { children } = props;
3733

38-
const [colorMode, setColorMode] = useState(getDefaultThemeName());
34+
const [colorMode, setColorMode] = useState(getThemeName());
3935

40-
// On initial mount in the browser, use any theme from local storage.
36+
// On initial mount in the browser, load user's theme preference.
4137
useEffect(() => {
4238
setColorMode(getThemeName());
4339
}, []);

electron/renderer/lib/theme.ts

+74-23
Original file line numberDiff line numberDiff line change
@@ -6,47 +6,98 @@ import type { Maybe } from '../../common/types.js';
66
import { LocalStorage } from './local-storage.js';
77

88
/**
9-
* The functions here are for tracking and setting the current theme.
10-
* localStorage is used to store the currently preferred them, though
11-
* that doesn't work on the server, where we just use a default.
9+
* Find all the theme links on the page that we could switch between.
10+
* See `_document.tsx` for how these are added to the page.
1211
*/
12+
const getAllThemeLinks = (): Array<HTMLLinkElement> => {
13+
return [
14+
...document.querySelectorAll<HTMLLinkElement>(
15+
'link[data-name="eui-theme"]'
16+
),
17+
];
18+
};
1319

14-
const getAllThemes = (): Array<HTMLLinkElement> => {
15-
// @ts-ignore
16-
return [...document.querySelectorAll('link[data-name="eui-theme"]')];
20+
/**
21+
* Find the theme link on the page that matches the given theme name.
22+
*/
23+
const getThemeLink = (themeName: EuiThemeColorMode): Maybe<HTMLLinkElement> => {
24+
return getAllThemeLinks().find((themeLink) => {
25+
return themeLink.dataset.theme === themeName;
26+
});
1727
};
1828

29+
/**
30+
* Sets the user's theme preference and actively updates the UI to match.
31+
* To only set the preference without updating the UI, use `setThemeName`.
32+
*/
1933
export const enableTheme = (newThemeName: EuiThemeColorMode): void => {
2034
const oldThemeName = getThemeName();
21-
LocalStorage.set<EuiThemeColorMode>('theme', newThemeName);
35+
setThemeName(newThemeName);
2236

23-
for (const themeLink of getAllThemes()) {
24-
// Disable all theme links, except for the desired theme, which we enable
25-
themeLink.disabled = themeLink.dataset.theme !== newThemeName;
26-
themeLink.ariaDisabled = String(themeLink.dataset.theme !== newThemeName);
27-
}
37+
const newThemeLink = getThemeLink(newThemeName);
2838

29-
// Add a class to the `body` element that indicates which theme we're using.
30-
// This allows any custom styling to adapt to the current theme.
31-
if (document.body.classList.contains(`appTheme-${oldThemeName}`)) {
32-
document.body.classList.replace(
33-
`appTheme-${oldThemeName}`,
34-
`appTheme-${newThemeName}`
35-
);
36-
} else {
37-
document.body.classList.add(`appTheme-${newThemeName}`);
39+
if (!newThemeLink) {
40+
return;
3841
}
42+
43+
// When toggling the theme, to prevent a flash of unstyled content
44+
// then preload the new theme's CSS before enabling it.
45+
const preloadThemeLink = document.createElement('link');
46+
preloadThemeLink.rel = 'preload';
47+
preloadThemeLink.as = 'style';
48+
preloadThemeLink.href = newThemeLink.href;
49+
document.head.appendChild(preloadThemeLink);
50+
51+
// Once the new theme is loaded then we can disable the old theme.
52+
// Because the new theme is preloaded, the change should be instant.
53+
preloadThemeLink.onload = () => {
54+
for (const themeLink of getAllThemeLinks()) {
55+
// Disable all theme links, except for the desired theme, which we enable
56+
themeLink.disabled = themeLink.dataset.theme !== newThemeName;
57+
themeLink.ariaDisabled = String(themeLink.dataset.theme !== newThemeName);
58+
}
59+
60+
// Add a class to the `body` element that indicates which theme we're using.
61+
// This allows any custom styling to adapt to the current theme.
62+
if (document.body.classList.contains(`appTheme-${oldThemeName}`)) {
63+
document.body.classList.replace(
64+
`appTheme-${oldThemeName}`,
65+
`appTheme-${newThemeName}`
66+
);
67+
} else {
68+
document.body.classList.add(`appTheme-${newThemeName}`);
69+
}
70+
71+
// Remove the preload link element.
72+
document.head.removeChild(preloadThemeLink);
73+
};
3974
};
4075

76+
/**
77+
* Gets the user's theme preference.
78+
*/
4179
export const getThemeName = (): EuiThemeColorMode => {
4280
return getStoredThemeName() || getDefaultThemeName();
4381
};
4482

45-
export const getStoredThemeName = (): Maybe<EuiThemeColorMode> => {
83+
/**
84+
* Sets the user's theme preference.
85+
* Does not actively change the UI look and feel.
86+
* To do that, use `enableTheme` or reload the app.
87+
*/
88+
export const setThemeName = (themeName: EuiThemeColorMode): void => {
89+
setStoredThemeName(themeName);
90+
};
91+
92+
const getStoredThemeName = (): Maybe<EuiThemeColorMode> => {
4693
return LocalStorage.get<EuiThemeColorMode>('theme');
4794
};
4895

49-
export const getDefaultThemeName = (): EuiThemeColorMode => {
96+
const setStoredThemeName = (themeName: EuiThemeColorMode): void => {
97+
LocalStorage.set<EuiThemeColorMode>('theme', themeName);
98+
};
99+
100+
const getDefaultThemeName = (): EuiThemeColorMode => {
50101
return 'dark';
51102
};
52103

electron/renderer/pages/_document.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import { useMemo } from 'react';
66
import type { LinkHTMLAttributes, ReactElement } from 'react';
77
import type React from 'react';
88
import type { Theme } from '../lib/theme.js';
9-
import { getDefaultThemeName, themeConfig } from '../lib/theme.js';
9+
import { getThemeName, themeConfig } from '../lib/theme.js';
1010

1111
function createThemeLink(theme: Theme): ReactElement {
1212
let disabledProps = {};
1313

14-
if (theme.id !== getDefaultThemeName()) {
14+
if (theme.id !== getThemeName()) {
1515
disabledProps = {
1616
'disabled': true,
1717
'aria-disabled': true,

0 commit comments

Comments
 (0)