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 all 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
2 changes: 1 addition & 1 deletion cypress/integration/sitemap-vrt/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ export const SITEMAP = [
"/patterns/notifications-and-feedback/",
"/patterns/pricing/",
"/patterns/privacy/",
"/patterns/status/",
"/patterns/object-status/",
"/primitives/combobox-primitive/",
"/patterns/create/",
"/primitives/",
Expand Down
2 changes: 2 additions & 0 deletions packages/paste-website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@
"airtable": "^0.11.6",
"color": "^3.1.2",
"common-tags": "^1.8.2",
"cookie": "^1.0.2",
"date-fns": "2.21.3",
"deepmerge": "4.2.2",
"dogapi": "^2.8.4",
Expand All @@ -175,6 +176,7 @@
"micromark-extension-mdxjs": "^2.0.0",
"minimist": "^1.2.8",
"next": "^14.0.0",
"nookies": "^2.5.2",
"openai": "^4.79.1",
"pretty-format": "^28.1.0",
"prism-react-renderer": "^1.3.5",
Expand Down
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
26 changes: 7 additions & 19 deletions packages/paste-website/src/hooks/useDarkMode.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
import type { ValueOf } from "@twilio-paste/types";
import * as React from "react";

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

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

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

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 type ValidThemeName = ValueOf<typeof ValidThemes>;

export const useDarkMode = (): UseDarkModeReturn => {
const [theme, setTheme] = React.useState<ValidThemeName>(ValidThemes.DEFAULT);
export const useDarkMode = (defaultValue: ValidThemeName): 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(null, themeCookieKey, mode);
};

const toggleTheme = (): void => {
Expand All @@ -34,16 +32,6 @@ export const useDarkMode = (): UseDarkModeReturn => {
};

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

if (window.matchMedia?.("(prefers-color-scheme: dark)").matches && !localTheme) {
setMode(ValidThemes.DARK);
} else if (localTheme && isValidTheme(localTheme)) {
setTheme(localTheme);
} else {
setMode(ValidThemes.DEFAULT);
}

setComponentMounted(true);
}, []);

Expand Down
55 changes: 37 additions & 18 deletions packages/paste-website/src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,36 @@
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";
import { parseCookies } from "nookies";
import * as React from "react";

