Skip to content

Commit 5406fa3

Browse files
committed
Featre SSR colorScheme based on cookie
1 parent e38d859 commit 5406fa3

File tree

6 files changed

+164
-29
lines changed

6 files changed

+164
-29
lines changed

src/lib/colorScheme.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@ const useColorSchemeServerSide: UseColorScheme = () => {
3030
/* nothing */
3131
});
3232

33+
console.log("(server) useColorSchemeServerSide: ", $colorScheme.current);
34+
3335
return {
34-
"colorScheme": "light",
36+
"colorScheme": $colorScheme.current,
3537
setColorScheme
3638
};
3739
};
@@ -66,6 +68,27 @@ export function startObservingColorSchemeHtmlAttribute() {
6668
"attributeFilter": [data_fr_theme]
6769
});
6870

71+
{
72+
const setColorSchemeCookie = (colorScheme: ColorScheme) => {
73+
let newCookie = `${data_fr_theme}=${colorScheme};path=/;max-age=31536000`;
74+
75+
//We do not set the domain if we are on localhost or an ip
76+
if (window.location.hostname.match(/\.[a-zA-Z]{2,}$/)) {
77+
newCookie += `;domain=${
78+
window.location.hostname.split(".").length >= 3
79+
? window.location.hostname.replace(/^[^.]+\./, "")
80+
: window.location.hostname
81+
}`;
82+
}
83+
84+
document.cookie = newCookie;
85+
};
86+
87+
setColorSchemeCookie($colorScheme.current);
88+
89+
$colorScheme.subscribe(setColorSchemeCookie);
90+
}
91+
6992
//TODO: <meta name="theme-color" content="#000091"><!-- Défini la couleur de thème du navigateur (Safari/Android) -->
7093

7194
//TODO: Remove once https://github.com/GouvernementFR/dsfr/issues/407 is dealt with

src/lib/nextJs.tsx

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import React from "react";
22
import Head from "next/head";
33
import type { NextComponentType } from "next";
4-
import type { AppProps } from "next/app";
4+
import DefaultApp from "next/app";
5+
import type { AppProps, AppContext } from "next/app";
56
import { startDsfrReact } from "./start";
67
import type { Params as startDsfrReactParams } from "./start";
78
import { isBrowser } from "./tools/isBrowser";
@@ -20,6 +21,11 @@ import appleTouchIcon from "../dsfr/favicon/apple-touch-icon.png";
2021
import faviconSvg from "../dsfr/favicon/favicon.svg";
2122
import faviconIco from "../dsfr/favicon/favicon.ico";
2223
import faviconWebmanifestUrl from "../dsfr/favicon/manifest.webmanifest";
24+
import type { DocumentContext, DocumentProps } from "next/document";
25+
import { data_fr_scheme, data_fr_theme, $colorScheme } from "./colorScheme";
26+
import type { ColorScheme } from "./colorScheme";
27+
import { assert } from "tsafe/assert";
28+
import { is } from "tsafe/is";
2329

