Skip to content

Commit f399185

Browse files
committed
No need to opt out optimization to avoid white flashes
1 parent 9c6c111 commit f399185

File tree

6 files changed

+261
-210
lines changed

6 files changed

+261
-210
lines changed

src/lib/darkMode.ts

Lines changed: 151 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,33 @@
1+
import { isBrowser } from "./tools/isBrowser";
2+
import { assert } from "tsafe/assert";
13
import { createStatefulObservable, useRerenderOnChange } from "./tools/StatefulObservable";
2-
import { createContext, useContext } from "react";
34
import { useConstCallback } from "./tools/powerhooks/useConstCallback";
4-
import { assert } from "tsafe/assert";
5-
import { isBrowser } from "./tools/isBrowser";
5+
import { createContext, useContext } from "react";
6+
import { memoize } from "./tools/memoize";
7+
import { getColors } from "./colors";
68

79
export type ColorScheme = "light" | "dark";
810

911
export const data_fr_theme = "data-fr-theme";
1012
export const data_fr_scheme = "data-fr-scheme";
11-
export const rootColorSchemeStyleTagId = "root-color-scheme";
12-
//export const $colorScheme = createStatefulObservable<ColorScheme>(() => "light");
13-
export const $isDark = createStatefulObservable(() => false);
13+
export const rootColorSchemeStyleTagId = "dsfr-root-color-scheme";
14+
15+
const $clientSideIsDark = createStatefulObservable<boolean>(() => {
16+
throw new Error("not initialized yet");
17+
});
18+
19+
export const getClientSideIsDark = memoize(() => $clientSideIsDark.current);
1420

1521
type UseIsDark = () => {
1622
isDark: boolean;
1723
setIsDark: (isDark: boolean | "system") => void;
1824
};
1925

