Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/bright-themes-guess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@nodejs/doc-kit-generator-react': patch
---

Show the active light or dark theme icon when the system preference is selected.
4 changes: 2 additions & 2 deletions packages/react/src/html/ui/components/ThemeToggle.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import withIsland from '../islands/withIsland.jsx';
* Theme switcher.
*/
const Toggle = () => {
const [themePreference, setThemePreference] = useTheme();
const [currentTheme, setThemePreference] = useTheme();

return (
<ThemeToggle onChange={setThemePreference} currentTheme={themePreference} />
<ThemeToggle onChange={setThemePreference} currentTheme={currentTheme} />
);
};

Expand Down
16 changes: 16 additions & 0 deletions packages/react/src/html/ui/hooks/__tests__/theme.test.mjs
Original file line number Diff line number Diff line change
@@ -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');
});
});
17 changes: 17 additions & 0 deletions packages/react/src/html/ui/hooks/theme.mjs
Original file line number Diff line number Diff line change
@@ -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;
};
22 changes: 19 additions & 3 deletions packages/react/src/html/ui/hooks/useTheme.mjs
Original file line number Diff line number Diff line change
@@ -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. */
Expand Down Expand Up @@ -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.
Expand All @@ -44,17 +54,23 @@ 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. */
const setTheme = useCallback(next => {
setPref(next);
if (!server) {
localStorage.setItem('theme', next);
setCurrentTheme(getDisplayedTheme(next, getSystemTheme() === 'dark'));
}
}, []);

return [pref, setTheme];
return [currentTheme, setTheme];
};