Skip to content

Commit 356a0a7

Browse files
committed
refactor(theme): enhance theme mode handling with new initialization and script generation functions
1 parent 41c8eb5 commit 356a0a7

File tree

3 files changed

+152
-35
lines changed

3 files changed

+152
-35
lines changed

packages/ui/src/hooks/use-theme-mode.ts

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,15 @@ export type ThemeMode = "light" | "dark" | "auto";
1414
export function useThemeMode() {
1515
const [mode, setMode] = useState<ThemeMode>(getInitialMode(getMode()));
1616

17-
/**
18-
* Persist `mode` in local storage and add/remove `dark` class on `html`
19-
*/
20-
useEffect(() => {
21-
setModeInLS(mode);
22-
setModeInDOM(mode);
23-
}, []);
24-
2517
/**
2618
* Sync all tabs with the latest theme mode value
2719
*/
2820
useWatchLocalStorageValue({
2921
key: LS_THEME_MODE,
30-
onChange(newValue) {
31-
if (newValue) handleSetMode(newValue as ThemeMode);
22+
onChange(newValue: ThemeMode) {
23+
if (newValue) {
24+
handleSetMode(newValue);
25+
}
3226
},
3327
});
3428

@@ -53,23 +47,31 @@ export function useThemeMode() {
5347
function toggleMode() {
5448
let newMode = mode;
5549

56-
if (newMode === "auto") newMode = computeModeValue(newMode);
50+
if (newMode === "auto") {
51+
newMode = computeModeValue(newMode);
52+
}
5753

5854
newMode = newMode === "dark" ? "light" : "dark";
5955

6056
handleSetMode(newMode);
6157
}
6258

6359
/**
64-
* Sets the value to `<Flowbite theme={{ mode }}>` prop
60+
* Sets the value to `<ThemeConfig mode={mode} />` prop
6561
*/
6662
function clearMode() {
6763
const newMode = mode ?? DEFAULT_MODE;
6864

6965
handleSetMode(newMode);
7066
}
7167

72-
return { mode, computedMode: computeModeValue(mode), setMode: handleSetMode, toggleMode, clearMode };
68+
return {
69+
mode,
70+
computedMode: computeModeValue(mode),
71+
setMode: handleSetMode,
72+
toggleMode,
73+
clearMode,
74+
};
7375
}
7476

7577
/**
@@ -79,7 +81,6 @@ function useSyncMode(onChange: (mode: ThemeMode) => void) {
7981
useEffect(() => {
8082
function handleSync(e: Event) {
8183
const mode = (e as CustomEvent<ThemeMode>).detail;
82-
8384
onChange(mode);
8485
}
8586

@@ -110,9 +111,11 @@ function setModeInDOM(mode: ThemeMode) {
110111
}
111112

112113
function getInitialMode(defaultMode?: ThemeMode): ThemeMode {
113-
if (!isClient()) return DEFAULT_MODE;
114+
if (!isClient()) {
115+
return DEFAULT_MODE;
116+
}
114117

115-
const LSMode = localStorage.getItem(LS_THEME_MODE) as ThemeMode | undefined;
118+
const LSMode = localStorage.getItem(LS_THEME_MODE) as ThemeMode | null;
116119

117120
return LSMode ?? defaultMode ?? DEFAULT_MODE;
118121
}

packages/ui/src/theme/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,5 +94,5 @@ export const theme: FlowbiteTheme = {
9494
};
9595

9696
export { ThemeConfig, type ThemeConfigProps } from "./config";
97-
export { ThemeModeScript, type ThemeModeScriptProps } from "./mode-script";
97+
export { getThemeModeScript, initThemeMode, ThemeModeScript, type ThemeModeScriptProps } from "./mode-script";
9898
export { ThemeProvider, useThemeProvider, type ThemeProviderProps, type ThemeProviderValue } from "./provider";

packages/ui/src/theme/mode-script.tsx

Lines changed: 132 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,59 +2,173 @@ import type React from "react";
22
import type { ThemeMode } from "../hooks/use-theme-mode";
33
import { getPrefix } from "../store";
44

5+
const defaultOptions = {
6+
mode: "auto" as ThemeMode,
7+
defaultMode: "auto" as ThemeMode,
8+
localStorageKey: "flowbite-theme-mode",
9+
prefix: "",
10+
};
11+
512
export interface ThemeModeScriptProps extends React.ComponentPropsWithoutRef<"script"> {
13+
/**
14+
* The current theme mode to use
15+
*
16+
* @type {"light" | "dark" | "auto"}
17+
*/
618
mode?: ThemeMode;
719
/**
8-
* "light" | "dark" | "auto"
20+
* The default theme mode if none is set
21+
*
22+
* @type {"light" | "dark" | "auto"}
23+
* @default "auto"
924
*/
1025
defaultMode?: ThemeMode;
1126
/**
27+
* Key used to store theme mode in localStorage
28+
*
29+
* @type {string}
1230
* @default "flowbite-theme-mode"
1331
*/
1432
localStorageKey?: string;
1533
}
1634

35+
/**
36+
* A script component that handles theme mode initialization
37+
*
38+
* @param {Object} props - The component props
39+
* @param {ThemeMode} [props.mode] - The current theme mode to use
40+
* @param {ThemeMode} [props.defaultMode="auto"] - The default theme mode if none is set
41+
* @param {string} [props.localStorageKey="flowbite-theme-mode"] - Key used to store theme mode in localStorage
42+
* @param {React.ComponentPropsWithoutRef<"script">} props.others - Additional script element props
43+
* @returns {JSX.Element} Script element that initializes theme mode
44+
*/
1745
export function ThemeModeScript({
1846
mode,
19-
defaultMode = "light",
20-
localStorageKey = "flowbite-theme-mode",
47+
defaultMode = defaultOptions.defaultMode,
48+
localStorageKey = defaultOptions.localStorageKey,
2149
...others
22-
}: ThemeModeScriptProps) {
50+
}: ThemeModeScriptProps): JSX.Element {
2351
return (
2452
<script
2553
{...others}
2654
data-flowbite-theme-mode-script
2755
dangerouslySetInnerHTML={{
28-
__html: getScript({ mode, defaultMode, localStorageKey, prefix: getPrefix() ?? "" }),
56+
__html: getThemeModeScript({ mode, defaultMode, localStorageKey, prefix: getPrefix() ?? "" }),
2957
}}
3058
/>
3159
);
3260
}
3361

