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 <lorber.sebastien@gmail.com>
This commit is contained in:
Joshua Chen 2022-01-20 14:51:18 +08:00 committed by GitHub
parent 19fb337618
commit abdcad7316
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 378 additions and 109 deletions

View file

@ -1,5 +1,10 @@
{
"ci": {
"assert": {
"assertions": {
"categories:accessibility": ["error", {"minScore": 1}]
}
},
"collect": {
"settings": {
"skipAudits": [

View file

@ -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;

View file

@ -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;

View file

@ -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%;

View file

@ -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",

View file

@ -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<HTMLInputElement>) {
// 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 (
<div>
<Admonition type="tip">
<p>
Aim for at least{' '}
<Link href="https://www.w3.org/TR/WCAG20/#visual-audio-contrast-contrast">
WCAG-AA contrast ratio
</Link>{' '}
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&apos;t usually work in both light and dark mode.
</p>
</Admonition>
<p>
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<label htmlFor="primary_color">
@ -89,33 +103,69 @@ function ColorGenerator(): JSX.Element {
</label>{' '}
<input
id="primary_color"
className={styles.input}
defaultValue={baseColor}
onChange={(event) => {
// 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}
/>
<input
type="color"
className={styles.colorInput}
// value has to always be a valid color, so baseColor instead of inputColor
value={baseColor}
onChange={updateColor}
/>
<button
type="button"
className="clean-btn button button--primary margin-left--md"
onClick={() => {
if (isDarkTheme) {
setLightTheme();
} else {
setDarkTheme();
}
}}>
Edit {isDarkTheme ? 'light' : 'dark'} mode
</button>
<button
type="button"
className="clean-btn button button--secondary margin-left--md"
onClick={() => {
setInputColor(DEFAULT_PRIMARY_COLOR);
setBaseColor(DEFAULT_PRIMARY_COLOR);
setBackground(DEFAULT_BACKGROUND_COLOR);
setShades(COLOR_SHADES);
}}>
Reset
</button>
</p>
<p>
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<label htmlFor="background_color">
<strong className="margin-right--sm">Background:</strong>
</label>
<input
id="background_color"
type="color"
className={clsx(styles.colorInput, 'margin-right--sm')}
value={background}
onChange={(e) => {
setBackground(e.target.value);
}}
/>
</p>
<div>
<table>
<table className={styles.colorTable}>
<thead>
<tr>
<th>CSS Variable Name</th>
<th>Hex</th>
<th>Adjustment</th>
<th>Contrast Rating</th>
</tr>
</thead>
<tbody>
{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 {
/>
)}
</td>
<td
style={{
fontSize: 'medium',
backgroundColor: background,
color: hex,
}}>
<b>{wcagContrast(hex, background)}</b>
</td>
</tr>
);
})}
@ -170,11 +228,17 @@ function ColorGenerator(): JSX.Element {
Replace the variables in <code>src/css/custom.css</code> with these new
variables.
</p>
<CodeBlock className="css">
{adjustedColors
.sort((a, b) => a.codeOrder - b.codeOrder)
.map((value) => `${value.variableName}: ${value.hex.toLowerCase()};`)
.join('\n')}
<CodeBlock className="language-css" title="/src/css/custom.css">
{`${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};`
: ''
}
}`}
</CodeBlock>
</div>
);

View file

@ -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;
}

View file

@ -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%);

View file

@ -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;
}

View file

@ -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 (
<OriginalToggle
{...props}
onChange={(e) => {
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);
}
}}
/>
);
}

View file

@ -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);
}