26+
const $isAfterFirstEffect = createStatefulObservable(() => false);
27+
2028
const useIsDarkClientSide: UseIsDark = () => {
21-
useRerenderOnChange($isDark);
29+
useRerenderOnChange($clientSideIsDark);
30+
useRerenderOnChange($isAfterFirstEffect);
2231

2332
const setIsDark = useConstCallback((isDark: boolean | "system") =>
2433
document.documentElement.setAttribute(
@@ -36,19 +45,26 @@ const useIsDarkClientSide: UseIsDark = () => {
3645
)
3746
);
3847

39-
return { "isDark": $isDark.current, setIsDark };
48+
return {
49+
"isDark": $isAfterFirstEffect.current
50+
? $clientSideIsDark.current
51+
: ssrWasPerformedWithIsDark,
52+
setIsDark
53+
};
4054
};
4155

42-
export const isDarkContext = createContext<boolean | undefined>(undefined);
56+
const ssrIsDarkContext = createContext<boolean | undefined>(undefined);
57+
58+
export const { Provider: SsrIsDarkProvider } = ssrIsDarkContext;
4359

4460
const useIsDarkServerSide: UseIsDark = () => {
4561
const setIsDark = useConstCallback(() => {
4662
/* nothing */
4763
});
4864

49-
const isDark = useContext(isDarkContext);
65+
const isDark = useContext(ssrIsDarkContext);
5066

51-
assert(isDark !== undefined, "color scheme context should be provided");
67+
assert(isDark !== undefined, "Not within provider");
5268

5369
return {
5470
isDark,
@@ -58,11 +74,14 @@ const useIsDarkServerSide: UseIsDark = () => {
5874

5975
export const useIsDark = isBrowser ? useIsDarkClientSide : useIsDarkServerSide;
6076

61-
function getCurrentIsDarkFromHtmlAttribute(): boolean {
77+
let ssrWasPerformedWithIsDark: boolean;
78+
79+
function getCurrentIsDarkFromHtmlAttribute(): boolean | undefined {
6280
const colorSchemeFromHtmlAttribute = document.documentElement.getAttribute(data_fr_theme);
6381

6482
switch (colorSchemeFromHtmlAttribute) {
6583
case null:
84+
return undefined;
6685
case "light":
6786
return false;
6887
case "dark":
@@ -72,22 +91,106 @@ function getCurrentIsDarkFromHtmlAttribute(): boolean {
7291
assert(false);
7392
}
7493

75-
export const refDoPersistDarkModePreferenceWithCookie = { "current": false };
94+
export function startClientSideIsDarkLogic(params: {
95+
registerEffectAction: (action: () => void) => void;
96+
doPersistDarkModePreferenceWithCookie: boolean;
97+
colorSchemeExplicitlyProvidedAsParameter: ColorScheme | "system";
98+
}) {
99+
const {
100+
doPersistDarkModePreferenceWithCookie,
101+
registerEffectAction,
102+
colorSchemeExplicitlyProvidedAsParameter
103+
} = params;
104+
105+
const { clientSideIsDark, ssrWasPerformedWithIsDark: ssrWasPerformedWithIsDark_ } = ((): {
106+
clientSideIsDark: boolean;
107+
ssrWasPerformedWithIsDark: boolean;
108+
} => {
109+
const isDarkFromHtmlAttribute = getCurrentIsDarkFromHtmlAttribute();
110+
111+
if (isDarkFromHtmlAttribute !== undefined) {
112+
return {
113+
"clientSideIsDark": isDarkFromHtmlAttribute,
114+
"ssrWasPerformedWithIsDark": isDarkFromHtmlAttribute
115+
};
116+
}
117+
118+
const isDarkExplicitlyProvidedAsParameter = (() => {
119+
if (colorSchemeExplicitlyProvidedAsParameter === "system") {
120+
return undefined;
121+
}
76122

77-
export function startObservingColorSchemeHtmlAttribute() {
78-
$isDark.current = getCurrentIsDarkFromHtmlAttribute();
123+
switch (colorSchemeExplicitlyProvidedAsParameter as ColorScheme) {
124+
case "dark":
125+
return true;
126+
case "light":
127+
return false;
128+
}
129+
})();
79130

80-
new MutationObserver(() => ($isDark.current = getCurrentIsDarkFromHtmlAttribute())).observe(
81-
document.documentElement,
82-
{
83-
"attributes": true,
84-
"attributeFilter": [data_fr_theme]
85-
}
131+
const isDarkFromLocalStorage = (() => {
132+
const colorSchemeReadFromLocalStorage = localStorage.getItem("scheme");
133+
134+
if (colorSchemeReadFromLocalStorage === null) {
135+
return undefined;
136+
}
137+
138+
if (colorSchemeReadFromLocalStorage === "system") {
139+
return undefined;
140+
}
141+
142+
switch (colorSchemeExplicitlyProvidedAsParameter as ColorScheme) {
143+
case "dark":
144+
return true;
145+
case "light":
146+
return false;
147+
}
148+
})();
149+
150+
const isDarkFromOsPreference = (() => {
151+
if (!window.matchMedia) {
152+
return undefined;
153+
}
154+
155+
return window.matchMedia("(prefers-color-scheme: dark)").matches;
156+
})();
157+
158+
const isDarkFallback = false;
159+
160+
return {
161+
"ssrWasPerformedWithIsDark": isDarkExplicitlyProvidedAsParameter ?? isDarkFallback,
162+
"clientSideIsDark":
163+
isDarkFromLocalStorage ??
164+
isDarkExplicitlyProvidedAsParameter ??
165+
isDarkFromOsPreference ??
166+
isDarkFallback
167+
};
168+
})();
169+
170+
ssrWasPerformedWithIsDark = ssrWasPerformedWithIsDark_;
171+
172+
$clientSideIsDark.current = clientSideIsDark;
173+
174+
registerEffectAction(() => ($isAfterFirstEffect.current = true));
175+
176+
[data_fr_scheme, data_fr_theme].forEach(attr =>
177+
document.documentElement.setAttribute(attr, clientSideIsDark ? "dark" : "light")
86178
);
87179

180+
new MutationObserver(() => {
181+
const isDarkFromHtmlAttribute = getCurrentIsDarkFromHtmlAttribute();
182+
183+
assert(isDarkFromHtmlAttribute !== undefined);
184+
185+
$clientSideIsDark.current = isDarkFromHtmlAttribute;
186+
}).observe(document.documentElement, {
187+
"attributes": true,
188+
"attributeFilter": [data_fr_theme]
189+
});
190+
88191
{
89192
const setColorSchemeCookie = (isDark: boolean) => {
90-
if (!refDoPersistDarkModePreferenceWithCookie.current) {
193+
if (!doPersistDarkModePreferenceWithCookie) {
91194
return;
92195
}
93196

@@ -109,39 +212,42 @@ export function startObservingColorSchemeHtmlAttribute() {
109212
document.cookie = newCookie;
110213
};
111214

112-
setColorSchemeCookie($isDark.current);
215+
setColorSchemeCookie($clientSideIsDark.current);
113216

114-
$isDark.subscribe(setColorSchemeCookie);
217+
$clientSideIsDark.subscribe(setColorSchemeCookie);
115218
}
116219

117-
//TODO: <meta name="theme-color" content="#000091"><!-- Défini la couleur de thème du navigateur (Safari/Android) -->
118-
119-
//TODO: Remove once https://github.com/GouvernementFR/dsfr/issues/407 is dealt with
120220
{
121221
const setRootColorScheme = (isDark: boolean) => {
122-
const colorScheme: ColorScheme = isDark ? "dark" : "light";
123-
124-
remove_existing_element: {
125-
const element = document.getElementById(rootColorSchemeStyleTagId);
126-
127-
if (element === null) {
128-
break remove_existing_element;
129-
}
130-
131-
element.remove();
132-
}
133-
134-
const element = document.createElement("style");
222+
document.getElementById(rootColorSchemeStyleTagId)?.remove();
223+
224+
document.head.insertAdjacentHTML(
225+
"afterend",
226+
`<style id="${rootColorSchemeStyleTagId}">:root { color-scheme: ${
227+
isDark ? "dark" : "light"
228+
}; }</style>`
229+
);
230+
};
135231

136-
element.id = rootColorSchemeStyleTagId;
232+
setRootColorScheme($clientSideIsDark.current);
137233

138-
element.innerHTML = `:root { color-scheme: ${colorScheme}; }`;
234+
$clientSideIsDark.subscribe(setRootColorScheme);
235+
}
139236

140-
document.getElementsByTagName("head")[0].appendChild(element);
237+
{
238+
const setThemeColor = (isDark: boolean) => {
239+
document.querySelector("meta[name=theme-color]")?.remove();
240+
241+
document.head.insertAdjacentHTML(
242+
"afterend",
243+
`<meta name="theme-color" content="${
244+
getColors(isDark).decisions.background.default.grey.default
245+
}">`
246+
);
141247
};
142248

143-
setRootColorScheme($isDark.current);
249+
setThemeColor($clientSideIsDark.current);
144250

145-
$isDark.subscribe(setRootColorScheme);
251+
$clientSideIsDark.subscribe(setThemeColor);
146252
}
147253
}

src/lib/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
export * from "./start";
2-
export { useIsDark, $isDark } from "./darkMode";
1+
export { startReactDsfr } from "./start";
2+
export type { Params } from "./start";
3+
export { useIsDark } from "./darkMode";
34
export * from "./colors";
45
export type { BreakpointKeys } from "./breakpoints";
56
import { breakpoints } from "./breakpoints";

0 commit comments

Comments
 (0)