3462
ThemeModeScript.displayName = "ThemeModeScript";
3563

36-
function getScript({
37-
mode,
38-
defaultMode,
39-
localStorageKey,
40-
prefix,
41-
}: {
42-
mode?: ThemeMode;
43-
defaultMode: ThemeMode;
44-
localStorageKey: string;
45-
prefix: string;
46-
}) {
64+
/**
65+
* Generates a script string that handles theme mode initialization
66+
*
67+
* @param {Object} options - The options object
68+
* @param {ThemeMode} [options.mode] - The current theme mode to use
69+
* @param {ThemeMode} [options.defaultMode="auto"] - The default theme mode if none is set
70+
* @param {string} [options.localStorageKey="flowbite-theme-mode"] - Key used to store theme mode in localStorage
71+
* @param {string} [options.prefix=""] - The prefix to use for the theme mode class
72+
* @returns {string} Script string that initializes theme mode
73+
*/
74+
export function getThemeModeScript(
75+
props: {
76+
mode?: ThemeMode;
77+
defaultMode?: ThemeMode;
78+
localStorageKey?: string;
79+
prefix?: string;
80+
} = {},
81+
): string {
82+
const {
83+
mode,
84+
defaultMode = defaultOptions.defaultMode,
85+
localStorageKey = defaultOptions.localStorageKey,
86+
prefix = defaultOptions.prefix,
87+
} = props;
88+
4789
return `
4890
try {
49-
const mode = window.localStorage.getItem("${localStorageKey}") ?? "${mode}" ?? "${defaultMode}";
91+
const resolvedMode = window.localStorage.getItem("${localStorageKey}") ?? "${mode}" ?? "${defaultMode}";
5092
const computedMode =
51-
mode === "auto" ? (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light") : mode;
93+
resolvedMode === "auto" ? (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light") : resolvedMode;
5294
5395
if (computedMode === "dark") {
5496
document.documentElement.classList.add("${prefix}dark");
5597
} else {
5698
document.documentElement.classList.remove("${prefix}dark");
5799
}
100+
localStorage.setItem("${localStorageKey}", resolvedMode);
101+
102+
// Add listener for system theme changes
103+
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
104+
mediaQuery.addEventListener("change", (e) => {
105+
const currentMode = window.localStorage.getItem("${localStorageKey}");
106+
if (currentMode === "auto") {
107+
if (e.matches) {
108+
document.documentElement.classList.add("${prefix}dark");
109+
} else {
110+
document.documentElement.classList.remove("${prefix}dark");
111+
}
112+
}
113+
});
58114
} catch (e) {}
59115
`;
60116
}
117+
118+
/**
119+
* Initializes the theme mode by checking localStorage, provided mode, or default mode
120+
* and applies the appropriate class to the document element
121+
*
122+
* @param {Object} options - The options object
123+
* @param {ThemeMode} [options.mode] - The current theme mode to use
124+
* @param {ThemeMode} [options.defaultMode="auto"] - The default theme mode if none is set
125+
* @param {string} [options.localStorageKey="flowbite-theme-mode"] - Key used to store theme mode in localStorage
126+
* @param {string} [options.prefix=""] - The prefix to use for the theme mode class
127+
* @returns {void}
128+
*/
129+
export function initThemeMode(
130+
props: {
131+
mode?: ThemeMode;
132+
defaultMode?: ThemeMode;
133+
localStorageKey?: string;
134+
prefix?: string;
135+
} = {},
136+
): void {
137+
const {
138+
mode,
139+
defaultMode = defaultOptions.defaultMode,
140+
localStorageKey = defaultOptions.localStorageKey,
141+
prefix = defaultOptions.prefix,
142+
} = props;
143+
144+
try {
145+
const resolvedMode = window.localStorage.getItem(localStorageKey) ?? mode ?? defaultMode;
146+
const computedMode =
147+
resolvedMode === "auto"
148+
? window.matchMedia("(prefers-color-scheme: dark)").matches
149+
? "dark"
150+
: "light"
151+
: resolvedMode;
152+
153+
if (computedMode === "dark") {
154+
document.documentElement.classList.add(`${prefix}dark`);
155+
} else {
156+
document.documentElement.classList.remove(`${prefix}dark`);
157+
}
158+
localStorage.setItem(localStorageKey, resolvedMode);
159+
160+
// Add listener for system theme changes
161+
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
162+
mediaQuery.addEventListener("change", (e) => {
163+
const currentMode = window.localStorage.getItem(localStorageKey);
164+
if (currentMode === "auto") {
165+
if (e.matches) {
166+
document.documentElement.classList.add(`${prefix}dark`);
167+
} else {
168+
document.documentElement.classList.remove(`${prefix}dark`);
169+
}
170+
}
171+
});
172+
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-empty
173+
} catch (e) {}
174+
}

0 commit comments

Comments
 (0)