Skip to content

Commit b6ea0d4

Browse files
committed
fix: improve handling of dark mode preferences
Closes #4365 Signed-off-by: Jon Koops <[email protected]>
1 parent c23a503 commit b6ea0d4

File tree

2 files changed

+114
-2
lines changed

2 files changed

+114
-2
lines changed

packages/documentation-framework/layouts/sideNavLayout/sideNavLayout.js

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useEffect, useState, createContext } from 'react';
1+
import React, { useEffect, useState, createContext, useCallback } from 'react';
22
import {
33
Button,
44
Page,
@@ -232,6 +232,26 @@ export function attachDocSearch(algolia, inputSelector, timeout) {
232232
}
233233
}
234234

235+
const DARK_MODE_CLASS = "pf-v6-theme-dark";
236+
const DARK_MODE_STORAGE_KEY = "dark-mode";
237+
238+
/**
239+
* Determines if dark mode is enabled based on the stored value or the system preference.
240+
* @returns {boolean} true if dark mode is enabled, false otherwise
241+
*/
242+
function isDarkModeEnabled() {
243+
// When running in prerender mode we can't access the browser APIs.
244+
if (process.env.PRERENDER) {
245+
return false;
246+
}
247+
248+
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
249+
const storedValue = localStorage.getItem(DARK_MODE_STORAGE_KEY);
250+
const isEnabled = storedValue === null ? mediaQuery.matches : storedValue === "true";
251+
252+
return isEnabled;
253+
}
254+
235255
export const SideNavLayout = ({ children, groupedRoutes, navOpen: navOpenProp }) => {
236256
const algolia = process.env.algolia;
237257
const hasGdprBanner = process.env.hasGdprBanner;
@@ -245,7 +265,63 @@ export const SideNavLayout = ({ children, groupedRoutes, navOpen: navOpenProp })
245265

246266
const [versions, setVersions] = useState({ ...staticVersions });
247267
const [isRTL, setIsRTL] = useState(false);
248-
const [isDarkTheme, setIsDarkTheme] = React.useState(false);
268+
const [isDarkTheme, setIsDarkThemeInternal] = useState(() => isDarkModeEnabled());
269+
270+
/**
271+
* Stores the dark mode preference in local storage and updates the dark mode class.
272+
*/
273+
const setIsDarkTheme = useCallback((value) => {
274+
localStorage.setItem(DARK_MODE_STORAGE_KEY, value.toString());
275+
updateDarkMode();
276+
}, []);
277+
278+
/**
279+
* Updates the dark mode class to the root element depending on whether dark mode is enabled.
280+
*/
281+
const updateDarkMode = useCallback(() => {
282+
const isEnabled = isDarkModeEnabled();
283+
const root = document.documentElement;
284+
285+
if (isEnabled) {
286+
root.classList.add(DARK_MODE_CLASS);
287+
} else {
288+
root.classList.remove(DARK_MODE_CLASS);
289+
}
290+
291+
setIsDarkThemeInternal(isEnabled);
292+
}, []);
293+
294+
useEffect(() => {
295+
// When running in prerender mode we can't access the browser APIs.
296+
if (process.env.PRERENDER) {
297+
return;
298+
}
299+
300+
// Update the dark mode when the the user changes their system/browser preference.
301+
const onQueryChange = () => {
302+
// Remove the stored value to defer to the system preference.
303+
localStorage.removeItem(DARK_MODE_STORAGE_KEY);
304+
updateDarkMode();
305+
};
306+
307+
// Update the dark mode when the user changes the preference in another context (e.g. tab or window).
308+
/** @type {(event: StorageEvent) => void} */
309+
const onStorageChange = (event) => {
310+
if (event.key === DARK_MODE_STORAGE_KEY) {
311+
updateDarkMode();
312+
}
313+
};
314+
315+
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
316+
317+
mediaQuery.addEventListener("change", onQueryChange);
318+
window.addEventListener("storage", onStorageChange);
319+
320+
return () => {
321+
mediaQuery.removeEventListener("change", onQueryChange);
322+
window.removeEventListener("storage", onStorageChange);
323+
};
324+
}, []);
249325

250326
useEffect(() => {
251327
if (typeof window === 'undefined') {

packages/documentation-framework/templates/html.ejs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<head>
44
<meta charset="utf-8">
55
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
6+
<meta name="color-scheme" content="light dark">
67
<meta name="description" content="PatternFly is Red Hat's open source design system. It consists of components, documentation, and code for building enterprise applications at scale.">
78
<title><%= title %></title>
89
<link rel="shortcut icon" href="/assets/Favicon-Light.png">
@@ -13,6 +14,41 @@
1314
<meta name="mobile-web-app-capable" content="yes">
1415
<meta name="theme-color" content="#151515">
1516
<meta name="application-name" content="PatternFly docs">
17+
<script>
18+
(() => {
19+
"use strict";
20+
21+
const DARK_MODE_CLASS = "pf-v6-theme-dark";
22+
const DARK_MODE_STORAGE_KEY = "dark-mode";
23+
const darkModeQuery = window.matchMedia("(prefers-color-scheme: dark)");
24+
25+
// Ensure that the dark mode is correctly set before the page starts rendering.
26+
updateDarkMode();
27+
28+
/**
29+
* Applies the dark mode class to the root element if dark mode is enabled.
30+
*/
31+
function updateDarkMode() {
32+
const isEnabled = isDarkModeEnabled();
33+
const root = document.documentElement;
34+
35+
if (isEnabled) {
36+
root.classList.add(DARK_MODE_CLASS);
37+
}
38+
}
39+
40+
/**
41+
* Determines if dark mode is enabled based on the stored value or the system preference.
42+
* @returns {boolean} true if dark mode is enabled, false otherwise
43+
*/
44+
function isDarkModeEnabled() {
45+
const storedValue = localStorage.getItem(DARK_MODE_STORAGE_KEY);
46+
const isEnabled = storedValue === null ? darkModeQuery.matches : storedValue === "true";
47+
48+
return isEnabled;
49+
}
50+
})();
51+
</script>
1652
<%= htmlWebpackPlugin.tags.headTags %>
1753
</head>
1854
<body>

0 commit comments

Comments
 (0)