diff --git a/.changeset/bright-themes-guess.md b/.changeset/bright-themes-guess.md new file mode 100644 index 00000000..6e65dc3e --- /dev/null +++ b/.changeset/bright-themes-guess.md @@ -0,0 +1,5 @@ +--- +'@nodejs/doc-kit-generator-react': patch +--- + +Show the active light or dark theme icon when the system preference is selected. diff --git a/packages/react/src/html/ui/components/ThemeToggle.jsx b/packages/react/src/html/ui/components/ThemeToggle.jsx index 2e973755..e4e91e10 100644 --- a/packages/react/src/html/ui/components/ThemeToggle.jsx +++ b/packages/react/src/html/ui/components/ThemeToggle.jsx @@ -7,10 +7,10 @@ import withIsland from '../islands/withIsland.jsx'; * Theme switcher. */ const Toggle = () => { - const [themePreference, setThemePreference] = useTheme(); + const [currentTheme, setThemePreference] = useTheme(); return ( - + ); }; diff --git a/packages/react/src/html/ui/hooks/__tests__/theme.test.mjs b/packages/react/src/html/ui/hooks/__tests__/theme.test.mjs new file mode 100644 index 00000000..2ffb22f3 --- /dev/null +++ b/packages/react/src/html/ui/hooks/__tests__/theme.test.mjs @@ -0,0 +1,16 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import { getDisplayedTheme } from '../theme.mjs'; + +describe('getDisplayedTheme', () => { + it('uses the resolved system scheme for the initial system preference', () => { + assert.strictEqual(getDisplayedTheme('system', true), 'dark'); + assert.strictEqual(getDisplayedTheme('system', false), 'light'); + }); + + it('preserves explicit user preferences', () => { + assert.strictEqual(getDisplayedTheme('dark', false), 'dark'); + assert.strictEqual(getDisplayedTheme('light', true), 'light'); + }); +}); diff --git a/packages/react/src/html/ui/hooks/theme.mjs b/packages/react/src/html/ui/hooks/theme.mjs new file mode 100644 index 00000000..54958973 --- /dev/null +++ b/packages/react/src/html/ui/hooks/theme.mjs @@ -0,0 +1,17 @@ +/** + * Resolves the theme preference that should be represented by the toggle icon. + * + * The selector still stores `system` as the user preference, but its trigger + * should show the light or dark icon that is currently applied to the page. + * + * @param {'system'|'light'|'dark'} preference - The stored theme preference. + * @param {boolean} systemSupportsDarkMode - Whether the OS currently prefers dark mode. + * @returns {'light'|'dark'} The theme currently displayed by the page. + */ +export const getDisplayedTheme = (preference, systemSupportsDarkMode) => { + if (preference === 'system') { + return systemSupportsDarkMode ? 'dark' : 'light'; + } + + return preference; +}; diff --git a/packages/react/src/html/ui/hooks/useTheme.mjs b/packages/react/src/html/ui/hooks/useTheme.mjs index 3ed562d1..c1333096 100644 --- a/packages/react/src/html/ui/hooks/useTheme.mjs +++ b/packages/react/src/html/ui/hooks/useTheme.mjs @@ -1,5 +1,7 @@ import { useState, useEffect, useCallback } from 'react'; +import { getDisplayedTheme } from './theme.mjs'; + import { server } from '#theme/config'; /** @returns {'dark'|'light'} The current OS-level color scheme. */ @@ -33,6 +35,14 @@ export const useTheme = () => { const [pref, setPref] = useState(() => server ? 'system' : (localStorage.getItem('theme') ?? 'system') ); + const [currentTheme, setCurrentTheme] = useState(() => + server + ? 'system' + : getDisplayedTheme( + localStorage.getItem('theme') ?? 'system', + getSystemTheme() === 'dark' + ) + ); // Apply theme on every preference change, and if 'system', // also listen for OS-level color scheme changes. @@ -44,8 +54,13 @@ export const useTheme = () => { } const mql = matchMedia('(prefers-color-scheme: dark)'); - mql.addEventListener('change', applySystemTheme); - return () => mql.removeEventListener('change', applySystemTheme); + /** Synchronizes the icon when the OS-level color scheme changes. */ + const handleSystemThemeChange = () => { + applySystemTheme(); + setCurrentTheme(getSystemTheme()); + }; + mql.addEventListener('change', handleSystemThemeChange); + return () => mql.removeEventListener('change', handleSystemThemeChange); }, [pref]); /** Updates the preference in both React state and localStorage. */ @@ -53,8 +68,9 @@ export const useTheme = () => { setPref(next); if (!server) { localStorage.setItem('theme', next); + setCurrentTheme(getDisplayedTheme(next, getSystemTheme() === 'dark')); } }, []); - return [pref, setTheme]; + return [currentTheme, setTheme]; };