import packageJSON from "../../../paste-core/core-bundle/package.json";
import { CookieConsent } from "../components/CookieConsent";
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 { ValidThemeName, 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 {
serverThemeCookie?: ValidThemeName;
}

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

React.useEffect(() => {
Expand Down Expand Up @@ -111,26 +119,37 @@ const App = ({ Component, pageProps }: AppProps): React.ReactElement => {
</>
)}
<Theme.Provider
theme={theme}
theme={serverThemeCookie || theme}
customBreakpoints={SITE_BREAKPOINTS}
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;
const responseCookie = context.ctx?.res?.getHeader(themeCookieKey);

if (cookies) {
const cookiestring = new RegExp(`${themeCookieKey}=[^;]+`).exec(cookies);
const decodedString = decodeURIComponent(cookiestring ? cookiestring.toString().replace(/^[^=]+./, "") : "");

return { ...ctx, serverThemeCookie: decodedString as ValidThemeName };
} else if (responseCookie) {
return { ...ctx, serverThemeCookie: responseCookie as ValidThemeName };
}

return { ...ctx };
};

export default App;
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ We can't thank Megan enough for her contributions. Her dedication to creating a
| **Navigation UI Kit components** | Complementary React components to implement our Navigation UI kit. |
| **Progress Steps component** | A highly requested component that shows a user a clear path to complete a complex multi-step task. |
| **Number Input component** | An input that limits data to number entry and has styled +/- buttons. |
| **Status components** | Components that implement the [Status Pattern](/patterns/status), including Status Badge and Status Menu. |
| **Status components** | Components that implement the [Object Status Pattern](/patterns/object-status), including Status Badge and Status Menu. |
| **Checkbox and Radio Menu Items** | A menu that presents a list of items that a user can choose to perform an action with. |
| **Design of the Slider component** | A component that allows a draggable input over a range of numbers. |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ Providing a common way for a user to control or edit their account, settings or

<ResponsiveImage src={StatusBadgeImage} alt="Preview of the Navigation UI Kit components" />

The [Status Badge](/components/status-badge) further evolves from our [Status Pattern](/patterns/status), allowing you to communicate status updates for any process or connection seamlessly. It provides an implementation of the Status Pattern using an appropriate combination of status level indicators, icons and colors.
The [Status Badge](/components/status-badge) further evolves from our [Object Status Pattern](/patterns/object-status), allowing you to communicate status updates for any process or connection seamlessly. It provides an implementation of the Status Pattern using an appropriate combination of status level indicators, icons and colors.

### [Status Menu](/components/status-menu)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ Use a basic Description List for conveying small, static bits of data.

### Description List with Icon

The Description List can be used with [Status Pattern](/patterns/status).
The Description List can be used with [Object Status Pattern](/patterns/object-status).

<LivePreview
scope={{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export const getStaticProps = async () => {

## About Status Badge

Status badge is an implementation of the [Status Pattern](/patterns/status) to display the status of a process or connectivity in your product.
Status badge is an implementation of the [Object Status Pattern](/patterns/object-status) to display the status of a process or connectivity in your product.

### Accessibility

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export const getStaticProps = async () => {

### About Status Menu

Use a Status Menu to both display and change the status of a connection or process. It closely follows the [Status Pattern](/patterns/status) and comes with preconfigured options for displaying the status of both processes and connections, using the correct icons for each.
Use a Status Menu to both display and change the status of a connection or process. It closely follows the [Object Status Pattern](/patterns/object-status) and comes with preconfigured options for displaying the status of both processes and connections, using the correct icons for each.

### Accessibility

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export const getStaticProps = async () => {
</Heading>
<Paragraph marginBottom="space0">
Create a composition with <Anchor href="/primitives/box">Box</Anchor>,{' '}
<Anchor href="/components/icon">Icon</Anchor>, and <Anchor href="/components/text">Text</Anchor>.
<Anchor href="/components/icon">Icon</Anchor>, and <Anchor href="/primitives/text">Text</Anchor>.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

For some reason the link test started failing, probably because it saw the 404 error now that we don't delay rendering

</Paragraph>
</Card>
</Column>
Expand Down
30 changes: 30 additions & 0 deletions packages/paste-website/src/utils/cookies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ParseOptions, parse } from "cookie";
import { NextPageContext } from "next";
import { parseCookies as parseRequestCookies } from "nookies";

export { setCookie, destroyCookie } from "nookies";

const cookieStringFromSetCookie = (cookies: string[] = []): string =>
cookies
.map((cookie) => {
const [keyValuePart] = cookie.split(";");
return keyValuePart;
})
.join("; ");

const parseResponseCookies = (ctx: NextPageContext, options?: ParseOptions): Record<string, any> => {
let responseCookies: Record<string, any> = {};
if (ctx?.res) {
const setCookieHeader = ctx.res.getHeader("Set-Cookie");
const cookie = cookieStringFromSetCookie(Array.isArray(setCookieHeader) ? setCookieHeader : []);
responseCookies = parse(cookie, options);
}

return responseCookies;
};

export const getCookie = (ctx: NextPageContext, name: string, options?: ParseOptions): string => {
const responseCookies = parseResponseCookies(ctx, options);
const requestCookies = parseRequestCookies(ctx);
return responseCookies[name] || requestCookies[name];
};
28 changes: 27 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -16289,6 +16289,7 @@ __metadata:
airtable: ^0.11.6
color: ^3.1.2
common-tags: ^1.8.2
cookie: ^1.0.2
date-fns: 2.21.3
deepmerge: 4.2.2
dogapi: ^2.8.4
Expand All @@ -16310,6 +16311,7 @@ __metadata:
micromark-extension-mdxjs: ^2.0.0
minimist: ^1.2.8
next: ^14.0.0
nookies: ^2.5.2
openai: ^4.79.1
pretty-format: ^28.1.0
prism-react-renderer: ^1.3.5
Expand Down Expand Up @@ -22377,13 +22379,20 @@ __metadata:
languageName: node
linkType: hard

"cookie@npm:~0.4.1":
"cookie@npm:^0.4.1, cookie@npm:~0.4.1":
version: 0.4.2
resolution: "cookie@npm:0.4.2"
checksum: a00833c998bedf8e787b4c342defe5fa419abd96b32f4464f718b91022586b8f1bafbddd499288e75c037642493c83083da426c6a9080d309e3bd90fd11baa9b
languageName: node
linkType: hard

"cookie@npm:^1.0.2":
version: 1.0.2
resolution: "cookie@npm:1.0.2"
checksum: 2c5a6214147ffa7135ce41860c781de17e93128689b0d080d3116468274b3593b607bcd462ac210d3a61f081db3d3b09ae106e18d60b1f529580e95cf2db8a55
languageName: node
linkType: hard

"copy-descriptor@npm:^0.1.0":
version: 0.1.1
resolution: "copy-descriptor@npm:0.1.1"
Expand Down Expand Up @@ -35769,6 +35778,16 @@ fsevents@^1.2.7:
languageName: node
linkType: hard

"nookies@npm:^2.5.2":
version: 2.5.2
resolution: "nookies@npm:2.5.2"
dependencies:
cookie: ^0.4.1
set-cookie-parser: ^2.4.6
checksum: 4cc6fd8d0ab9fce840ccdb161eefe9dee581f441429fc729f279924491ebd1c12c220f889b6e315d66f8ea00bef944966969c966c957a6b9918ced7d7f9cc8a8
languageName: node
linkType: hard

"nopt@npm:^4.0.3":
version: 4.0.3
resolution: "nopt@npm:4.0.3"
Expand Down Expand Up @@ -41673,6 +41692,13 @@ resolve@^2.0.0-next.3:
languageName: node
linkType: hard

"set-cookie-parser@npm:^2.4.6":
version: 2.7.1
resolution: "set-cookie-parser@npm:2.7.1"
checksum: 2ef8b351094712f8f7df6d63ed4b10350b24a5b515772690e7dec227df85fcef5bc451c7765f484fd9f36694ece5438d1456407d017f237d0d3351d7dbbd3587
languageName: node
linkType: hard

"set-function-length@npm:^1.1.1":
version: 1.1.1
resolution: "set-function-length@npm:1.1.1"
Expand Down
Loading