2430
const fontUrlByFileBasename = {
2531
"Marianne-Light": marianneLightWoff2Url,
@@ -41,7 +47,7 @@ export type Params = startDsfrReactParams & {
4147
preloadFonts?: (keyof typeof fontUrlByFileBasename)[];
4248
};
4349

44-
export function withDsfr<AppComponent extends NextComponentType<any, any, any>>(
50+
export function withAppDsfr<AppComponent extends NextComponentType<any, any, any>>(
4551
App: AppComponent,
4652
params: Params
4753
): AppComponent {
@@ -85,7 +91,90 @@ export function withDsfr<AppComponent extends NextComponentType<any, any, any>>(
8591
staticMethod => ((AppWithDsfr as any)[staticMethod] = (App as any)[staticMethod])
8692
);
8793

94+
AppWithDsfr.getInitialProps = async (appContext: AppContext) => {
95+
console.log("here here here");
96+
97+
if (!isBrowser) {
98+
/*
99+
$colorScheme.current = (() => {
100+
101+
const cookie = appContext.ctx.req?.headers.cookie
102+
103+
return cookie === undefined ? undefined : readColorSchemeInCookie(cookie);
104+
105+
})() ?? "light";
106+
*/
107+
108+
const colorScheme = (() => {
109+
const cookie = appContext.ctx.req?.headers.cookie;
110+
111+
return cookie === undefined ? undefined : readColorSchemeInCookie(cookie);
112+
})();
113+
114+
console.log(
115+
"(server) App.getInitialProps, we read the colorScheme from cookie: ",
116+
colorScheme
117+
);
118+
119+
$colorScheme.current = colorScheme ?? "light";
120+
}
121+
122+
return { ...(await (App.getInitialProps ?? DefaultApp.getInitialProps)(appContext)) };
123+
};
124+
88125
AppWithDsfr.displayName = AppWithDsfr.name;
89126

90127
return AppWithDsfr as any;
91128
}
129+
130+
export function getDocumentDsfrInitialProps(ctx: DocumentContext) {
131+
const colorScheme: ColorScheme | undefined = (() => {
132+
const cookie = ctx.req?.headers.cookie;
133+
134+
return cookie === undefined ? undefined : readColorSchemeInCookie(cookie);
135+
})();
136+
137+
return { colorScheme };
138+
}
139+
140+
export function getDsfrHtmlAttributes(props: DocumentProps) {
141+
assert(is<ReturnType<typeof getDocumentDsfrInitialProps>>(props));
142+
143+
const { colorScheme } = props;
144+
145+
if (colorScheme === undefined) {
146+
return {};
147+
}
148+
149+
$colorScheme.current = colorScheme;
150+
151+
return {
152+
[data_fr_scheme]: colorScheme,
153+
[data_fr_theme]: colorScheme
154+
};
155+
}
156+
157+
function readColorSchemeInCookie(cookie: string) {
158+
const parsedCookies = Object.fromEntries(
159+
cookie
160+
.split(/; */)
161+
.map(line => line.split("="))
162+
.map(([key, value]) => [key, decodeURIComponent(value)])
163+
);
164+
165+
if (!(data_fr_theme in parsedCookies)) {
166+
return undefined;
167+
}
168+
169+
const colorScheme = parsedCookies[data_fr_theme];
170+
171+
return (() => {
172+
switch (colorScheme) {
173+
case "light":
174+
case "dark":
175+
return colorScheme;
176+
default:
177+
return undefined;
178+
}
179+
})();
180+
}

src/lib/start.ts

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,13 @@ export async function startDsfrReact(params: Params) {
3131

3232
isStarted = true;
3333

34-
const global: any = window;
34+
//NOTE: It's non null if we are in a SSR setup
35+
if (document.documentElement.getAttribute(data_fr_theme) === null) {
36+
console.log("(client) Start: There was NO HTML tag");
3537

36-
document.documentElement.setAttribute(data_fr_scheme, defaultColorScheme);
38+
//NOTE: SPA or SSG
3739

38-
const isNextJsDevMode = global.__NEXT_DATA__?.buildId === "development";
39-
40-
hack_html_attribute_supposed_to_be_set_by_js: {
41-
if (isNextJsDevMode) {
42-
// NOTE: Or else we get an hydration error.
43-
break hack_html_attribute_supposed_to_be_set_by_js;
44-
}
40+
document.documentElement.setAttribute(data_fr_scheme, defaultColorScheme);
4541

4642
document.documentElement.setAttribute(
4743
data_fr_theme,
@@ -67,24 +63,22 @@ export async function startDsfrReact(params: Params) {
6763
: "light";
6864
})()
6965
);
66+
} else {
67+
console.log("(client) Start: Html tags are present");
7068
}
7169

7270
startObservingColorSchemeHtmlAttribute();
7371

74-
global.dsfr = { verbose, "mode": "manual" };
72+
(window as any).dsfr = { verbose, "mode": "manual" };
7573

7674
await import("@gouvfr/dsfr/dist/dsfr.module");
7775

78-
if (isNextJsDevMode) {
79-
console.log(
80-
[
81-
"Artificial delay to avoid the",
82-
"'Hydration failed because the initial UI does not match what was rendered on the server.'",
83-
"Error. In production mode the white flash wont be that long."
84-
].join(" ")
85-
);
76+
if ((window as any).__NEXT_DATA__?.buildId === "development") {
77+
// NOTE: @gouvfr/dsfr/dist/dsfr.module.js is not isomorphic, it can't run on the Server.",
78+
// We set an artificial delay before starting the module otherwise to avoid getting",
79+
// Hydration error from Next.js
8680
await new Promise(resolve => setTimeout(resolve, 150));
8781
}
8882

89-
global.dsfr.start();
83+
(window as any).dsfr.start();
9084
}

src/test/frameworks/next/pages/_app.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import DefaultApp from "next/app";
22
import "dsfr-react/dsfr/dsfr.css";
3-
import { withDsfr } from "dsfr-react/lib/nextJs";
4-
3+
import { withAppDsfr } from "dsfr-react/lib/nextJs";
54

65
//export default withDsfr(DefaultApp);
7-
export default withDsfr(DefaultApp, {
6+
export default withAppDsfr(DefaultApp, {
87
"defaultColorScheme": "system",
98
"preloadFonts": [
109
//"Marianne-Light",
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import DefaultDocument, { Html, Head, Main, NextScript } from 'next/document'
2+
import type { DocumentContext } from "next/document";
3+
import { getDocumentDsfrInitialProps, getDsfrHtmlAttributes } from "dsfr-react/lib/nextJs";
4+
5+
export default class Document extends DefaultDocument {
6+
static async getInitialProps(ctx: DocumentContext) {
7+
8+
const initialProps = await DefaultDocument.getInitialProps(ctx);
9+
10+
const dsfrInitialProps = getDocumentDsfrInitialProps(ctx);
11+
12+
console.log("(server) Document.getInitialProps we read colorScheme from cookie: ", dsfrInitialProps);
13+
14+
return { ...initialProps, ...dsfrInitialProps };
15+
16+
}
17+
18+
render() {
19+
return (
20+
<Html {...getDsfrHtmlAttributes(this.props)}>
21+
<Head />
22+
<body>
23+
<Main />
24+
<NextScript />
25+
</body>
26+
</Html>
27+
);
28+
}
29+
30+
}

src/test/frameworks/next/pages/index.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ export default function Index() {
1010

1111
return (
1212
<>
13-
<h1>Color Scheme: {colorScheme}</h1>
14-
<button onClick={()=> setColorScheme("dark")}>Set color scheme to dark</button>
15-
<button onClick={()=> setColorScheme("light")}>Set color scheme to light</button>
16-
<button onClick={()=> setColorScheme("system")}>Set color scheme to system</button>
13+
<h1>Color Scheme: {colorScheme}</h1>
14+
<button onClick={() => setColorScheme("dark")}>Set color scheme to dark</button>
15+
<button onClick={() => setColorScheme("light")}>Set color scheme to light</button>
16+
<button onClick={() => setColorScheme("system")}>Set color scheme to system</button>
1717
<header role="banner" className="fr-header">
1818
<div className="fr-header__body">
1919
<div className="fr-container">

0 commit comments

Comments
 (0)