From abdcad7316a5f3d02aba84dc5525d51ac2886308 Mon Sep 17 00:00:00 2001 From: Joshua Chen Date: Thu, 20 Jan 2022 14:51:18 +0800 Subject: [PATCH] feat: update website & init template palette to pass WCAG test; include contrast check in ColorGenerator (#5822) * docs: update website palette to pass WCAG test * Darker palette in light mode * Fix CodeBlock * Fix front page * Fix announcement color * Unify primary color * Add contrast check in website * Fix color input not updating * Use website for preview; allow changing background * Persist in localStorage * Fixes * Fix SSR * Edit dark mode separately * Fix light mode palette * Fix storage reset * Fix CSS * Fix * fix toggling when not on styling-layout * require 100% lighthouse accessibility score * use sessionStorage * refactor * tweak light color * update comments Co-authored-by: sebastienlorber --- .github/workflows/lighthousesrc.json | 5 + .../templates/classic/src/css/custom.css | 25 +- .../templates/facebook/src/css/custom.css | 25 +- website/docs/styling-layout.md | 4 - website/package.json | 1 + .../src/components/ColorGenerator/index.tsx | 232 +++++++++++------- .../ColorGenerator/styles.module.css | 14 ++ website/src/css/custom.css | 26 +- website/src/pages/styles.module.css | 8 + website/src/theme/Toggle.tsx | 39 +++ website/src/utils/colorUtils.ts | 108 ++++++++ 11 files changed, 378 insertions(+), 109 deletions(-) create mode 100644 website/src/theme/Toggle.tsx create mode 100644 website/src/utils/colorUtils.ts diff --git a/.github/workflows/lighthousesrc.json b/.github/workflows/lighthousesrc.json index 266bc66b49..68d1186a7c 100644 --- a/.github/workflows/lighthousesrc.json +++ b/.github/workflows/lighthousesrc.json @@ -1,5 +1,10 @@ { "ci": { + "assert": { + "assertions": { + "categories:accessibility": ["error", {"minScore": 1}] + } + }, "collect": { "settings": { "skipAudits": [ diff --git a/packages/create-docusaurus/templates/classic/src/css/custom.css b/packages/create-docusaurus/templates/classic/src/css/custom.css index 6abe148544..3247c4327c 100644 --- a/packages/create-docusaurus/templates/classic/src/css/custom.css +++ b/packages/create-docusaurus/templates/classic/src/css/custom.css @@ -6,16 +6,27 @@ /* You can override the default Infima variables here. */ :root { - --ifm-color-primary: #25c2a0; - --ifm-color-primary-dark: rgb(33, 175, 144); - --ifm-color-primary-darker: rgb(31, 165, 136); - --ifm-color-primary-darkest: rgb(26, 136, 112); - --ifm-color-primary-light: rgb(70, 203, 174); - --ifm-color-primary-lighter: rgb(102, 212, 189); - --ifm-color-primary-lightest: rgb(146, 224, 208); + --ifm-color-primary: #2e8555; + --ifm-color-primary-dark: #29784c; + --ifm-color-primary-darker: #277148; + --ifm-color-primary-darkest: #205d3b; + --ifm-color-primary-light: #33925d; + --ifm-color-primary-lighter: #359962; + --ifm-color-primary-lightest: #3cad6e; --ifm-code-font-size: 95%; } +/* For readability concerns, you should choose a lighter palette in dark mode. */ +html[data-theme='dark'] { + --ifm-color-primary: #25c2a0; + --ifm-color-primary-dark: #21af90; + --ifm-color-primary-darker: #1fa588; + --ifm-color-primary-darkest: #1a8870; + --ifm-color-primary-light: #29d5b0; + --ifm-color-primary-lighter: #32d8b4; + --ifm-color-primary-lightest: #4fddbf; +} + .docusaurus-highlight-code-line { background-color: rgba(0, 0, 0, 0.1); display: block; diff --git a/packages/create-docusaurus/templates/facebook/src/css/custom.css b/packages/create-docusaurus/templates/facebook/src/css/custom.css index 3fcaec3bc4..c204c4bbea 100644 --- a/packages/create-docusaurus/templates/facebook/src/css/custom.css +++ b/packages/create-docusaurus/templates/facebook/src/css/custom.css @@ -15,16 +15,27 @@ /* You can override the default Infima variables here. */ :root { - --ifm-color-primary: #25c2a0; - --ifm-color-primary-dark: rgb(33, 175, 144); - --ifm-color-primary-darker: rgb(31, 165, 136); - --ifm-color-primary-darkest: rgb(26, 136, 112); - --ifm-color-primary-light: rgb(70, 203, 174); - --ifm-color-primary-lighter: rgb(102, 212, 189); - --ifm-color-primary-lightest: rgb(146, 224, 208); + --ifm-color-primary: #2e8555; + --ifm-color-primary-dark: #29784c; + --ifm-color-primary-darker: #277148; + --ifm-color-primary-darkest: #205d3b; + --ifm-color-primary-light: #33925d; + --ifm-color-primary-lighter: #359962; + --ifm-color-primary-lightest: #3cad6e; --ifm-code-font-size: 95%; } +/* For readability concerns, you should choose a lighter palette in dark mode. */ +html[data-theme='dark'] { + --ifm-color-primary: #25c2a0; + --ifm-color-primary-dark: #21af90; + --ifm-color-primary-darker: #1fa588; + --ifm-color-primary-darkest: #1a8870; + --ifm-color-primary-light: #29d5b0; + --ifm-color-primary-lighter: #32d8b4; + --ifm-color-primary-lightest: #4fddbf; +} + .docusaurus-highlight-code-line { background-color: rgb(72, 77, 91); display: block; diff --git a/website/docs/styling-layout.md b/website/docs/styling-layout.md index 2980765d84..f3e927f7c1 100644 --- a/website/docs/styling-layout.md +++ b/website/docs/styling-layout.md @@ -94,10 +94,6 @@ import CodeBlock from '@theme/CodeBlock'; When you scaffold your Docusaurus project with `create-docusaurus`, the website will be generated with basic Infima stylesheets and default styling. You can override Infima CSS variables globally. ```css title="/src/css/custom.css" -/** - * You can override the default Infima variables here. - * Note: this is not a complete list of --ifm- variables. - */ :root { --ifm-color-primary: #25c2a0; --ifm-code-font-size: 95%; diff --git a/website/package.json b/website/package.json index 3393c476bf..4895535aae 100644 --- a/website/package.json +++ b/website/package.json @@ -38,6 +38,7 @@ "@docusaurus/plugin-pwa": "2.0.0-beta.14", "@docusaurus/preset-classic": "2.0.0-beta.14", "@docusaurus/remark-plugin-npm2yarn": "2.0.0-beta.14", + "@docusaurus/theme-common": "2.0.0-beta.14", "@docusaurus/theme-live-codeblock": "2.0.0-beta.14", "@docusaurus/utils": "2.0.0-beta.14", "@popperjs/core": "^2.10.2", diff --git a/website/src/components/ColorGenerator/index.tsx b/website/src/components/ColorGenerator/index.tsx index 14a0d8e5eb..f12be2f770 100644 --- a/website/src/components/ColorGenerator/index.tsx +++ b/website/src/components/ColorGenerator/index.tsx @@ -5,83 +5,97 @@ * LICENSE file in the root directory of this source tree. */ -import React, {useState} from 'react'; +import React, {useEffect, useState} from 'react'; +import clsx from 'clsx'; import Color from 'color'; import CodeBlock from '@theme/CodeBlock'; +import Admonition from '@theme/Admonition'; +import Link from '@docusaurus/Link'; +import {useColorMode} from '@docusaurus/theme-common'; +import { + type ColorState, + COLOR_SHADES, + LIGHT_PRIMARY_COLOR, + DARK_PRIMARY_COLOR, + LIGHT_BACKGROUND_COLOR, + DARK_BACKGROUND_COLOR, + getAdjustedColors, + lightStorage, + darkStorage, + updateDOMColors, +} from '@site/src/utils/colorUtils'; import styles from './styles.module.css'; -const COLOR_SHADES: Record< - string, - { - adjustment: number; - adjustmentInput: string; - displayOrder: number; - codeOrder: number; - } -> = { - '--ifm-color-primary': { - adjustment: 0, - adjustmentInput: '0', - displayOrder: 3, - codeOrder: 0, - }, - '--ifm-color-primary-dark': { - adjustment: 0.1, - adjustmentInput: '10', - displayOrder: 4, - codeOrder: 1, - }, - '--ifm-color-primary-darker': { - adjustment: 0.15, - adjustmentInput: '15', - displayOrder: 5, - codeOrder: 2, - }, - '--ifm-color-primary-darkest': { - adjustment: 0.3, - adjustmentInput: '30', - displayOrder: 6, - codeOrder: 3, - }, - '--ifm-color-primary-light': { - adjustment: -0.1, - adjustmentInput: '-10', - displayOrder: 2, - codeOrder: 4, - }, - '--ifm-color-primary-lighter': { - adjustment: -0.15, - adjustmentInput: '-15', - displayOrder: 1, - codeOrder: 5, - }, - '--ifm-color-primary-lightest': { - adjustment: -0.3, - adjustmentInput: '-30', - displayOrder: 0, - codeOrder: 6, - }, -}; - -const DEFAULT_PRIMARY_COLOR = '3578e5'; +function wcagContrast(foreground: string, background: string) { + const contrast = Color(foreground).contrast(Color(background)); + // eslint-disable-next-line no-nested-ternary + return contrast > 7 ? 'AAA 🏅' : contrast > 4.5 ? 'AA 👍' : 'Fail 🔴'; +} function ColorGenerator(): JSX.Element { + const {isDarkTheme, setDarkTheme, setLightTheme} = useColorMode(); + const DEFAULT_PRIMARY_COLOR = isDarkTheme + ? DARK_PRIMARY_COLOR + : LIGHT_PRIMARY_COLOR; + const DEFAULT_BACKGROUND_COLOR = isDarkTheme + ? DARK_BACKGROUND_COLOR + : LIGHT_BACKGROUND_COLOR; + + const [inputColor, setInputColor] = useState(DEFAULT_PRIMARY_COLOR); const [baseColor, setBaseColor] = useState(DEFAULT_PRIMARY_COLOR); + const [background, setBackground] = useState(DEFAULT_BACKGROUND_COLOR); const [shades, setShades] = useState(COLOR_SHADES); - const color = Color(`#${baseColor}`); - const adjustedColors = Object.keys(shades) - .map((shade) => ({ - ...shades[shade], - variableName: shade, - })) - .map((value) => ({ - ...value, - hex: color.darken(value.adjustment).hex(), - })); + const [storage, setStorage] = useState( + isDarkTheme ? darkStorage : lightStorage, + ); + + useEffect(() => { + setStorage(isDarkTheme ? darkStorage : lightStorage); + }, [isDarkTheme]); + + // Switch modes -> update state by stored values + useEffect(() => { + const storedValues: ColorState = JSON.parse(storage.get() ?? '{}'); + setInputColor(storedValues.baseColor ?? DEFAULT_PRIMARY_COLOR); + setBaseColor(storedValues.baseColor ?? DEFAULT_PRIMARY_COLOR); + setBackground(storedValues.background ?? DEFAULT_BACKGROUND_COLOR); + setShades(storedValues.shades ?? COLOR_SHADES); + }, [storage, DEFAULT_BACKGROUND_COLOR, DEFAULT_PRIMARY_COLOR]); + + // State changes -> update DOM styles + useEffect(() => { + updateDOMColors({baseColor, background, shades}); + storage.set(JSON.stringify({baseColor, background, shades})); + }, [baseColor, background, shades, storage]); + + function updateColor(event: React.ChangeEvent) { + // Only prepend # when there isn't one. + // e.g. ccc -> #ccc, #ccc -> #ccc, ##ccc -> ##ccc, + const colorValue = event.target.value.replace(/^(?=[^#])/, '#'); + setInputColor(colorValue); + + try { + setBaseColor(Color(colorValue).hex()); + } catch { + // Don't update for invalid colors. + } + } return (
+ +

+ Aim for at least{' '} + + WCAG-AA contrast ratio + {' '} + for the primary color to ensure readability. Use the Docusaurus + website itself to preview how your color palette would look like. You + can use alternative palettes in dark mode because one color + doesn't usually work in both light and dark mode. +

+

{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} {' '} { - // Replace all the prefix '#' with an empty string. - // For example, '#ccc' -> 'ccc', '##ccc' -> 'ccc' - const colorValue = event.target.value.replace(/^#+/, ''); - - try { - Color(`#${colorValue}`); - setBaseColor(colorValue); - } catch { - // Don't update for invalid colors. + type="text" + className={clsx(styles.input, 'margin-right--sm')} + value={inputColor} + onChange={updateColor} + /> + + + +

+

+ {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} + + { + setBackground(e.target.value); }} />

- +
+ - {adjustedColors + {getAdjustedColors(shades, baseColor) .sort((a, b) => a.displayOrder - b.displayOrder) .map((value) => { const {variableName, adjustment, adjustmentInput, hex} = value; @@ -160,6 +210,14 @@ function ColorGenerator(): JSX.Element { /> )} + ); })} @@ -170,11 +228,17 @@ function ColorGenerator(): JSX.Element { Replace the variables in src/css/custom.css with these new variables.

- - {adjustedColors - .sort((a, b) => a.codeOrder - b.codeOrder) - .map((value) => `${value.variableName}: ${value.hex.toLowerCase()};`) - .join('\n')} + + {`${isDarkTheme ? "html[data-theme='dark']" : ':root'} { +${getAdjustedColors(shades, baseColor) + .sort((a, b) => a.codeOrder - b.codeOrder) + .map((value) => ` ${value.variableName}: ${value.hex.toLowerCase()};`) + .join('\n')}${ + background !== DEFAULT_BACKGROUND_COLOR + ? `\n --ifm-background-color: ${background};` + : '' + } +}`} ); diff --git a/website/src/components/ColorGenerator/styles.module.css b/website/src/components/ColorGenerator/styles.module.css index f657cea47c..2baf569616 100644 --- a/website/src/components/ColorGenerator/styles.module.css +++ b/website/src/components/ColorGenerator/styles.module.css @@ -21,3 +21,17 @@ font-size: var(--ifm-font-size-base); padding: 0.5rem; } + +.colorInput { + position: relative; + border-color: var(--ifm-color-content-secondary); + border-radius: var(--ifm-global-radius); + border-style: solid; + border-width: var(--ifm-global-border-width); + height: 2.25rem; + top: 7px; +} + +.colorTable { + font-size: small; +} diff --git a/website/src/css/custom.css b/website/src/css/custom.css index 9198eca45a..d61ca4879e 100644 --- a/website/src/css/custom.css +++ b/website/src/css/custom.css @@ -13,6 +13,25 @@ --site-primary-hue-saturation: 167, 68%; --site-primary-hue-saturation-light: 167, 56%; /* do we really need this extra one? */ + --ifm-color-primary: hsl(var(--site-primary-hue-saturation), 30%); + --ifm-color-primary-dark: hsl(var(--site-primary-hue-saturation), 26%); + --ifm-color-primary-darker: hsl(var(--site-primary-hue-saturation), 23%); + --ifm-color-primary-darkest: hsl(var(--site-primary-hue-saturation), 17%); + + --ifm-color-primary-light: hsl(var(--site-primary-hue-saturation-light), 39%); + --ifm-color-primary-lighter: hsl( + var(--site-primary-hue-saturation-light), + 47% + ); + --ifm-color-primary-lightest: hsl( + var(--site-primary-hue-saturation-light), + 58% + ); + + --ifm-color-feedback-background: #fff; +} + +html[data-theme='dark'] { --ifm-color-primary: hsl(var(--site-primary-hue-saturation), 45%); --ifm-color-primary-dark: hsl(var(--site-primary-hue-saturation), 41%); --ifm-color-primary-darker: hsl(var(--site-primary-hue-saturation), 38%); @@ -27,16 +46,9 @@ var(--site-primary-hue-saturation-light), 73% ); - - --site-color-feedback-background: #fff; - --site-color-favorite-background: #f6fdfd; --site-color-tooltip: #fff; --site-color-tooltip-background: #353738; --site-color-svgIcon-favorite: #e9669e; - --site-color-checkbox-checked-bg: hsl(167deg 56% 73% / 25%); -} - -html[data-theme='dark'] { --site-color-feedback-background: #f0f8ff; --site-color-favorite-background: #1d1e1e; --site-color-checkbox-checked-bg: hsl(167deg 56% 73% / 10%); diff --git a/website/src/pages/styles.module.css b/website/src/pages/styles.module.css index ea097ee149..9df7892b17 100644 --- a/website/src/pages/styles.module.css +++ b/website/src/pages/styles.module.css @@ -39,6 +39,7 @@ .announcementDark { background-color: #20232a; color: #fff; + --ifm-link-color: hsl(var(--site-primary-hue-saturation), 45%); } .announcementInner { @@ -49,6 +50,8 @@ .hero { background-color: #2b3137; padding: 48px; + --ifm-color-primary: hsl(var(--site-primary-hue-saturation), 45%); + --ifm-color-primary-dark: hsl(var(--site-primary-hue-saturation), 41%); } .heroInner { @@ -104,6 +107,11 @@ margin-top: 24px; } +.indexCtas a, +.indexCtas a:hover { + color: black; +} + .indexCtas a:last-of-type { margin: 20px 36px; } diff --git a/website/src/theme/Toggle.tsx b/website/src/theme/Toggle.tsx new file mode 100644 index 0000000000..9084521e77 --- /dev/null +++ b/website/src/theme/Toggle.tsx @@ -0,0 +1,39 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import OriginalToggle from '@theme-original/Toggle'; +import type {Props} from '@theme/Toggle'; +import { + lightStorage, + darkStorage, + type ColorState, + updateDOMColors, +} from '@site/src/utils/colorUtils'; + +// The ColorGenerator modifies the DOM styles. The styles are persisted in +// session storage, and we need to apply the same style when toggling modes even +// when we are not on the styling-layout page. The only way to do this so far is +// by hooking into the Toggle component. +export default function Toggle(props: Props): JSX.Element { + return ( + { + props.onChange(e); + const isDarkMode = e.target.checked; + const storage = isDarkMode ? darkStorage : lightStorage; + const colorState = JSON.parse( + storage.get() ?? 'null', + ) as ColorState | null; + if (colorState) { + updateDOMColors(colorState); + } + }} + /> + ); +} diff --git a/website/src/utils/colorUtils.ts b/website/src/utils/colorUtils.ts new file mode 100644 index 0000000000..ecb2aff34b --- /dev/null +++ b/website/src/utils/colorUtils.ts @@ -0,0 +1,108 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import Color from 'color'; +import {createStorageSlot} from '@docusaurus/theme-common'; + +// These values are shared between the Toggle component and the ColorGenerator + +/** + * Stored in session storage + */ +export type ColorState = { + baseColor: string; + background: string; + shades: Shades; +}; + +export type Shades = Record< + string, + { + adjustment: number; + adjustmentInput: string; + displayOrder: number; + codeOrder: number; + } +>; +export const COLOR_SHADES: Shades = { + '--ifm-color-primary': { + adjustment: 0, + adjustmentInput: '0', + displayOrder: 3, + codeOrder: 0, + }, + '--ifm-color-primary-dark': { + adjustment: 0.1, + adjustmentInput: '10', + displayOrder: 4, + codeOrder: 1, + }, + '--ifm-color-primary-darker': { + adjustment: 0.15, + adjustmentInput: '15', + displayOrder: 5, + codeOrder: 2, + }, + '--ifm-color-primary-darkest': { + adjustment: 0.3, + adjustmentInput: '30', + displayOrder: 6, + codeOrder: 3, + }, + '--ifm-color-primary-light': { + adjustment: -0.1, + adjustmentInput: '-10', + displayOrder: 2, + codeOrder: 4, + }, + '--ifm-color-primary-lighter': { + adjustment: -0.15, + adjustmentInput: '-15', + displayOrder: 1, + codeOrder: 5, + }, + '--ifm-color-primary-lightest': { + adjustment: -0.3, + adjustmentInput: '-30', + displayOrder: 0, + codeOrder: 6, + }, +}; + +export const LIGHT_PRIMARY_COLOR = '#2e8555'; +export const DARK_PRIMARY_COLOR = '#25c2a0'; +export const LIGHT_BACKGROUND_COLOR = '#ffffff'; +export const DARK_BACKGROUND_COLOR = '#181920'; + +// sessionStorage allows resetting everything next time users visit the site +export const lightStorage = createStorageSlot('ifm-theme-colors-light', { + persistence: 'sessionStorage', +}); +export const darkStorage = createStorageSlot('ifm-theme-colors-dark', { + persistence: 'sessionStorage', +}); + +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export function getAdjustedColors(shades: Shades, baseColor: string) { + return Object.keys(shades).map((shade) => ({ + ...shades[shade], + variableName: shade, + hex: Color(baseColor).darken(shades[shade].adjustment).hex(), + })); +} + +export function updateDOMColors({ + shades, + baseColor, + background, +}: ColorState): void { + const root = document.documentElement; + getAdjustedColors(shades, baseColor).forEach((value) => { + root.style.setProperty(value.variableName, value.hex); + }); + root.style.setProperty('--ifm-background-color', background); +}
CSS Variable Name Hex AdjustmentContrast Rating
+ {wcagContrast(hex, background)} +