Skip to content

Commit aa5adb0

Browse files
sadpandajoeclaude
andauthored
fix(embedded): default to light theme instead of system preference (#38644)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent dcb414a commit aa5adb0

4 files changed

Lines changed: 126 additions & 1 deletion

File tree

superset-frontend/packages/superset-core/src/theme/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,7 @@ export interface ThemeControllerOptions {
426426
canUpdateTheme?: () => boolean;
427427
canUpdateMode?: () => boolean;
428428
isGlobalContext?: boolean;
429+
initialMode?: ThemeMode;
429430
}
430431

431432
export interface ThemeContextType {

superset-frontend/src/embedded/EmbeddedContextProviders.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { DynamicPluginProvider } from 'src/components';
2626
import { EmbeddedUiConfigProvider } from 'src/components/UiConfigContext';
2727
import { SupersetThemeProvider } from 'src/theme/ThemeProvider';
2828
import { ThemeController } from 'src/theme/ThemeController';
29-
import { type ThemeStorage } from '@apache-superset/core/theme';
29+
import { type ThemeStorage, ThemeMode } from '@apache-superset/core/theme';
3030
import { store } from 'src/views/store';
3131
import querystring from 'query-string';
3232

@@ -52,6 +52,7 @@ class ThemeMemoryStorageAdapter implements ThemeStorage {
5252

5353
const themeController = new ThemeController({
5454
storage: new ThemeMemoryStorageAdapter(),
55+
initialMode: ThemeMode.DEFAULT,
5556
});
5657

5758
export const getThemeController = (): ThemeController => themeController;

superset-frontend/src/theme/ThemeController.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,15 +102,19 @@ export class ThemeController {
102102
// Track loaded font URLs to avoid duplicate injections
103103
private loadedFontUrls: Set<string> = new Set();
104104

105+
private initialMode: ThemeMode | undefined;
106+
105107
constructor({
106108
storage = new LocalStorageAdapter(),
107109
modeStorageKey = STORAGE_KEYS.THEME_MODE,
108110
themeObject = supersetThemeObject,
109111
defaultTheme = (supersetThemeObject.theme as AnyThemeConfig) ?? {},
110112
onChange = undefined,
113+
initialMode = undefined,
111114
}: ThemeControllerOptions = {}) {
112115
this.storage = storage;
113116
this.modeStorageKey = modeStorageKey;
117+
this.initialMode = initialMode;
114118

115119
// Controller creates and owns the global theme
116120
this.globalTheme = themeObject;
@@ -743,6 +747,13 @@ export class ThemeController {
743747
return ThemeMode.DEFAULT;
744748
}
745749

750+
// Use explicit initial mode if provided (e.g. embedded dashboards default to light)
751+
if (
752+
this.initialMode !== undefined &&
753+
this.isValidThemeMode(this.initialMode)
754+
)
755+
return this.initialMode;
756+
746757
// Default to system preference when both themes are available
747758
return ThemeMode.SYSTEM;
748759
}

superset-frontend/src/theme/tests/ThemeController.test.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1686,3 +1686,115 @@ test('font loading: adds new font URLs when switching themes', () => {
16861686
.querySelectorAll('style[data-superset-fonts]')
16871687
.forEach(el => el.remove());
16881688
});
1689+
1690+
test('ThemeController uses initialMode when provided and no saved mode exists', () => {
1691+
mockGetBootstrapData.mockReturnValue(
1692+
createMockBootstrapData({
1693+
default: DEFAULT_THEME,
1694+
dark: DARK_THEME,
1695+
}),
1696+
);
1697+
1698+
const controller = createController({ initialMode: ThemeMode.DEFAULT });
1699+
1700+
expect(controller.getCurrentMode()).toBe(ThemeMode.DEFAULT);
1701+
});
1702+
1703+
test('ThemeController defaults to SYSTEM when initialMode is not provided', () => {
1704+
mockGetBootstrapData.mockReturnValue(
1705+
createMockBootstrapData({
1706+
default: DEFAULT_THEME,
1707+
dark: DARK_THEME,
1708+
}),
1709+
);
1710+
1711+
const controller = createController();
1712+
1713+
expect(controller.getCurrentMode()).toBe(ThemeMode.SYSTEM);
1714+
});
1715+
1716+
test('ThemeController saved mode takes precedence over initialMode', () => {
1717+
mockGetBootstrapData.mockReturnValue(
1718+
createMockBootstrapData({
1719+
default: DEFAULT_THEME,
1720+
dark: DARK_THEME,
1721+
}),
1722+
);
1723+
1724+
mockLocalStorage.getItem.mockReturnValue(ThemeMode.DARK);
1725+
1726+
const controller = createController({ initialMode: ThemeMode.DEFAULT });
1727+
1728+
expect(controller.getCurrentMode()).toBe(ThemeMode.DARK);
1729+
});
1730+
1731+
test('ThemeController with initialMode DEFAULT applies light theme even when system prefers dark', () => {
1732+
mockGetBootstrapData.mockReturnValue(
1733+
createMockBootstrapData({
1734+
default: DEFAULT_THEME,
1735+
dark: DARK_THEME,
1736+
}),
1737+
);
1738+
1739+
mockMatchMedia.mockReturnValue({
1740+
matches: true, // system prefers dark
1741+
addEventListener: jest.fn(),
1742+
removeEventListener: jest.fn(),
1743+
});
1744+
1745+
const controller = createController({ initialMode: ThemeMode.DEFAULT });
1746+
1747+
expect(controller.getCurrentMode()).toBe(ThemeMode.DEFAULT);
1748+
const lastCall =
1749+
mockSetConfig.mock.calls[mockSetConfig.mock.calls.length - 1][0];
1750+
expect(lastCall.token.colorBgBase).toBe(DEFAULT_THEME.token!.colorBgBase);
1751+
});
1752+
1753+
test('ThemeController with initialMode still allows setThemeMode after init', () => {
1754+
mockGetBootstrapData.mockReturnValue(
1755+
createMockBootstrapData({
1756+
default: DEFAULT_THEME,
1757+
dark: DARK_THEME,
1758+
}),
1759+
);
1760+
1761+
const controller = createController({ initialMode: ThemeMode.DEFAULT });
1762+
expect(controller.getCurrentMode()).toBe(ThemeMode.DEFAULT);
1763+
1764+
controller.setThemeMode(ThemeMode.DARK);
1765+
expect(controller.getCurrentMode()).toBe(ThemeMode.DARK);
1766+
1767+
controller.setThemeMode(ThemeMode.SYSTEM);
1768+
expect(controller.getCurrentMode()).toBe(ThemeMode.SYSTEM);
1769+
});
1770+
1771+
test('ThemeController initialMode is ignored when no dark theme exists', () => {
1772+
mockGetBootstrapData.mockReturnValue(
1773+
createMockBootstrapData({
1774+
default: DEFAULT_THEME,
1775+
dark: {},
1776+
}),
1777+
);
1778+
1779+
const controller = createController({ initialMode: ThemeMode.SYSTEM });
1780+
1781+
// Should still be DEFAULT because there's no dark theme available
1782+
expect(controller.getCurrentMode()).toBe(ThemeMode.DEFAULT);
1783+
});
1784+
1785+
test('ThemeController invalid initialMode falls back to SYSTEM', () => {
1786+
mockGetBootstrapData.mockReturnValue(
1787+
createMockBootstrapData({
1788+
default: DEFAULT_THEME,
1789+
dark: DARK_THEME,
1790+
}),
1791+
);
1792+
1793+
const controller = createController({
1794+
initialMode: 'invalid' as ThemeMode,
1795+
});
1796+
1797+
// Invalid initialMode should be rejected by isValidThemeMode,
1798+
// falling through to the default SYSTEM mode
1799+
expect(controller.getCurrentMode()).toBe(ThemeMode.SYSTEM);
1800+
});

0 commit comments

Comments
 (0)