1+ import { isBrowser } from "./tools/isBrowser" ;
2+ import { assert } from "tsafe/assert" ;
13import { createStatefulObservable , useRerenderOnChange } from "./tools/StatefulObservable" ;
2- import { createContext , useContext } from "react" ;
34import { 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
79export type ColorScheme = "light" | "dark" ;
810
911export const data_fr_theme = "data-fr-theme" ;
1012export 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
1521type UseIsDark = ( ) => {
1622 isDark : boolean ;
1723 setIsDark : ( isDark : boolean | "system" ) => void ;
1824} ;
1925
26+ const $isAfterFirstEffect = createStatefulObservable ( ( ) => false ) ;
27+
2028const 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
4460const 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
5975export 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}
0 commit comments