Skip to content

Commit

Permalink
FIDEFE-4626 - Add HCM media queries to built CSS (mozilla#3)
Browse files Browse the repository at this point in the history
* FIDEFE-4626 - Add HCM media queries to built CSS

* remove some of the 'default' references from JSON, fix test fixtures

* address review comments
  • Loading branch information
hannajones authored Jan 29, 2024
1 parent 48d8fe6 commit fc44ab9
Show file tree
Hide file tree
Showing 5 changed files with 250 additions and 43 deletions.
30 changes: 26 additions & 4 deletions toolkit/themes/shared/design-system/build/css/tokens-shared.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
:root {
--text-deemphasized: color-mix(in srgb, currentColor 60%, transparent);
--text-platform: currentColor;
--text-color: CanvasText;
--text-color-deemphasized: color-mix(in srgb, currentColor 60%, transparent);
--color-white: #ffffff;
--color-yellow-05: #ffebcd;
--color-yellow-80: #5a3100;
Expand Down Expand Up @@ -32,9 +30,33 @@
--color-blue-50: #0060df;
--color-blue-30: #73a7f3;
--color-black-a10: rgba(0, 0, 0, 0.1);
--text-brand: light-dark(var(--color-gray-100), var(--color-gray-05));
--border-width: 1px;
--border-radius-medium: 8px;
--border-radius-small: 4px;
--border-radius-circle: 9999px;
--color-background-warning: light-dark(var(--color-yellow-05), var(--color-blue-80));
--color-background-success: light-dark(var(--color-green-05), var(--color-yellow-80));
--color-background-information: light-dark(var(--color-blue-05), var(--color-blue-80));
--color-background-critical: light-dark(var(--color-red-05), var(--color-red-80));
}

@media (prefers-contrast) {
:root {
--text-color-deemphasized: inherit;
--text-color-default: CanvasText;
--border-interactive-color-disabled: GrayText;
--border-interactive-color-active: AccentColor;
--border-interactive-color-hover: SelectedItem;
--border-interactive-color-default: AccentColor;
--border-color: var(--text-color-default);
}
}

@media (forced-colors) {
:root {
--border-interactive-color-disabled: GrayText;
--border-interactive-color-active: ButtonText;
--border-interactive-color-hover: ButtonText;
--border-interactive-color-default: ButtonText;
}
}
103 changes: 100 additions & 3 deletions toolkit/themes/shared/design-system/config.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,105 @@

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/* eslint-env node */

const StyleDictionary = require("style-dictionary");
const { formattedVariables } = StyleDictionary.formatHelpers;

const MEDIA_QUERY_PROPERTY_MAP = {
"forced-colors": "forcedColors",
"prefers-contrast": "prefersContrast",
};

/**
* Formats built CSS to include "prefers-contrast" and "forced-colors" media
* queries.
*
* @param {object} args
* Formatter arguments provided by style-dictionary. See more at
* https://amzn.github.io/style-dictionary/#/formats?id=formatter
* @returns {string}
* Formatted CSS including media queries.
*/
function hcmFormatter(args) {
return (
formatTokens({ args }) +
formatTokens({ mediaQuery: "prefers-contrast", args }) +
formatTokens({ mediaQuery: "forced-colors", args })
);
}

/**
* Formats a subset of tokens into CSS. Wraps token CSS in a media query when
* applicable.
*
* @param {object} tokenArgs
* @param {string} [tokenArgs.mediaQuery]
* Media query formatted CSS should be wrapped in. This is used
* to determine what property we are parsing from the token values.
* @param {object} tokenArgs.args
* Formatter arguments provided by style-dictionary. See more at
* https://amzn.github.io/style-dictionary/#/formats?id=formatter
* @returns {string} Tokens formatted into a CSS string.
*/
function formatTokens({ mediaQuery, args }) {
let prop = MEDIA_QUERY_PROPERTY_MAP[mediaQuery] ?? "value";
let dictionary = Object.assign({}, args.dictionary);
let tokens = [];

dictionary.allTokens.forEach(token => {
let value = token[prop] || token.original.value[prop]
if (value && typeof value !== "object") {
let formattedToken = transformTokenValue(token, prop, dictionary);
tokens.push(formattedToken);
}
});

dictionary.allTokens = dictionary.allProperties = tokens;

let formattedVars = formattedVariables({
format: "css",
dictionary,
outputReferences: args.options.outputReferences,
formatting: {
indentation: mediaQuery ? " " : " ",
},
});

// Weird spacing below is unfortunately necessary formatting the built CSS.
if (mediaQuery) {
return `
@media (${mediaQuery}) {
:root {
${formattedVars}
}
}
`;
}

return `:root {\n${formattedVars}\n}\n`;
}

/**
* Takes a token object and changes "value" based on the supplied prop. Also
* preserves variable references when necessary.
*
* @param {object} token - Token object parsed from JSON by style-dictionary.
* @param {string} prop
* Name of the property used to get the token's new value.
* @param {object} dictionary
* Object of transformed tokens and helper fns provided by style-dictionary.
* @returns {object} Token object with an updated value.
*/
function transformTokenValue(token, prop, dictionary) {
let originalVal = token.original.value[prop];
if (dictionary.usesReference(originalVal)) {
let refs = dictionary.getReferences(originalVal);
return { ...token, value: `var(--${refs[0].name})` };
}
return { ...token, value: token[prop] || originalVal};
}

module.exports = {
source: ["design-tokens.json"],
Expand All @@ -15,7 +109,7 @@ module.exports = {
transitive: true,
name: "defaultTransform",
matcher: token => token.original.value.default,
transformer: token => token.original.value.default
transformer: token => token.original.value.default,
},
lightDarkTransform: {
type: "value",
Expand All @@ -27,6 +121,9 @@ module.exports = {
},
},
},
format: {
"css/variables/hcm": hcmFormatter,
},
platforms: {
css: {
// The ordering of transforms matter, so if we encountered
Expand All @@ -42,7 +139,7 @@ module.exports = {
files: [
{
destination: "tokens-shared.css",
format: "css/variables",
format: "css/variables/hcm",
options: {
outputReferences: true,
showFileHeader: false,
Expand Down
74 changes: 59 additions & 15 deletions toolkit/themes/shared/design-system/design-tokens.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,53 @@
{
"border": {
"color": {
"value": {
"prefersContrast": "{text.color.default}"
}
},
"interactive": {
"color": {
"default": {
"value": {
"prefersContrast": "AccentColor",
"forcedColors": "ButtonText"
}
},
"hover": {
"value": {
"forcedColors": "ButtonText",
"prefersContrast": "SelectedItem"
}
},
"active": {
"value": {
"prefersContrast": "AccentColor",
"forcedColors": "ButtonText"
}
},
"disabled": {
"value": {
"prefersContrast": "GrayText",
"forcedColors": "GrayText"
}
}
}
},
"radius": {
"circle": {
"value": "9999px"
},
"small": {
"value": "4px"
},
"medium": {
"value": "8px"
}
},
"width": {
"value": "1px"
}
},
"color": {
"black": {
"a10": {
Expand Down Expand Up @@ -133,21 +182,16 @@
},
"text": {
"color": {
"value": "CanvasText"
},
"brand": {
"value": {
"light": "{color.gray.100}",
"dark": "{color.gray.05}"
}
},
"platform": {
"value": "currentColor"
},
"deemphasized": {
"value": {
"default": "color-mix(in srgb, currentColor 60%, transparent)",
"prefersContrast": "inherit"
"default": {
"value": {
"prefersContrast": "CanvasText"
}
},
"deemphasized": {
"value": {
"default": "color-mix(in srgb, currentColor 60%, transparent)",
"prefersContrast": "inherit"
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion toolkit/themes/shared/design-system/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"jest/globals": true
},
"scripts": {
"test": "jest --testPathPattern=tests",
"test": "jest --testPathPattern=tests --silent",
"build": "style-dictionary build"
},
"author": "",
Expand Down
84 changes: 64 additions & 20 deletions toolkit/themes/shared/design-system/tests/config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ const config = require("../config");

const TEST_BUILD_PATH = "tests/build/css/";

const EXPECTED_CSS_RULES = {
"--color-background-critical": "light-dark(var(--color-red-05), var(--color-red-80))",
"--color-background-information": "light-dark(var(--color-blue-05), var(--color-blue-80))",
"--color-background-success": "light-dark(var(--color-green-05), var(--color-yellow-80))",
"--color-background-warning": "light-dark(var(--color-yellow-05), var(--color-blue-80))",
const BASE_CSS_RULES = {
"--color-background-critical":
"light-dark(var(--color-red-05), var(--color-red-80))",
"--color-background-information":
"light-dark(var(--color-blue-05), var(--color-blue-80))",
"--color-background-success":
"light-dark(var(--color-green-05), var(--color-yellow-80))",
"--color-background-warning":
"light-dark(var(--color-yellow-05), var(--color-blue-80))",
"--color-black-a10": "rgba(0, 0, 0, 0.1)",
"--color-blue-05": "#deeafc",
"--color-blue-30": "#73a7f3",
Expand Down Expand Up @@ -42,10 +46,35 @@ const EXPECTED_CSS_RULES = {
"--color-yellow-30": "#e49c49",
"--color-yellow-50": "#cd411e",
"--color-yellow-80": "#5a3100",
"--text-brand": "light-dark(var(--color-gray-100), var(--color-gray-05))",
"--text-color": "CanvasText",
"--text-deemphasized": "color-mix(in srgb, currentColor 60%, transparent)",
"--text-platform": "currentColor",
"--text-color-deemphasized":
"color-mix(in srgb, currentColor 60%, transparent)",
"--border-radius-circle": "9999px",
"--border-radius-small": "4px",
"--border-radius-medium": "8px",
"--border-width": "1px",
};

const PREFERS_CONTRAST_CSS_RULES = {
"--text-color-deemphasized": "inherit",
"--text-color-default": "CanvasText",
"--border-interactive-color-disabled": "GrayText",
"--border-interactive-color-active": "AccentColor",
"--border-interactive-color-hover": "SelectedItem",
"--border-interactive-color-default": "AccentColor",
"--border-color": "var(--text-color-default)",
};

const FORCED_COLORS_CSS_RULES = {
"--border-interactive-color-disabled": "GrayText",
"--border-interactive-color-active": "ButtonText",
"--border-interactive-color-hover": "ButtonText",
"--border-interactive-color-default": "ButtonText",
};

const FIXTURE_BY_QUERY = {
base: BASE_CSS_RULES,
"prefers-contrast": PREFERS_CONTRAST_CSS_RULES,
"forced-colors": FORCED_COLORS_CSS_RULES,
};

// Use our real config, just modify some values for the test.
Expand All @@ -56,21 +85,36 @@ testConfig.platforms.css.buildPath = TEST_BUILD_PATH;
describe("generated CSS", () => {
StyleDictionary.extend(testConfig).buildAllPlatforms();

describe("css/variables", () => {
describe("css/variables/hcm format", () => {
const output = fs.readFileSync(`${TEST_BUILD_PATH}tokens-shared.css`, {
encoding: "UTF-8",
});

it("should produce the expected CSS", () => {
let formattedCSS = output.split("\n").reduce((rulesObj, rule) => {
let [key, val] = rule.split(":");
if (key && val) {
return { ...rulesObj, [key.trim()]: val.trim().replace(";", "") };
}
return rulesObj;
}, {});

expect(formattedCSS).toMatchObject(EXPECTED_CSS_RULES);
let rulesByMediaQuery = output.split("@media");

it("should contain three blocks of CSS, including media queries", () => {
expect(rulesByMediaQuery.length).toBe(3);
expect(rulesByMediaQuery[1]).toEqual(
expect.stringContaining("prefers-contrast")
);
expect(rulesByMediaQuery[2]).toEqual(
expect.stringContaining("forced-colors")
);
});

rulesByMediaQuery.forEach(ruleSet => {
let queryName = ruleSet.trim().match(/(?<=\().+?(?=\) \{)/) || "base";
it(`should produce the expected ${queryName} CSS rules`, () => {
let formattedCSS = ruleSet.split("\n").reduce((rulesObj, rule) => {
let [key, val] = rule.split(":");
if (key.trim() && val) {
return { ...rulesObj, [key.trim()]: val.trim().replace(";", "") };
}
return rulesObj;
}, {});

expect(formattedCSS).toStrictEqual(FIXTURE_BY_QUERY[queryName]);
});
});
});
});
Expand Down

0 comments on commit fc44ab9

Please sign in to comment.