diff --git a/assets/images/help/repository/repository-name-change.png b/assets/images/help/repository/repository-name-change.png index 8deb370992..f828f77b27 100644 Binary files a/assets/images/help/repository/repository-name-change.png and b/assets/images/help/repository/repository-name-change.png differ diff --git a/components/hooks/useSession.ts b/components/hooks/useSession.ts index f9119798a1..7bbbfaaca9 100644 --- a/components/hooks/useSession.ts +++ b/components/hooks/useSession.ts @@ -1,4 +1,3 @@ -import type { ThemeProviderProps } from '@primer/react' import { useEffect } from 'react' import useSWR from 'swr' @@ -14,16 +13,6 @@ export type Session = { isSignedIn: boolean csrfToken?: string userLanguage: string // en, es, ja, cn - theme: { - colorMode: Pick - nightTheme: string - dayTheme: string - } - themeCss: { - colorMode: Pick - nightTheme: string - dayTheme: string - } } // React hook version diff --git a/components/hooks/useTheme.ts b/components/hooks/useTheme.ts new file mode 100644 index 0000000000..b8134a0e57 --- /dev/null +++ b/components/hooks/useTheme.ts @@ -0,0 +1,114 @@ +import { useState, useEffect } from 'react' +import Cookies from 'js-cookie' + +enum CssColorMode { + auto = 'auto', + light = 'light', + dark = 'dark', +} + +enum ComponentColorMode { + auto = 'auto', + day = 'day', + night = 'night', +} + +enum SupportedTheme { + light = 'light', + dark = 'dark', + dark_dimmed = 'dark_dimmed', + dark_high_contrast = 'dark_high_contrast', +} + +type CssColorTheme = { + colorMode: CssColorMode + lightTheme: SupportedTheme + darkTheme: SupportedTheme +} + +type ComponentColorTheme = { + colorMode: ComponentColorMode + dayScheme: SupportedTheme + nightScheme: SupportedTheme +} + +type ColorModeThemes = { + css: CssColorTheme + component: ComponentColorTheme +} + +export const defaultCSSTheme: CssColorTheme = { + colorMode: CssColorMode.auto, + lightTheme: SupportedTheme.light, + darkTheme: SupportedTheme.dark, +} + +export const defaultComponentTheme: ComponentColorTheme = { + colorMode: ComponentColorMode.auto, + dayScheme: SupportedTheme.light, + nightScheme: SupportedTheme.dark, +} + +const cssColorModeToComponentColorMode: Record = { + [CssColorMode.auto]: ComponentColorMode.auto, + [CssColorMode.light]: ComponentColorMode.day, + [CssColorMode.dark]: ComponentColorMode.night, +} + +function filterMode(mode = ''): CssColorMode | undefined { + if (Object.values(CssColorMode).includes(mode)) { + return mode as CssColorMode + } +} + +function filterTheme({ name = '', color_mode = '' } = {}): SupportedTheme | undefined { + if (Object.values(SupportedTheme).includes(name)) { + return name as SupportedTheme + } + if (Object.values(SupportedTheme).includes(color_mode)) { + return color_mode as SupportedTheme + } +} + +export function getCssTheme(cookieValue = ''): CssColorTheme { + if (!cookieValue) return defaultCSSTheme + try { + const parsed = JSON.parse(cookieValue) + const { color_mode, light_theme, dark_theme } = parsed + return { + colorMode: filterMode(color_mode) || defaultCSSTheme.colorMode, + lightTheme: filterTheme(light_theme) || defaultCSSTheme.lightTheme, + darkTheme: filterTheme(dark_theme) || defaultCSSTheme.darkTheme, + } + } catch (err) { + console.warn("Unable to parse 'color_mode' cookie", err) + return defaultCSSTheme + } +} + +export function getComponentTheme(cookieValue = ''): ComponentColorTheme { + const { colorMode, lightTheme, darkTheme } = getCssTheme(cookieValue) + return { + // The cookie value is a primer/css color_mode. + // We need to convert that to a primer/react compatible version. + colorMode: cssColorModeToComponentColorMode[colorMode], + dayScheme: lightTheme, + nightScheme: darkTheme, + } +} + +export function useTheme() { + const [theme, setTheme] = useState({ + css: defaultCSSTheme, + component: defaultComponentTheme, + }) + + useEffect(() => { + const cookieValue = Cookies.get('color_mode') + const css = getCssTheme(cookieValue) + const component = getComponentTheme(cookieValue) + setTheme({ css, component }) + }, []) + + return { theme } +} diff --git a/lib/get-theme.js b/lib/get-theme.js deleted file mode 100644 index a4b9b964dc..0000000000 --- a/lib/get-theme.js +++ /dev/null @@ -1,66 +0,0 @@ -export const defaultCSSTheme = { - colorMode: 'auto', // light, dark, auto - nightTheme: 'dark', - dayTheme: 'light', -} - -export const defaultComponentTheme = { - colorMode: 'auto', // day, night, auto - nightTheme: 'dark', - dayTheme: 'light', -} - -const cssColorModeToComponentColorMode = { - auto: 'auto', - light: 'day', - dark: 'night', -} - -const supportedThemes = ['light', 'dark', 'dark_dimmed'] - -/* - * Our version of primer/css is out of date, so we can only support known themes. - * For the least jarring experience possible, we fallback to the color_mode (light / dark) if provided by the theme, otherwise our own defaults - */ -function getSupportedTheme(theme, fallbackTheme) { - if (!theme) { - return fallbackTheme - } - - return supportedThemes.includes(theme.name) ? theme.name : theme.color_mode -} - -/* - * Returns theme for consumption by either primer/css or primer/components - * based on the cookie and/or fallback values - */ -export function getTheme(req, cssMode = false) { - const cookieValue = {} - - const defaultTheme = cssMode ? defaultCSSTheme : defaultComponentTheme - - if (req.cookies?.color_mode) { - try { - const parsed = JSON.parse(decodeURIComponent(req.cookies.color_mode)) - cookieValue.color_mode = parsed.color_mode - cookieValue.dark_theme = parsed.dark_theme - cookieValue.light_theme = parsed.light_theme - } catch (err) { - if (process.env.NODE_ENV !== 'test') { - console.warn("Unable to parse 'color_mode' cookie", err) - } - } - } - - // The cookie value is a primer/css color_mode. sometimes we need to convert that to a primer/components compatible version - const colorMode = - (cssMode - ? cookieValue.color_mode - : cssColorModeToComponentColorMode[cookieValue.color_mode || '']) || defaultTheme.colorMode - - return { - colorMode, - nightTheme: getSupportedTheme(cookieValue.dark_theme, defaultTheme.nightTheme), - dayTheme: getSupportedTheme(cookieValue.light_theme, defaultTheme.dayTheme), - } -} diff --git a/middleware/api/session.js b/middleware/api/session.js index c6d5e65ea8..4fe5d86f5c 100644 --- a/middleware/api/session.js +++ b/middleware/api/session.js @@ -1,5 +1,4 @@ import express from 'express' -import { getTheme } from '../../lib/get-theme.js' import { cacheControlFactory } from '../cache-control.js' const router = express.Router() @@ -11,8 +10,6 @@ router.get('/', (req, res) => { isSignedIn: Boolean(req.cookies?.dotcom_user), csrfToken: req.csrfToken?.() || '', userLanguage: req.userLanguage, - theme: getTheme(req), - themeCss: getTheme(req, true), }) }) diff --git a/pages/_app.tsx b/pages/_app.tsx index 24d691f143..bb9a4597b7 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -2,7 +2,7 @@ import React, { useEffect } from 'react' import App from 'next/app' import type { AppProps, AppContext } from 'next/app' import Head from 'next/head' -import { ThemeProvider, SSRProvider, ThemeProviderProps } from '@primer/react' +import { ThemeProvider, SSRProvider } from '@primer/react' import '../stylesheets/index.scss' @@ -10,16 +10,17 @@ import { initializeEvents } from 'components/lib/events' import experiment from 'components/lib/experiment' import { LanguagesContext, LanguagesContextT } from 'components/context/LanguagesContext' import { useSession } from 'components/hooks/useSession' +import { useTheme } from 'components/hooks/useTheme' type MyAppProps = AppProps & { isDotComAuthenticated: boolean languagesContext: LanguagesContextT } -type colorModeAuto = Pick - const MyApp = ({ Component, pageProps, languagesContext }: MyAppProps) => { const { session } = useSession() + const { theme } = useTheme() + useEffect(() => { if (session?.csrfToken) { initializeEvents(session.csrfToken) @@ -54,18 +55,15 @@ const MyApp = ({ Component, pageProps, languagesContext }: MyAppProps) => { {/* Appears Next.js can't modify after server rendering: https://stackoverflow.com/a/54774431 */}
diff --git a/stylesheets/index.scss b/stylesheets/index.scss index a4f3e1b7b7..d4f295dc41 100644 --- a/stylesheets/index.scss +++ b/stylesheets/index.scss @@ -13,9 +13,3 @@ @import "shadows.scss"; @import "syntax-highlighting.scss"; @import "utilities.scss"; - -// render a mostly gray background until we know the color mode via XHR -html, -body { - background: #6e7781; -} diff --git a/tests/unit/get-theme.js b/tests/unit/get-theme.js deleted file mode 100644 index aba0a20bcb..0000000000 --- a/tests/unit/get-theme.js +++ /dev/null @@ -1,80 +0,0 @@ -import { describe, expect, test } from '@jest/globals' - -import { getTheme, defaultCSSTheme, defaultComponentTheme } from '../../lib/get-theme.js' - -function serializeCookieValue(obj) { - return encodeURIComponent(JSON.stringify(obj)) -} - -describe('getTheme basics', () => { - test('always return an object with certain keys', () => { - const req = {} // doesn't even have a `.cookies`. - const theme = getTheme(req) - expect(theme.colorMode).toBe(defaultComponentTheme.colorMode) - expect(theme.nightTheme).toBe(defaultComponentTheme.nightTheme) - expect(theme.dayTheme).toBe(defaultComponentTheme.dayTheme) - const cssTheme = getTheme(req, true) - expect(cssTheme.colorMode).toBe(defaultCSSTheme.colorMode) - expect(cssTheme.nightTheme).toBe(defaultCSSTheme.nightTheme) - expect(cssTheme.dayTheme).toBe(defaultCSSTheme.dayTheme) - }) - - test('respect the color_mode cookie value', () => { - const req = { - cookies: { - color_mode: serializeCookieValue({ - color_mode: 'dark', - light_theme: { name: 'light_colorblind', color_mode: 'light' }, - dark_theme: { name: 'dark_tritanopia', color_mode: 'dark' }, - }), - }, - } - const theme = getTheme(req) - expect(theme.colorMode).toBe('night') - expect(theme.nightTheme).toBe(defaultComponentTheme.nightTheme) - expect(theme.dayTheme).toBe(defaultComponentTheme.dayTheme) - - const cssTheme = getTheme(req, true) - expect(cssTheme.colorMode).toBe('dark') - expect(cssTheme.nightTheme).toBe(defaultCSSTheme.nightTheme) - expect(cssTheme.dayTheme).toBe(defaultCSSTheme.dayTheme) - }) - - test('respect the color_mode cookie value', () => { - const req = { - cookies: { - color_mode: serializeCookieValue({ - color_mode: 'dark', - light_theme: { name: 'light_colorblind', color_mode: 'light' }, - dark_theme: { name: 'dark_tritanopia', color_mode: 'dark' }, - }), - }, - } - const theme = getTheme(req) - expect(theme.colorMode).toBe('night') - expect(theme.nightTheme).toBe(defaultComponentTheme.nightTheme) - expect(theme.dayTheme).toBe(defaultComponentTheme.dayTheme) - - const cssTheme = getTheme(req, true) - expect(cssTheme.colorMode).toBe('dark') - expect(cssTheme.nightTheme).toBe(defaultCSSTheme.nightTheme) - expect(cssTheme.dayTheme).toBe(defaultCSSTheme.dayTheme) - }) - - test('ignore "junk" cookie values', () => { - const req = { - cookies: { - color_mode: '[This is not valid JSON}', - }, - } - const theme = getTheme(req) - expect(theme.colorMode).toBe('auto') - expect(theme.nightTheme).toBe(defaultComponentTheme.nightTheme) - expect(theme.dayTheme).toBe(defaultComponentTheme.dayTheme) - - const cssTheme = getTheme(req, true) - expect(cssTheme.colorMode).toBe('auto') - expect(cssTheme.nightTheme).toBe(defaultCSSTheme.nightTheme) - expect(cssTheme.dayTheme).toBe(defaultCSSTheme.dayTheme) - }) -}) diff --git a/tests/unit/use-theme.js b/tests/unit/use-theme.js new file mode 100644 index 0000000000..76b050c36d --- /dev/null +++ b/tests/unit/use-theme.js @@ -0,0 +1,39 @@ +import { describe, expect, test } from '@jest/globals' +import { + getComponentTheme, + getCssTheme, + defaultCSSTheme, + defaultComponentTheme, +} from '../../components/hooks/useTheme.ts' + +describe('getTheme basics', () => { + test('always return an object with certain keys', () => { + const cookieValue = JSON.stringify({}) + expect(getCssTheme(cookieValue)).toEqual(defaultCSSTheme) + expect(getComponentTheme(cookieValue)).toEqual(defaultComponentTheme) + }) + + test('ignore "junk" cookie values', () => { + const cookieValue = '[This is not valid JSON}' + expect(getCssTheme(cookieValue)).toEqual(defaultCSSTheme) + expect(getComponentTheme(cookieValue)).toEqual(defaultComponentTheme) + }) + + test('respect the color_mode cookie value', () => { + const cookieValue = JSON.stringify({ + color_mode: 'dark', + light_theme: { name: 'light_colorblind', color_mode: 'light' }, + dark_theme: { name: 'dark_tritanopia', color_mode: 'dark' }, + }) + + const cssTheme = getCssTheme(cookieValue) + expect(cssTheme.colorMode).toBe('dark') + expect(cssTheme.darkTheme).toBe(defaultCSSTheme.darkTheme) + expect(cssTheme.lightTheme).toBe(defaultCSSTheme.lightTheme) + + const componentTheme = getComponentTheme(cookieValue) + expect(componentTheme.colorMode).toBe('night') + expect(componentTheme.nightScheme).toBe(defaultComponentTheme.nightScheme) + expect(componentTheme.dayScheme).toBe(defaultComponentTheme.dayScheme) + }) +})