Browse Source

Add tests to help with maintenance of theme color tokens

Thibaud Colas 1 year ago
parent
commit
40335ba9d1

+ 131 - 5
client/src/tokens/colorVariables.test.js

@@ -1,11 +1,15 @@
-const colors = require('./colors');
-const { generateColorVariables } = require('./colorVariables');
+const { staticColors, transparencies } = require('./colors');
+const colorThemes = require('./colorThemes');
+const {
+  generateColorVariables,
+  generateThemeColorVariables,
+} = require('./colorVariables');
 
 describe('generateColorVariables', () => {
   it('generates all variables', () => {
-    const colorVariables = generateColorVariables(colors);
+    const colorVariables = generateColorVariables(staticColors);
     const generatedVariables = Object.keys(colorVariables);
-    Object.values(colors).forEach((hues) => {
+    Object.values(staticColors).forEach((hues) => {
       Object.values(hues).forEach((shade) => {
         expect(generatedVariables).toContain(shade.cssVariable);
       });
@@ -20,7 +24,7 @@ describe('generateColorVariables', () => {
    * - Leave the copied content exactly as-is when pasting, to avoid any Markdown formatting issues.
    */
   it('is stable (update custom_user_interface_colours documentation when this changes)', () => {
-    const colorVariables = generateColorVariables(colors);
+    const colorVariables = generateColorVariables(staticColors);
     expect(colorVariables).toMatchInlineSnapshot(`
       Object {
         "--w-color-black": "hsl(var(--w-color-black-hue) var(--w-color-black-saturation) var(--w-color-black-lightness))",
@@ -131,3 +135,125 @@ describe('generateColorVariables', () => {
     `);
   });
 });
+
+describe('transparencies', () => {
+  it('is stable (update custom_user_interface_colours documentation when this changes)', () => {
+    expect(transparencies).toMatchInlineSnapshot(`
+      Object {
+        "--w-color-black-10": "rgba(0, 0, 0, 0.10)",
+        "--w-color-black-20": "rgba(0, 0, 0, 0.20)",
+        "--w-color-black-25": "rgba(0, 0, 0, 0.25)",
+        "--w-color-black-35": "rgba(0, 0, 0, 0.35)",
+        "--w-color-black-5": "rgba(0, 0, 0, 0.05)",
+        "--w-color-black-50": "rgba(0, 0, 0, 0.50)",
+        "--w-color-white-10": "rgba(255, 255, 255, 0.10)",
+        "--w-color-white-15": "rgba(255, 255, 255, 0.15)",
+        "--w-color-white-50": "rgba(255, 255, 255, 0.50)",
+        "--w-color-white-80": "rgba(255, 255, 255, 0.80)",
+      }
+    `);
+  });
+});
+
+describe('generateThemeColorVariables', () => {
+  it('uses the same variables in both themes', () => {
+    const light = Object.keys(generateThemeColorVariables(colorThemes.light));
+    const dark = Object.keys(generateThemeColorVariables(colorThemes.dark));
+    expect(light).toEqual(dark);
+  });
+
+  it('uses color variables for all values (except focus)', () => {
+    const values = [
+      ...Object.values(generateThemeColorVariables(colorThemes.light)),
+      ...Object.values(generateThemeColorVariables(colorThemes.dark)),
+    ];
+    expect(values.filter((val) => !val.startsWith('var('))).toEqual([
+      '#00A885',
+      '#00A885',
+    ]);
+  });
+
+  it('light theme is stable (update custom_user_interface_colours documentation when this changes)', () => {
+    expect(generateThemeColorVariables(colorThemes.light))
+      .toMatchInlineSnapshot(`
+      Object {
+        "--w-color-border-button-outline-default": "var(--w-color-secondary)",
+        "--w-color-border-button-small-outline-default": "var(--w-color-grey-150)",
+        "--w-color-border-field-default": "var(--w-color-grey-150)",
+        "--w-color-border-field-hover": "var(--w-color-grey-200)",
+        "--w-color-border-field-inactive": "var(--w-color-grey-150)",
+        "--w-color-border-furniture": "var(--w-color-grey-100)",
+        "--w-color-focus": "#00A885",
+        "--w-color-icon-primary": "var(--w-color-primary)",
+        "--w-color-icon-primary-hover": "var(--w-color-primary-200)",
+        "--w-color-icon-secondary": "var(--w-color-grey-400)",
+        "--w-color-icon-secondary-hover": "var(--w-color-primary-200)",
+        "--w-color-surface-button-default": "var(--w-color-secondary)",
+        "--w-color-surface-button-hover": "var(--w-color-secondary-400)",
+        "--w-color-surface-button-inactive": "var(--w-color-grey-400)",
+        "--w-color-surface-button-outline-hover": "var(--w-color-secondary-50)",
+        "--w-color-surface-field": "var(--w-color-white)",
+        "--w-color-surface-field-inactive": "var(--w-color-grey-50)",
+        "--w-color-surface-header": "var(--w-color-grey-50)",
+        "--w-color-surface-menu-item-active": "var(--w-color-primary-200)",
+        "--w-color-surface-menus": "var(--w-color-primary)",
+        "--w-color-surface-page": "var(--w-color-white)",
+        "--w-color-surface-tooltip": "var(--w-color-primary-200)",
+        "--w-color-text-button": "var(--w-color-white)",
+        "--w-color-text-button-outline-default": "var(--w-color-secondary)",
+        "--w-color-text-context": "var(--w-color-grey-600)",
+        "--w-color-text-error": "var(--w-color-critical-200)",
+        "--w-color-text-highlight": "var(--w-color-secondary-75)",
+        "--w-color-text-label": "var(--w-color-primary)",
+        "--w-color-text-label-menus-active": "var(--w-color-white)",
+        "--w-color-text-label-menus-default": "var(--w-color-white-80)",
+        "--w-color-text-link-default": "var(--w-color-secondary)",
+        "--w-color-text-link-hover": "var(--w-color-secondary-400)",
+        "--w-color-text-meta": "var(--w-color-grey-400)",
+        "--w-color-text-placeholder": "var(--w-color-grey-400)",
+      }
+    `);
+  });
+
+  it('dark theme is stable (update custom_user_interface_colours documentation when this changes)', () => {
+    expect(generateThemeColorVariables(colorThemes.dark))
+      .toMatchInlineSnapshot(`
+      Object {
+        "--w-color-border-button-outline-default": "var(--w-color-secondary-100)",
+        "--w-color-border-button-small-outline-default": "var(--w-color-grey-400)",
+        "--w-color-border-field-default": "var(--w-color-grey-400)",
+        "--w-color-border-field-hover": "var(--w-color-grey-200)",
+        "--w-color-border-field-inactive": "var(--w-color-grey-500)",
+        "--w-color-border-furniture": "var(--w-color-grey-500)",
+        "--w-color-focus": "#00A885",
+        "--w-color-icon-primary": "var(--w-color-grey-150)",
+        "--w-color-icon-primary-hover": "var(--w-color-grey-50)",
+        "--w-color-icon-secondary": "var(--w-color-grey-150)",
+        "--w-color-icon-secondary-hover": "var(--w-color-grey-50)",
+        "--w-color-surface-button-default": "var(--w-color-secondary)",
+        "--w-color-surface-button-hover": "var(--w-color-secondary-400)",
+        "--w-color-surface-button-inactive": "var(--w-color-grey-400)",
+        "--w-color-surface-button-outline-hover": "var(--w-color-grey-500)",
+        "--w-color-surface-field": "var(--w-color-grey-600)",
+        "--w-color-surface-field-inactive": "var(--w-color-grey-500)",
+        "--w-color-surface-header": "var(--w-color-grey-600)",
+        "--w-color-surface-menu-item-active": "var(--w-color-grey-600)",
+        "--w-color-surface-menus": "var(--w-color-grey-500)",
+        "--w-color-surface-page": "var(--w-color-grey-600)",
+        "--w-color-surface-tooltip": "var(--w-color-grey-500)",
+        "--w-color-text-button": "var(--w-color-white)",
+        "--w-color-text-button-outline-default": "var(--w-color-secondary-100)",
+        "--w-color-text-context": "var(--w-color-grey-50)",
+        "--w-color-text-error": "var(--w-color-critical-100)",
+        "--w-color-text-highlight": "var(--w-color-secondary-400)",
+        "--w-color-text-label": "var(--w-color-grey-150)",
+        "--w-color-text-label-menus-active": "var(--w-color-white)",
+        "--w-color-text-label-menus-default": "var(--w-color-white-80)",
+        "--w-color-text-link-default": "var(--w-color-secondary-100)",
+        "--w-color-text-link-hover": "var(--w-color-secondary-75)",
+        "--w-color-text-meta": "var(--w-color-grey-150)",
+        "--w-color-text-placeholder": "var(--w-color-grey-200)",
+      }
+    `);
+  });
+});

+ 18 - 2
client/src/tokens/colors.js

@@ -17,7 +17,7 @@
 }} Colors */
 
 /** @type {Colors} */
-const colors = {
+const staticColors = {
   black: {
     DEFAULT: {
       hex: '#000000',
@@ -273,4 +273,20 @@ const colors = {
   },
 };
 
-module.exports = colors;
+const transparencies = {
+  '--w-color-white-10': 'rgba(255, 255, 255, 0.10)',
+  '--w-color-white-15': 'rgba(255, 255, 255, 0.15)',
+  '--w-color-white-50': 'rgba(255, 255, 255, 0.50)',
+  '--w-color-white-80': 'rgba(255, 255, 255, 0.80)',
+  '--w-color-black-5': 'rgba(0, 0, 0, 0.05)',
+  '--w-color-black-10': 'rgba(0, 0, 0, 0.10)',
+  '--w-color-black-20': 'rgba(0, 0, 0, 0.20)',
+  '--w-color-black-25': 'rgba(0, 0, 0, 0.25)',
+  '--w-color-black-35': 'rgba(0, 0, 0, 0.35)',
+  '--w-color-black-50': 'rgba(0, 0, 0, 0.50)',
+};
+
+module.exports = {
+  staticColors,
+  transparencies,
+};

+ 58 - 13
client/src/tokens/colors.stories.tsx

@@ -1,7 +1,10 @@
-import React from 'react';
-import colors, { Hues, Shade } from './colors';
-import colorThemes, { Token, ThemeCategory } from './colorThemes';
-import { generateColorVariables } from './colorVariables';
+import React, { Fragment } from 'react';
+import { staticColors, Hues, Shade } from './colors';
+import colorThemes, { ThemeCategory } from './colorThemes';
+import {
+  generateColorVariables,
+  generateThemeColorVariables,
+} from './colorVariables';
 
 const description = `
 Wagtail’s typographic styles are made available as separate design tokens, but in most scenarios it’s better to use one of the predefined text styles.
@@ -21,7 +24,7 @@ const getContrastGridLink = () => {
     '?version=1.1.0&es-color-form__tile-size=compact&es-color-form__show-contrast=aaa&es-color-form__show-contrast=aa&es-color-form__show-contrast=aa18';
   const bg: string[] = [];
   const fg: string[] = [];
-  Object.values(colors).forEach((hues: Hues) => {
+  Object.values(staticColors).forEach((hues: Hues) => {
     Object.values(hues).forEach((shade: Shade) => {
       const color = `${shade.hex}, ${shade.textUtility.replace('w-text-', '')}`;
       bg.push(color);
@@ -77,7 +80,7 @@ export const ColorPalette = () => (
       color palette, with contrasting text chosen for readability of this
       example only.
     </p>
-    {Object.entries(colors).map(([color, hues]) => (
+    {Object.entries(staticColors).map(([color, hues]) => (
       <div key={color}>
         <h2 className="w-sr-only">{color}</h2>
         <Palette color={color} hues={hues} />
@@ -130,17 +133,25 @@ export const ColorThemes = () => (
   </>
 );
 
-const variablesMap = Object.entries(generateColorVariables(colors))
+const rootVariablesMap = [
+  ...Object.entries(generateColorVariables(staticColors)),
+  ...Object.entries(generateThemeColorVariables(colorThemes.light)),
+]
   .map(([cssVar, val]) => `${cssVar}: ${val};`)
   .join('');
-const secondaryHSL = colors.secondary.DEFAULT.hsl.match(
+const darkVariablesMap = Object.entries(
+  generateThemeColorVariables(colorThemes.dark),
+)
+  .map(([cssVar, val]) => `${cssVar}: ${val};`)
+  .join('');
+const secondaryHSL = staticColors.secondary.DEFAULT.hsl.match(
   /\d+(\.\d+)?/g,
 ) as string[];
 // Make sure this contains no empty lines, otherwise Sphinx docs will treat this as paragraphs.
 const liveEditorCustomisations = `:root {
-  --w-color-primary: ${colors.primary.DEFAULT.hex};
+  --w-color-primary: ${staticColors.primary.DEFAULT.hex};
   /* Any valid CSS format is supported. */
-  --w-color-primary-200: ${colors.primary[200].hsl};
+  --w-color-primary-200: ${staticColors.primary[200].hsl};
   /* Set each HSL component separately to change all hues at once. */
   --w-color-secondary-hue: ${secondaryHSL[0]};
   --w-color-secondary-saturation: ${secondaryHSL[1]}%;
@@ -148,13 +159,15 @@ const liveEditorCustomisations = `:root {
 }`;
 // Story using inline styles only so it can be copy-pasted into the Wagtail documentation for color customisations.
 const demoStyles = `
-  :root {${variablesMap}}
+  :root {${rootVariablesMap}}
+  .w-theme-dark {${darkVariablesMap}}
   .wagtail-color-swatch {
     border-collapse: separate;
     border-spacing: 4px;
   }
 
-  .wagtail-color-swatch td:first-child {
+  .wagtail-color-swatch td:first-child,
+  .wagtail-color-swatch .w-theme-dark {
     height: 1.5rem;
     width: 1.5rem;
     border: 1px solid #333;
@@ -190,6 +203,7 @@ const colorCustomisationsDemo = (
         {liveEditorCustomisations}
       </style>
     </pre>
+    <h3>Static colours</h3>
     <table className="wagtail-color-swatch">
       <thead>
         <tr>
@@ -199,7 +213,7 @@ const colorCustomisationsDemo = (
         </tr>
       </thead>
       <tbody>
-        {Object.values(colors).map((hues) =>
+        {Object.values(staticColors).map((hues) =>
           Object.entries(hues)
             //  Show DEFAULT shades first, then in numerical order.
             .sort(([nameA], [nameB]) =>
@@ -217,6 +231,37 @@ const colorCustomisationsDemo = (
         )}
       </tbody>
     </table>
+    <h3>Light & dark theme colours</h3>
+    <table className="wagtail-color-swatch">
+      <thead>
+        <tr>
+          <th>Light</th>
+          <th>Dark</th>
+          <th>Variable</th>
+        </tr>
+      </thead>
+      {colorThemes.light.map((category) => (
+        <tbody key={category.label}>
+          <tr>
+            <th scope="rowgroup" colSpan={3}>
+              {category.label}
+            </th>
+          </tr>
+          {Object.values(category.tokens).map((token) => (
+            <tr key={token.cssVariable}>
+              <td style={{ backgroundColor: `var(${token.cssVariable})` }} />
+              <td
+                className="w-theme-dark"
+                style={{ backgroundColor: `var(${token.cssVariable})` }}
+              />
+              <td>
+                <code>{token.cssVariable}</code>
+              </td>
+            </tr>
+          ))}
+        </tbody>
+      ))}
+    </table>
   </section>
 );
 

+ 4 - 13
client/tailwind.config.js

@@ -3,7 +3,7 @@ const vanillaRTL = require('tailwindcss-vanilla-rtl');
 /**
  * Design Tokens
  */
-const colors = require('./src/tokens/colors');
+const { staticColors, transparencies } = require('./src/tokens/colors');
 const {
   generateColorVariables,
   generateThemeColorVariables,
@@ -36,7 +36,7 @@ const scrollbarThin = require('./src/plugins/scrollbarThin');
  * themeColors: For converting our design tokens into a format that tailwind accepts
  */
 const themeColors = Object.fromEntries(
-  Object.entries(colors).map(([key, hues]) => {
+  Object.entries(staticColors).map(([key, hues]) => {
     const shades = Object.fromEntries(
       Object.entries(hues).map(([k, shade]) => [
         k,
@@ -165,17 +165,8 @@ module.exports = {
         ':root, :host': {
           '--w-font-sans': fontFamily.sans.join(', '),
           '--w-font-mono': fontFamily.mono.join(', '),
-          '--w-color-white-10': 'rgba(255, 255, 255, 0.10)',
-          '--w-color-white-15': 'rgba(255, 255, 255, 0.15)',
-          '--w-color-white-50': 'rgba(255, 255, 255, 0.50)',
-          '--w-color-white-80': 'rgba(255, 255, 255, 0.80)',
-          '--w-color-black-5': 'rgba(0, 0, 0, 0.05)',
-          '--w-color-black-10': 'rgba(0, 0, 0, 0.10)',
-          '--w-color-black-20': 'rgba(0, 0, 0, 0.20)',
-          '--w-color-black-25': 'rgba(0, 0, 0, 0.25)',
-          '--w-color-black-35': 'rgba(0, 0, 0, 0.35)',
-          '--w-color-black-50': 'rgba(0, 0, 0, 0.50)',
-          ...generateColorVariables(colors),
+          ...transparencies,
+          ...generateColorVariables(staticColors),
           ...generateThemeColorVariables(colorThemes.light),
         },
         '.w-theme-system': {