You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ms...@apache.org on 2026/05/06 15:18:02 UTC
(superset) branch master updated: feat(themes): expose active theme mode via data-theme-mode attribute (#39063)
This is an automated email from the ASF dual-hosted git repository.
msyavuz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new e667ceb6cfa feat(themes): expose active theme mode via data-theme-mode attribute (#39063)
e667ceb6cfa is described below
commit e667ceb6cfadde071a02d8882817da2f004238b8
Author: Sebastian Mohr <39...@users.noreply.github.com>
AuthorDate: Wed May 6 17:17:54 2026 +0200
feat(themes): expose active theme mode via data-theme-mode attribute (#39063)
---
superset-frontend/src/theme/ThemeController.ts | 14 ++++++
superset-frontend/src/theme/ThemeProvider.tsx | 14 ++++--
.../src/theme/tests/ThemeController.test.ts | 35 ++++++++++++++
.../src/theme/tests/ThemeProvider.test.tsx | 56 ++++++++++++++++++++++
4 files changed, 116 insertions(+), 3 deletions(-)
diff --git a/superset-frontend/src/theme/ThemeController.ts b/superset-frontend/src/theme/ThemeController.ts
index 1f420cf75d4..28d9939b5be 100644
--- a/superset-frontend/src/theme/ThemeController.ts
+++ b/superset-frontend/src/theme/ThemeController.ts
@@ -307,6 +307,20 @@ export class ThemeController {
return this.currentMode;
}
+ /**
+ * Returns the resolved theme mode as 'dark' or 'light'.
+ * Takes into account SYSTEM mode and returns the actual resolved preference.
+ */
+ public getCurrentModeResolved(): 'dark' | 'light' {
+ const activeTheme = this.getThemeForMode(this.currentMode);
+ if (activeTheme) {
+ const normalizedTheme = this.normalizeTheme(activeTheme);
+ return isThemeConfigDark(normalizedTheme) ? 'dark' : 'light';
+ }
+
+ return this.currentMode === ThemeMode.DARK ? 'dark' : 'light';
+ }
+
/**
* Sets new theme.
* @param theme - The new theme to apply
diff --git a/superset-frontend/src/theme/ThemeProvider.tsx b/superset-frontend/src/theme/ThemeProvider.tsx
index ab1beb80289..ef979ff39bb 100644
--- a/superset-frontend/src/theme/ThemeProvider.tsx
+++ b/superset-frontend/src/theme/ThemeProvider.tsx
@@ -53,11 +53,19 @@ export function SupersetThemeProvider({
);
useEffect(() => {
- const unsubscribe = themeController.onChange(theme => {
+ // TODO: Once we migrate to react>=18 is should be possible
+ // to replace the useState and useEffect with a singular
+ // useSyncExternalStore, simplifying quite a bit
+ const updateState = (theme: Theme) => {
setCurrentTheme(theme);
setCurrentThemeMode(themeController.getCurrentMode());
- });
-
+ document.documentElement.setAttribute(
+ 'data-theme-mode',
+ themeController.getCurrentModeResolved(),
+ );
+ };
+ const unsubscribe = themeController.onChange(updateState);
+ updateState(themeController.getTheme());
return unsubscribe;
}, [themeController]);
diff --git a/superset-frontend/src/theme/tests/ThemeController.test.ts b/superset-frontend/src/theme/tests/ThemeController.test.ts
index e905ed5e7c3..b427107772d 100644
--- a/superset-frontend/src/theme/tests/ThemeController.test.ts
+++ b/superset-frontend/src/theme/tests/ThemeController.test.ts
@@ -1798,3 +1798,38 @@ test('ThemeController invalid initialMode falls back to SYSTEM', () => {
// falling through to the default SYSTEM mode
expect(controller.getCurrentMode()).toBe(ThemeMode.SYSTEM);
});
+
+test('getCurrentModeResolved returns light for light theme', () => {
+ mockGetBootstrapData.mockReturnValue(
+ createMockBootstrapData({
+ default: { token: { colorBgBase: '#ffffff' } },
+ dark: {
+ token: { colorBgBase: '#000000' },
+ algorithm: ThemeAlgorithm.DARK,
+ },
+ }),
+ );
+
+ const controller = createController();
+ expect(controller.getCurrentModeResolved()).toBe('light');
+ controller.setThemeMode(ThemeMode.DARK);
+ expect(controller.getCurrentModeResolved()).toBe('dark');
+});
+
+test('getResolvedThemeMode returns dark when default theme is dark but mode is DEFAULT', () => {
+ // Setup: default theme is dark (has dark algorithm)
+ // This simulates single-theme deployments where THEME_DARK=None but default is dark
+ mockGetBootstrapData.mockReturnValue(
+ createMockBootstrapData({
+ default: {
+ token: { colorBgBase: '#000000' }, // dark background
+ algorithm: antdThemeImport.darkAlgorithm,
+ },
+ dark: {}, // empty - no separate dark theme
+ }),
+ );
+
+ const controller = createController();
+ expect(controller.getCurrentMode()).toBe(ThemeMode.DEFAULT);
+ expect(controller.getCurrentModeResolved()).toBe('dark');
+});
diff --git a/superset-frontend/src/theme/tests/ThemeProvider.test.tsx b/superset-frontend/src/theme/tests/ThemeProvider.test.tsx
index 85e50cfcf1a..a15cd5289f9 100644
--- a/superset-frontend/src/theme/tests/ThemeProvider.test.tsx
+++ b/superset-frontend/src/theme/tests/ThemeProvider.test.tsx
@@ -75,6 +75,7 @@ describe('SupersetThemeProvider', () => {
mockThemeController = {
getTheme: jest.fn().mockReturnValue(mockTheme),
getCurrentMode: jest.fn().mockReturnValue(ThemeMode.DEFAULT),
+ getCurrentModeResolved: jest.fn().mockReturnValue('dark'),
setTheme: jest.fn(),
setThemeMode: jest.fn(),
resetTheme: jest.fn(),
@@ -276,4 +277,59 @@ describe('SupersetThemeProvider', () => {
);
});
});
+
+ afterEach(() => {
+ document.documentElement.removeAttribute('data-theme-mode');
+ });
+
+ test('should set data-theme-mode="light" on mount when resolved mode is light', () => {
+ mockThemeController.getCurrentModeResolved.mockReturnValue('light');
+
+ render(
+ <SupersetThemeProvider themeController={mockThemeController}>
+ <div>Content</div>
+ </SupersetThemeProvider>,
+ );
+
+ expect(document.documentElement.getAttribute('data-theme-mode')).toBe(
+ 'light',
+ );
+ });
+
+ test('should set data-theme-mode="dark" on mount when resolved mode is dark', () => {
+ mockThemeController.getCurrentModeResolved.mockReturnValue('dark');
+
+ render(
+ <SupersetThemeProvider themeController={mockThemeController}>
+ <div>Content</div>
+ </SupersetThemeProvider>,
+ );
+
+ expect(document.documentElement.getAttribute('data-theme-mode')).toBe(
+ 'dark',
+ );
+ });
+
+ test('should update data-theme-mode when theme changes', () => {
+ mockThemeController.getCurrentModeResolved.mockReturnValue('light');
+
+ render(
+ <SupersetThemeProvider themeController={mockThemeController}>
+ <div>Content</div>
+ </SupersetThemeProvider>,
+ );
+
+ expect(document.documentElement.getAttribute('data-theme-mode')).toBe(
+ 'light',
+ );
+
+ act(() => {
+ mockThemeController.getCurrentModeResolved.mockReturnValue('dark');
+ mockOnChangeCallback(mockDarkTheme);
+ });
+
+ expect(document.documentElement.getAttribute('data-theme-mode')).toBe(
+ 'dark',
+ );
+ });
});