Skip to content

Fix dark mode flicker docs #4250

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 30 commits into from
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
67b66e1
Revert "Fix theme flash on paste website (#4025)"
PixeledCode Feb 19, 2025
b27d1ee
fix(docs): build failure for changing to ssr
krisantrobus Feb 19, 2025
27b1546
fix(docs): build failure for changing to ssr
krisantrobus Feb 19, 2025
95a21bf
fix(theme-flicker): use cookies to get and set a default theme
krisantrobus Feb 19, 2025
49d320b
fix(docs): update keyboard shortcut default
krisantrobus Feb 19, 2025
61772bc
fix(docs): add in better type check
krisantrobus Feb 19, 2025
936d03c
fix(docs-theme): remove local storage
krisantrobus Feb 19, 2025
ff6a92d
fix(ci): lint
krisantrobus Feb 19, 2025
0d609b2
fix(test): fix broken link
krisantrobus Feb 19, 2025
34d32e4
chore(docs): debug ssr app cookie capture
krisantrobus Feb 19, 2025
3526bf8
chore(docs): debug ssr app cookie capture
krisantrobus Feb 19, 2025
b55612c
chore(docs): debug ssr app cookie capture
krisantrobus Feb 19, 2025
35e52a4
chore(docs): debug ssr app cookie capture
krisantrobus Feb 19, 2025
7af23e2
chore(docs): debug ssr app cookie capture
krisantrobus Feb 19, 2025
9260386
chore(docs): debug ssr app cookie capture
krisantrobus Feb 19, 2025
927dece
chore(docs): debug ssr app cookie capture
krisantrobus Feb 19, 2025
4cb9ff7
chore(docs): debug ssr app cookie capture
krisantrobus Feb 19, 2025
1760cd0
chore(docs): debug ssr app cookie capture
krisantrobus Feb 19, 2025
19e0232
chore(docs): debug ssr app cookie capture
krisantrobus Feb 20, 2025
26c18ec
chore(docs): debug ssr app cookie capture
krisantrobus Feb 20, 2025
2ea9427
chore(docs): debug ssr app cookie capture
krisantrobus Feb 20, 2025
196faf0
chore(docs): debug ssr app cookie capture
krisantrobus Feb 20, 2025
0d5fe90
chore(docs): debug ssr app cookie capture
krisantrobus Feb 20, 2025
801a9ed
chore(docs): debug ssr app cookie capture
krisantrobus Feb 20, 2025
3aaa322
chore(docs): debug ssr app cookie capture
krisantrobus Feb 20, 2025
4eb27ee
chore(docs): test dark by default
krisantrobus Feb 20, 2025
752dd2d
chore(docs): test cookie theme only
krisantrobus Feb 20, 2025
8079b9f
chore(pr): cleanup
krisantrobus Feb 20, 2025
73e7cd1
fix(tests): incorrect page link
krisantrobus Feb 20, 2025
07a5a8f
fix(tests): incorrect page link
krisantrobus Feb 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,14 @@ import { SiteSearch } from "../../site-search";
const SiteHeaderSearch: React.FC = () => {
const [isOpen, setIsOpen] = React.useState(false);
const { breakpointIndex } = useWindowSize();
const isMacOS = navigator.platform.toUpperCase().includes("MAC");
// navigator is not available in SSR, settign a default until it renders to client
const [isMacOS, setIsMacOS] = React.useState<boolean>();
const platformTriggerKey = isMacOS ? "Meta" : "Control";

React.useEffect(() => {
setIsMacOS(typeof window !== "undefined" && navigator && navigator?.platform.toUpperCase().includes("MAC"));
}, []);
Comment on lines +18 to +20
Copy link
Collaborator Author

@krisantrobus krisantrobus Feb 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An error came up after removing the delayed rendering as navigator is only available running client side.


const onOpen = (): void => {
setIsOpen(true);
};
Expand Down Expand Up @@ -68,7 +73,7 @@ const SiteHeaderSearch: React.FC = () => {
Search
</Text>
</Box>
{breakpointIndex === 0 ? null : (
{breakpointIndex === 0 || isMacOS === undefined ? null : (
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not render the keyboard shortcuts in the search bar until we know what OS they are using

<>
<Box as="span" color="colorText" aria-hidden="true" marginLeft="space30" lineHeight="lineHeight20">
<KeyboardKeyGroup {...keyCombinationState}>
Expand Down
45 changes: 30 additions & 15 deletions packages/paste-website/src/hooks/useDarkMode.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,47 @@
import type { ValueOf } from "@twilio-paste/types";
import * as React from "react";

import { SimpleStorage } from "../utils/SimpleStorage";

export type UseDarkModeReturn = [ValidThemeName, () => void, boolean];

export const themeCookieKey = "paste-docs-theme";

const setCookie = (name: string, value: any, days: number): void => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot use the NextJS cookies tool are this does not run client side and I couldn't get it to work except this way or we add a new cookie dependency

let expires = "";
if (days) {
const date = new Date();
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
expires = `; expires=${date.toUTCString()}`;
}
// eslint-disable-next-line unicorn/no-document-cookie
document.cookie = `${name}=${value || ""}${expires}; path=/`;
};

const getCookie = (name: string) => {

Check failure on line 19 in packages/paste-website/src/hooks/useDarkMode.tsx

View workflow job for this annotation

GitHub Actions / Lint repository

Missing return type on function

Check failure on line 19 in packages/paste-website/src/hooks/useDarkMode.tsx

View workflow job for this annotation

GitHub Actions / Lint repository

Expected to return a value at the end of arrow function
// eslint-disable-next-line unicorn/no-document-cookie
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) {
const part = parts.pop();
if (part) {
return part.split(";").shift();
}
}
};

const ValidThemes = {
DEFAULT: "twilio",
DARK: "twilio-dark",
} as const;

type ValidThemeName = ValueOf<typeof ValidThemes>;

const isValidTheme = (themeName: ValidThemeName): boolean => {
return Object.values(ValidThemes).includes(themeName);
};

export const useDarkMode = (): UseDarkModeReturn => {
const [theme, setTheme] = React.useState<ValidThemeName>(ValidThemes.DEFAULT);
export const useDarkMode = (defaultValue: ValidThemeName = ValidThemes.DEFAULT): UseDarkModeReturn => {
const [theme, setTheme] = React.useState<ValidThemeName>(defaultValue);
const [componentMounted, setComponentMounted] = React.useState(false);

const setMode = (mode: ValidThemeName): void => {
SimpleStorage.set("theme", mode);
setTheme(mode);
setCookie(themeCookieKey, mode, 365);
};

const toggleTheme = (): void => {
Expand All @@ -34,14 +53,10 @@
};

React.useEffect(() => {
const localTheme = SimpleStorage.get("theme") as ValidThemeName;
const cookieTheme = getCookie(themeCookieKey) as ValidThemeName;

if (window.matchMedia?.("(prefers-color-scheme: dark)").matches && !localTheme) {
if (window.matchMedia?.("(prefers-color-scheme: dark)").matches && !cookieTheme) {
setMode(ValidThemes.DARK);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we are setting the default value from the cookie we don't need local storage anymore. We also don't want to use teh document cookie in this hook as we will still get the flickering issue. The reason this works now is that itis inside a useEffect hook and in functions so we know it would already be rendered in the client.

} else if (localTheme && isValidTheme(localTheme)) {
setTheme(localTheme);
} else {
setMode(ValidThemes.DEFAULT);
}

setComponentMounted(true);
Expand Down
46 changes: 30 additions & 16 deletions packages/paste-website/src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { datadogRum } from "@datadog/browser-rum";
import { Theme } from "@twilio-paste/theme";
import type { AppProps } from "next/app";
import type { AppContext, AppInitialProps, AppProps } from "next/app";
import NextApp from "next/app";
import Head from "next/head";
import { useRouter } from "next/router";
import Script from "next/script";
Expand All @@ -11,17 +12,21 @@
import { DATADOG_APPLICATION_ID, DATADOG_CLIENT_TOKEN, ENVIRONMENT_CONTEXT, SITE_BREAKPOINTS } from "../constants";
import { DarkModeContext } from "../context/DarkModeContext";
import { PreviewThemeContext } from "../context/PreviewThemeContext";
import { useDarkMode } from "../hooks/useDarkMode";
import { themeCookieKey, useDarkMode } from "../hooks/useDarkMode";
import * as gtag from "../lib/gtag";
import { SimpleStorage } from "../utils/SimpleStorage";
import { inCypress } from "../utils/inCypress";

const isProd = ENVIRONMENT_CONTEXT === "production";

const App = ({ Component, pageProps }: AppProps): React.ReactElement => {
interface AppPageProps {
themeCookie: any;
}

const App = ({ Component, pageProps, themeCookie }: AppProps & AppPageProps): React.ReactElement => {
const router = useRouter();
const localStorageKey = "cookie-consent-accepted";
const [theme, toggleMode, componentMounted] = useDarkMode();
const [theme, toggleMode, componentMounted] = useDarkMode(themeCookie);
const [previewTheme, setPreviewTheme] = React.useState("twilio");
const [cookiesAccepted, setCookiesAccepted] = React.useState<null | string>();

Expand Down Expand Up @@ -116,21 +121,30 @@
disableAnimations={inCypress()}
cacheProviderProps={{ key: "next" }}
>
{componentMounted ? (
<DarkModeContext.Provider value={{ theme, toggleMode, componentMounted }}>
<PreviewThemeContext.Provider value={{ theme: previewTheme, selectTheme: setPreviewTheme }}>
<Component {...pageProps} />
{cookiesAccepted === null && (
<CookieConsent onAccept={handleCookieAccept} onReject={handleCookieReject} />
)}
</PreviewThemeContext.Provider>
</DarkModeContext.Provider>
) : (
<></>
)}
<DarkModeContext.Provider value={{ theme, toggleMode, componentMounted }}>
<PreviewThemeContext.Provider value={{ theme: previewTheme, selectTheme: setPreviewTheme }}>
<Component {...pageProps} />
{cookiesAccepted === null && <CookieConsent onAccept={handleCookieAccept} onReject={handleCookieReject} />}
</PreviewThemeContext.Provider>
</DarkModeContext.Provider>
</Theme.Provider>
</>
);
};

App.getInitialProps = async (context: AppContext): Promise<AppPageProps & AppInitialProps> => {
const ctx = await NextApp.getInitialProps(context);

const cookies = context.ctx.req?.headers?.cookie;

if (!cookies) {
return { ...ctx, themeCookie: null };
}

const cookiestring = RegExp(`${themeCookieKey}=[^;]+`).exec(cookies);

Check failure on line 144 in packages/paste-website/src/pages/_app.tsx

View workflow job for this annotation

GitHub Actions / Lint repository

Use `new RegExp()` instead of `RegExp()`
const decodedString = decodeURIComponent(cookiestring ? cookiestring.toString().replace(/^[^=]+./, "") : "");

return { ...ctx, themeCookie: decodedString };
};

export default App;
Loading