Skip to content

Commit 4a82cb3

Browse files
committed
Dark mode persistance
1 parent 1c2551f commit 4a82cb3

File tree

5 files changed

+102
-34
lines changed

5 files changed

+102
-34
lines changed

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,29 @@ If you are deploying Keycloak on Kubernetes using Helm, here's how to configure
4747
...
4848
```
4949
50+
### Dark Mode Persistence
51+
52+
If you want to ensure the color scheme preference from your app to be persisted when navigating to the Keycloak pages
53+
you need to add `dark=true` or `dark=false` when redirecting to the login or account page.
54+
55+
With [oidc-spa](https://oidc-spa.dev) and [react-dsfr](https://github.com/codegouvfr/react-dsfr) for the login theme:
56+
57+
`src/oidc.ts`
58+
```tsx
59+
import { getIsDark } from "@codegouvfr/react-dsfr/useIsDark";
60+
export const { OidcProvider, useOidc } = createReactOidc({
61+
// ...
62+
extraQueryParams: ()=> ({
63+
dark: getIsDark() ? "true" : "false"
64+
})
65+
});
66+
```
67+
68+
For the Account page:
69+
70+
```
71+
https://your-keycloak-url/auth/realms/your-realm/account?referrer=your-app-url&dark=true
72+
```
5073

5174
# License
5275

src/account/Template.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import "@codegouvfr/react-dsfr/main.css";
12
import { useEffect } from "react";
23
import { clsx } from "keycloakify/tools/clsx";
34
import { kcSanitize } from "keycloakify/lib/kcSanitize";
@@ -7,17 +8,16 @@ import { useInitialize } from "keycloakify/account/Template.useInitialize";
78
import type { TemplateProps } from "keycloakify/account/TemplateProps";
89
import type { I18n } from "./i18n";
910
import type { KcContext } from "./KcContext";
10-
import "@codegouvfr/react-dsfr/main.css";
1111
import { startReactDsfr } from "@codegouvfr/react-dsfr/spa";
12-
import { getReferrerUrl } from "../login/shared/getReferrerUrl";
1312
import { Header as DsfrHeader } from "@codegouvfr/react-dsfr/Header";
1413
import { Footer as DsfrFooter } from "@codegouvfr/react-dsfr/Footer";
1514
import { SideMenu } from "@codegouvfr/react-dsfr/SideMenu";
1615
import { Alert } from "@codegouvfr/react-dsfr/Alert";
1716
import { headerFooterDisplayItem } from "@codegouvfr/react-dsfr/Display";
1817
import { fr } from "@codegouvfr/react-dsfr";
18+
import { getColorScheme } from "../shared/getColorScheme";
1919

20-
startReactDsfr({ defaultColorScheme: "system" });
20+
startReactDsfr({ defaultColorScheme: getColorScheme() ?? "system" });
2121

2222
export default function Template(props: TemplateProps<KcContext, I18n>) {
2323
const { kcContext, i18n, doUseDefaultCss, active, classes, children } = props;
@@ -59,7 +59,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
5959
/>
6060
}
6161
homeLinkProps={{
62-
href: getReferrerUrl(),
62+
href: referrer?.url ?? "#",
6363
title: "Accueil"
6464
}}
6565
serviceTitle={
@@ -70,7 +70,6 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
7070
/>
7171
}
7272
quickAccessItems={[
73-
headerFooterDisplayItem,
7473
...(referrer?.url
7574
? [
7675
<a key="back-to-link" href={referrer.url} id="referrer" className={fr.cx("fr-btn", "fr-btn--tertiary-no-outline")}>

src/login/Template.tsx

Lines changed: 23 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@ import { Alert } from "@codegouvfr/react-dsfr/Alert";
1515
import { fr } from "@codegouvfr/react-dsfr";
1616
import "@codegouvfr/react-dsfr/main.css";
1717
import { startReactDsfr } from "@codegouvfr/react-dsfr/spa";
18+
import { getColorScheme } from "../shared/getColorScheme";
1819

19-
startReactDsfr({ defaultColorScheme: "system" });
20+
startReactDsfr({ defaultColorScheme: getColorScheme() ?? "system" });
2021

2122
type Props = TemplateProps<KcContext, I18n>;
2223

@@ -65,7 +66,27 @@ export default function Template(props: Props) {
6566

6667
return (
6768
<div style={{ minHeight: "100vh", display: "flex", flexDirection: "column" }}>
68-
<Header kcContext={kcContext} />
69+
<DsfrHeader
70+
brandTop={
71+
<div
72+
dangerouslySetInnerHTML={{
73+
__html: kcContext.properties.DSFR_THEME_BRAND_TOP
74+
}}
75+
/>
76+
}
77+
homeLinkProps={{
78+
href: getReferrerUrl(),
79+
title: kcContext.realm.displayName
80+
}}
81+
serviceTitle={
82+
<span
83+
dangerouslySetInnerHTML={{
84+
__html:
85+
kcContext.properties.DSFR_THEME_SERVICE_TITLE || kcContext.realm.displayNameHtml || kcContext.realm.displayName || ""
86+
}}
87+
/>
88+
}
89+
/>
6990
<div
7091
style={{
7192
flex: 1,
@@ -159,29 +180,3 @@ export default function Template(props: Props) {
159180
</div>
160181
);
161182
}
162-
163-
export function Header({ kcContext }: { kcContext: Props["kcContext"] }) {
164-
return (
165-
<DsfrHeader
166-
brandTop={
167-
<div
168-
dangerouslySetInnerHTML={{
169-
__html: kcContext.properties.DSFR_THEME_BRAND_TOP
170-
}}
171-
/>
172-
}
173-
homeLinkProps={{
174-
href: getReferrerUrl(),
175-
title: kcContext.realm.displayName
176-
}}
177-
quickAccessItems={[headerFooterDisplayItem]}
178-
serviceTitle={
179-
<span
180-
dangerouslySetInnerHTML={{
181-
__html: kcContext.properties.DSFR_THEME_SERVICE_TITLE || kcContext.realm.displayNameHtml || kcContext.realm.displayName || ""
182-
}}
183-
/>
184-
}
185-
/>
186-
);
187-
}

src/login/shared/getReferrerUrl.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ function getRefererUrlFromUrl() {
1111
return redirectUrl.origin;
1212
}
1313

14-
const sessionStorageKey = "theme_refererUrl";
14+
const sessionStorageKey = "keycloak-theme-dsfr:referrer";
1515

1616
export function getReferrerUrl() {
1717
const refererUrl = getRefererUrlFromUrl();

src/shared/getColorScheme.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
const SESSION_STORAGE_KEY = "keycloak-theme-dsfr:isDark";
2+
3+
function getIsDark(): boolean | undefined {
4+
from_url: {
5+
6+
const url = new URL(window.location.href);
7+
8+
const value = url.searchParams.get("dark");
9+
10+
if (value === null) {
11+
break from_url;
12+
}
13+
14+
{
15+
url.searchParams.delete("dark");
16+
window.history.replaceState({}, "", url.toString());
17+
}
18+
19+
const isDark = value === "true";
20+
21+
sessionStorage.setItem(SESSION_STORAGE_KEY, `${isDark}`);
22+
23+
return isDark;
24+
}
25+
26+
from_session_storage: {
27+
28+
const value = sessionStorage.getItem(SESSION_STORAGE_KEY);
29+
30+
if (value === null) {
31+
break from_session_storage;
32+
}
33+
34+
return value === "true";
35+
36+
}
37+
38+
return undefined;
39+
}
40+
41+
export function getColorScheme(){
42+
43+
const isDark = getIsDark();
44+
45+
if( isDark === undefined ) {
46+
return undefined;
47+
}
48+
49+
return isDark ? "dark" : "light";
50+
51+
}

0 commit comments

Comments
 (0)