Handle ColorMix properly by separating parsing and rendering

* Updated `colorMixHandler` to run before `varHandler`
so that colorMixHandler is run even though there is a variable reference
inside of the syntax.

* Used CodeMirror parser to get values out of `color-mix` syntax.

* Handle resolving variables properly and rendering them

Bug: none for now
Change-Id: Ifc0d2c4b418531936b6730b5bb1d0e48da76c715
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/4161386
Reviewed-by: Philip Pfaffe <pfaffe@chromium.org>
Auto-Submit: Ergün Erdoğmuş <ergunsh@chromium.org>
Commit-Queue: Ergün Erdoğmuş <ergunsh@chromium.org>
This commit is contained in:
Ergün Erdoğmuş
2023-01-19 10:17:47 +00:00
committed by Devtools-frontend LUCI CQ
parent 35d860d2cf
commit cd61077e86
14 changed files with 653 additions and 66 deletions
+1
View File
@@ -1684,6 +1684,7 @@ grd_files_debug_sources = [
"front_end/ui/legacy/components/inline_editor/CSSLengthUtils.js",
"front_end/ui/legacy/components/inline_editor/CSSShadowEditor.js",
"front_end/ui/legacy/components/inline_editor/CSSShadowModel.js",
"front_end/ui/legacy/components/inline_editor/ColorMixModel.js",
"front_end/ui/legacy/components/inline_editor/ColorMixSwatch.js",
"front_end/ui/legacy/components/inline_editor/ColorSwatch.js",
"front_end/ui/legacy/components/inline_editor/FontEditor.js",
+1 -1
View File
@@ -1864,7 +1864,7 @@ export class Legacy implements Color {
export const Regex: RegExp =
/((?:rgba?|hsla?|hwba?|lab|lch|oklab|oklch|color)\([^)]+\)|#[0-9a-fA-F]{8}|#[0-9a-fA-F]{6}|#[0-9a-fA-F]{3,4}|\b[a-zA-Z]+\b(?!-))/g;
export const ColorMixRegex: RegExp = /(?:color-mix)\(.+\)/g;
export const ColorMixRegex: RegExp = /color-mix\(.*,\s*(?<firstColor>.+)\s*,\s*(?<secondColor>.+)\s*\)/g;
export const enum Format {
Nickname = 'nickname',
HEX = 'hex',
@@ -462,3 +462,13 @@ export type LowerCaseString = string&LowerCaseStringTag;
export const toLowerCaseString = function(input: string): LowerCaseString {
return input.toLowerCase() as LowerCaseString;
};
// Replaces the last ocurrence of parameter `search` with parameter `replacement` in `input`
export const replaceLast = function(input: string, search: string, replacement: string): string {
const replacementStartIndex = input.lastIndexOf(search);
if (replacementStartIndex === -1) {
return input;
}
return input.slice(0, replacementStartIndex) + input.slice(replacementStartIndex).replace(search, replacement);
};
@@ -299,29 +299,94 @@ export class StylePropertyTreeElement extends UI.TreeOutline.TreeElement {
}
private processColorMix(text: string): Node {
const swatch = new InlineEditor.ColorMixSwatch.ColorMixSwatch();
swatch.setText(text);
const contentChild = document.createElement('span');
// We're matching the colors inside `color-mix` syntax like
// `red` and `blue` in `color-mix(in srgb, red, blue)` and adding
// color swatch for them.
const matches = TextUtils.TextUtils.Utils.splitStringByRegexes(text, [Common.Color.Regex]);
for (const match of matches) {
if (match.regexIndex === 0) {
const colorSwatch = this.processColor(match.value);
colorSwatch.addEventListener(InlineEditor.ColorSwatch.ColorChangedEvent.eventName, () => {
// Update the `color-mix` swatch when one parameter color changed through its color swatch
swatch.setText(this.property.value);
});
contentChild.appendChild(colorSwatch);
} else {
contentChild.appendChild(document.createTextNode(match.value));
}
let colorMixText = text;
const paramColorValues: string[] = [];
const colorMixModel = InlineEditor.ColorMixModel.ColorMixModel.parse(text);
if (!colorMixModel) {
return document.createTextNode(text);
}
const handleValue = (value: string, onChange: (newColorText: string) => void): void => {
// Parameter is a CSS variable
if (value.match(SDK.CSSMetadata.VariableRegex)) {
const computedSingleValue = this.matchedStylesInternal.computeSingleVariableValue(this.style, value);
// The variable is not defined or it is not a color
if (!computedSingleValue || !computedSingleValue.computedValue ||
!Common.Color.parse(computedSingleValue.computedValue)) {
return;
}
const {computedValue} = computedSingleValue;
// Update `var` reference in the color mix text with the variable's
// computed value since the same variable is not defined in DevTools
// reference to that in the CSS will result in undefined color.
colorMixText = colorMixText.replace(value, computedValue);
const varSwatch = this.processVar(value);
if (varSwatch instanceof InlineEditor.ColorSwatch.ColorSwatch) {
varSwatch.addEventListener(
InlineEditor.ColorSwatch.ColorChangedEvent.eventName,
(ev: InlineEditor.ColorSwatch.ColorChangedEvent) => {
onChange(ev.data.text);
});
}
contentChild.appendChild(varSwatch);
paramColorValues.push(computedSingleValue.computedValue);
return;
}
// Parameter is specified as an actual color (i.e. #000)
if (value.match(Common.Color.Regex)) {
const colorSwatch = this.processColor(value);
if (colorSwatch instanceof InlineEditor.ColorSwatch.ColorSwatch) {
colorSwatch.addEventListener(
InlineEditor.ColorSwatch.ColorChangedEvent.eventName,
(ev: InlineEditor.ColorSwatch.ColorChangedEvent) => {
onChange(ev.data.text);
});
}
contentChild.appendChild(colorSwatch);
paramColorValues.push(value);
}
};
const handleParam =
(paramParts: InlineEditor.ColorMixModel.ParamPart[], onChange: (newColorText: string) => void): void => {
for (let i = 0; i < paramParts.length; i++) {
const part = paramParts[i];
if (part.name === InlineEditor.ColorMixModel.PartName.Value) {
handleValue(part.value, onChange);
} else {
contentChild.appendChild(document.createTextNode(part.value));
}
if (i !== paramParts.length - 1) {
contentChild.appendChild(document.createTextNode(' '));
}
}
};
const [interpolationMethod, firstParam, secondParam] = colorMixModel.parts;
const swatch = new InlineEditor.ColorMixSwatch.ColorMixSwatch();
const contentChild = document.createElement('span');
contentChild.appendChild(document.createTextNode('color-mix('));
contentChild.appendChild(document.createTextNode(`${interpolationMethod.value}, `));
handleParam(firstParam.value, (color: string) => {
swatch.setFirstColor(color);
});
contentChild.appendChild(document.createTextNode(', '));
handleParam(secondParam.value, (color: string) => {
swatch.setSecondColor(color);
});
contentChild.appendChild(document.createTextNode(')'));
if (paramColorValues.length !== 2) {
return document.createTextNode(text);
}
swatch.appendChild(contentChild);
swatch.setFirstColor(paramColorValues[0]);
swatch.setSecondColor(paramColorValues[1]);
swatch.setColorMixText(colorMixText);
return swatch;
}
+11 -2
View File
@@ -2154,12 +2154,21 @@ export class StylesSidebarPropertyRenderer {
UI.Tooltip.Tooltip.install(valueElement, unescapeCssString(this.propertyValue));
}
const regexes = [SDK.CSSMetadata.VariableRegex, SDK.CSSMetadata.URLRegex];
const processors = [this.varHandler, this.processURL.bind(this)];
const regexes = [];
const processors = [];
// Push `color-mix` handler before pushing regex handler because
// `color-mix` can contain variables inside and we want to handle
// it as `color-mix` swatch that displays a variable swatch inside
// `color: color-mix(in srgb, var(--a), var(--b))` should be handled
// by colorMixHandler not varHandler.
if (this.colorMixHandler && metadata.isColorAwareProperty(this.propertyName)) {
regexes.push(Common.Color.ColorMixRegex);
processors.push(this.colorMixHandler);
}
regexes.push(SDK.CSSMetadata.VariableRegex, SDK.CSSMetadata.URLRegex);
processors.push(this.varHandler, this.processURL.bind(this));
// Handle `color` properties before handling other ones
// because color Regex is fairly narrow to only select real colors.
// However, some other Regexes like Bezier is very wide (text that
@@ -38,6 +38,7 @@ devtools_module("inline_editor") {
"CSSLengthUtils.ts",
"CSSShadowEditor.ts",
"CSSShadowModel.ts",
"ColorMixModel.ts",
"ColorMixSwatch.ts",
"ColorSwatch.ts",
"FontEditor.ts",
@@ -0,0 +1,162 @@
// Copyright 2023 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as CodeMirror from '../../../../third_party/codemirror.next/codemirror.next.js';
const cssParser = CodeMirror.css.cssLanguage.parser;
// color-mix() = color-mix( <color-interpolation-method> , [ <color> && <percentage [0,100]>? ]#{2})
function parseColorAndPercentage(cursor: CodeMirror.TreeCursor, referenceText: string): ParamPart[]|null {
const parts: ParamPart[] = [];
while (cursor.nextSibling()) {
if (cursor.name === ',' || cursor.name === ')') {
break;
}
if (cursor.name === 'ValueName' || cursor.name === 'CallExpression' || cursor.name === 'ColorLiteral') {
parts.push({
name: PartName.Value,
value: referenceText.substring(cursor.from, cursor.to),
});
} else if (cursor.name === 'NumberLiteral') {
const numberText = referenceText.substring(cursor.from, cursor.to);
if (!numberText.includes('%')) {
return null;
}
parts.push({
name: PartName.Percentage,
value: numberText,
});
} else {
return null;
}
}
return parts;
}
function parseColorInterpolationMethod(cursor: CodeMirror.TreeCursor, referenceText: string): {
name: PartName.InterpolationMethod,
value: string,
}|null {
let colorInterpolationMethod = '';
while (cursor.nextSibling()) {
if (cursor.name === ',') {
break;
}
colorInterpolationMethod += referenceText.substring(cursor.from, cursor.to) + ' ';
}
if (cursor.name !== ',') {
return null;
}
return {
name: PartName.InterpolationMethod,
value: colorInterpolationMethod.trimEnd(),
};
}
function isValidParam(param: ParamPart[]): boolean {
const hasOneValue = param.filter(paramPart => paramPart.name === PartName.Value).length === 1;
const hasAtMostOnePercentage = param.filter(paramPart => paramPart.name === PartName.Percentage).length <= 1;
return hasOneValue && hasAtMostOnePercentage;
}
export class ColorMixModel {
parts: Parts;
constructor(parts: Parts) {
this.parts = parts;
}
static parse(text: string): ColorMixModel|null {
const textToParse = `*{color:${text};}`;
const parsed = cssParser.parse(textToParse);
// Take the cursor from declaration
const cursor = parsed.cursorAt(textToParse.indexOf(':') + 1);
// Move until the `ArgList`
while (cursor.name !== 'ArgList' && cursor.next(true)) {
}
if (cursor.name !== 'ArgList') {
return null;
}
// We're on the `ArgList`, enter into it
cursor.firstChild();
// Parse first argument <color-interpolation-method>
const colorInterpolationMethodPart = parseColorInterpolationMethod(cursor, textToParse);
if (!colorInterpolationMethodPart) {
return null;
}
// Parse first color and percentage
const firstColorAndPercentage = parseColorAndPercentage(cursor, textToParse);
if (!firstColorAndPercentage) {
return null;
}
// Parse second color and percentage
const secondColorAndPercentage = parseColorAndPercentage(cursor, textToParse);
if (!secondColorAndPercentage) {
return null;
}
// Validate correctness of the syntax
// * interpolation method should start with `in`
if (!colorInterpolationMethodPart.value.startsWith('in')) {
return null;
}
// * Parts can't have more than one value
// * Parts can't have more than one percentage
if (!isValidParam(firstColorAndPercentage) || !isValidParam(secondColorAndPercentage)) {
return null;
}
return new ColorMixModel([
colorInterpolationMethodPart,
{
name: PartName.Param,
value: firstColorAndPercentage,
},
{
name: PartName.Param,
value: secondColorAndPercentage,
},
]);
}
}
export const enum PartName {
InterpolationMethod = 'IM',
Param = 'PA',
Value = 'V', // Can be anything that represents color (color literal, rgb/hsl/color call, variable etc.)
Percentage = 'P',
}
export type ParamPart = {
name: PartName.Percentage,
value: string,
}|{
name: PartName.Value,
value: string,
};
type Parts = [
{
name: PartName.InterpolationMethod,
value: string,
},
{
name: PartName.Param,
value: ParamPart[],
},
{
name: PartName.Param,
value: ParamPart[],
},
];
@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Common from '../../../../core/common/common.js';
import * as Platform from '../../../../core/platform/platform.js';
import * as ComponentHelpers from '../../../components/helpers/helpers.js';
import * as LitHtml from '../../../lit-html/lit-html.js';
@@ -11,9 +11,9 @@ import colorMixSwatchStyles from './colorMixSwatch.css.js';
export class ColorMixSwatch extends HTMLElement {
static readonly litTagName = LitHtml.literal`devtools-color-mix-swatch`;
private readonly shadow = this.attachShadow({mode: 'open'});
private text: string = 'color-mix(in srgb, hotpink, white)';
private firstColorText: string = 'hotpink';
private secondColorText: string = 'white';
private colorMixText: string = ''; // color-mix(in srgb, hotpink, white)
private firstColorText: string = ''; // hotpink
private secondColorText: string = ''; // white
constructor() {
super();
@@ -22,41 +22,40 @@ export class ColorMixSwatch extends HTMLElement {
];
}
setText(text: string): void {
this.text = text;
const parametersMatch = text.match(/color-mix\(in\s+(?:\w+)\s*,(?<firstColor>.+),(?<secondColor>.+)\)/);
if (!parametersMatch || !parametersMatch.groups || parametersMatch.length !== 3) {
this.#renderTextOnly();
return;
setFirstColor(text: string): void {
// We need to replace `colorMixText` because it is the CSS for the
// the middle section where we actually show the mixed colors.
// So, when a color changed, we need to update colorMixText to
// reflect the new color (not the old one).
if (this.firstColorText) {
this.colorMixText = this.colorMixText.replace(this.firstColorText, text);
}
const firstColorAndPercentage = parametersMatch.groups['firstColor'];
const secondColorAndPercentage = parametersMatch.groups['secondColor'];
const firstColorMatch = firstColorAndPercentage.match(Common.Color.Regex);
if (!firstColorMatch) {
this.#renderTextOnly();
return;
}
const secondColorMatch = secondColorAndPercentage.match(Common.Color.Regex);
if (!secondColorMatch) {
this.#renderTextOnly();
return;
}
this.firstColorText = firstColorMatch[0];
this.secondColorText = secondColorMatch[0];
this.firstColorText = text;
this.#render();
}
#renderTextOnly(): void {
LitHtml.render(this.text, this.shadow, {host: this});
setSecondColor(text: string): void {
// We need to replace from the last to handle the same colors case
// i.e. replacing the second color of `color-mix(in srgb, red 50%, red 10%)`
// to `blue` should result in `color-mix(in srgb, red 50%, blue 10%)`.
if (this.secondColorText) {
this.colorMixText = Platform.StringUtilities.replaceLast(this.colorMixText, this.secondColorText, text);
}
this.secondColorText = text;
this.#render();
}
setColorMixText(text: string): void {
this.colorMixText = text;
this.#render();
}
#render(): void {
if (!this.colorMixText || !this.firstColorText || !this.secondColorText) {
LitHtml.render(this.colorMixText, this.shadow, {host: this});
return;
}
// Disabled until https://crbug.com/1079231 is fixed.
// clang-format off
@@ -68,8 +67,8 @@ export class ColorMixSwatch extends HTMLElement {
LitHtml.html`<div class="swatch-icon">
<span class="swatch swatch-left" id="swatch-1" style="--color: ${this.firstColorText}"></span>
<span class="swatch swatch-right" id="swatch-2" style="--color: ${this.secondColorText}"></span>
<span class="swatch swatch-mix" id="mix-result" style="--color: ${this.text}"></span>
</div><slot>${this.text}</slot>`,
<span class="swatch swatch-mix" id="mix-result" style="--color: ${this.colorMixText}"></span>
</div><slot>${this.colorMixText}</slot>`,
this.shadow, {host: this});
// clang-format on
}
@@ -4,6 +4,7 @@
import * as BezierEditor from './BezierEditor.js';
import * as BezierUI from './BezierUI.js';
import * as ColorMixModel from './ColorMixModel.js';
import * as ColorMixSwatch from './ColorMixSwatch.js';
import * as ColorSwatch from './ColorSwatch.js';
import * as CSSAngle from './CSSAngle.js';
@@ -23,6 +24,7 @@ import * as SwatchPopoverHelper from './SwatchPopoverHelper.js';
export {
BezierEditor,
BezierUI,
ColorMixModel,
ColorMixSwatch,
ColorSwatch,
CSSAngle,
@@ -543,4 +543,21 @@ describe('StringUtilities', () => {
const lower = Platform.StringUtilities.toLowerCaseString('lower case string');
fnExpectingLowerCaseString(lower);
});
describe('replaceLast', () => {
it('should return the input string when the search is not found', () => {
const output = Platform.StringUtilities.replaceLast('input', 'search', 'repl');
assert.strictEqual(output, 'input');
});
it('should replace the occurrance when the search exists inside the input', () => {
const output = Platform.StringUtilities.replaceLast('input', 'pu', 'r');
assert.strictEqual(output, 'inrt');
});
it('should replace the last occurrence when there are multiple matches', () => {
const output = Platform.StringUtilities.replaceLast('inpuput', 'pu', 'r');
assert.strictEqual(output, 'inpurt');
});
});
});
@@ -19,7 +19,20 @@ const mockCssStyleDeclaration = {
const mockMatchedStyles = {
computeValue: () => null,
availableCSSVariables: () => [],
computeSingleVariableValue: (_: unknown, param: string) => {
const mockVariableMap: Record<string, string> = {
'var(--a)': 'red',
'var(--b)': 'blue',
};
if (!mockVariableMap[param]) {
return;
}
return {
computedValue: mockVariableMap[param],
};
},
} as unknown as SDK.CSSMatchedStyles.CSSMatchedStyles;
const mockCssProperty = {} as unknown as SDK.CSSProperty.CSSProperty;
@@ -36,7 +49,7 @@ describeWithRealConnection('StylePropertyTreeElement', async () => {
describe('updateTitle', () => {
describe('color-mix swatch', () => {
it('should show color mix swatch when color-mix is used', () => {
it('should show color mix swatch when color-mix is used with a color', () => {
const cssPropertyWithColorMix = new SDK.CSSProperty.CSSProperty(
mockCssStyleDeclaration, 0, 'color', 'color-mix(in srgb, red, blue)', true, false, true, false, '',
undefined);
@@ -52,6 +65,23 @@ describeWithRealConnection('StylePropertyTreeElement', async () => {
assertNotNullOrUndefined(colorSwatches.find(colorSwatch => colorSwatch.textContent === 'red'));
assertNotNullOrUndefined(colorSwatches.find(colorSwatch => colorSwatch.textContent === 'blue'));
});
it('should show color mix swatch when color-mix is used with a known variable', () => {
const cssPropertyWithColorMix = new SDK.CSSProperty.CSSProperty(
mockCssStyleDeclaration, 0, 'color', 'color-mix(in srgb, var(--a), var(--b))', true, false, true, false, '',
undefined);
const stylePropertyTreeElement = new Elements.StylePropertyTreeElement.StylePropertyTreeElement(
mockStylesSidebarPane, mockMatchedStyles, cssPropertyWithColorMix, false, false, false, true);
stylePropertyTreeElement.updateTitle();
const colorMixSwatch = stylePropertyTreeElement.valueElement?.querySelector('devtools-color-mix-swatch');
const cssVarSwatches =
Array.from(stylePropertyTreeElement.valueElement?.querySelectorAll('devtools-css-var-swatch') || []);
assertNotNullOrUndefined(colorMixSwatch);
assertNotNullOrUndefined(cssVarSwatches.find(cssVarSwatch => cssVarSwatch.textContent === 'var(--a)'));
assertNotNullOrUndefined(cssVarSwatches.find(cssVarSwatch => cssVarSwatch.textContent === 'var(--b)'));
});
});
});
@@ -10,6 +10,7 @@ ts_library("inline_editor") {
"BezierUI_test.ts",
"CSSAngle_test.ts",
"CSSLength_test.ts",
"ColorMixModel_test.ts",
"ColorMixSwatch_test.ts",
"ColorSwatch_test.ts",
"FontEditorUnitConverter_test.ts",
@@ -0,0 +1,287 @@
// Copyright 2023 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as InlineEditor from '../../../../../../../front_end/ui/legacy/components/inline_editor/inline_editor.js';
const {assert} = chai;
describe('ColorMixModel', () => {
it('should parse a normal case', () => {
const colorMixModel = InlineEditor.ColorMixModel.ColorMixModel.parse('color-mix(in srgb, red, blue)');
assert.deepEqual(colorMixModel?.parts, [
{
name: InlineEditor.ColorMixModel.PartName.InterpolationMethod,
value: 'in srgb',
},
{
name: InlineEditor.ColorMixModel.PartName.Param,
value: [{
name: InlineEditor.ColorMixModel.PartName.Value,
value: 'red',
}],
},
{
name: InlineEditor.ColorMixModel.PartName.Param,
value: [{name: InlineEditor.ColorMixModel.PartName.Value, value: 'blue'}],
},
]);
});
it('should extra whitespace be removed from the arguments', () => {
const colorMixModel = InlineEditor.ColorMixModel.ColorMixModel.parse('color-mix(in srgb , red , blue )');
assert.deepEqual(colorMixModel?.parts, [
{
name: InlineEditor.ColorMixModel.PartName.InterpolationMethod,
value: 'in srgb',
},
{
name: InlineEditor.ColorMixModel.PartName.Param,
value: [{
name: InlineEditor.ColorMixModel.PartName.Value,
value: 'red',
}],
},
{
name: InlineEditor.ColorMixModel.PartName.Param,
value: [{name: InlineEditor.ColorMixModel.PartName.Value, value: 'blue'}],
},
]);
});
describe('for different kinds of values', () => {
it('should parse with a variable in first position', () => {
const colorMixModel = InlineEditor.ColorMixModel.ColorMixModel.parse('color-mix(in srgb, var(--a), blue)');
assert.deepEqual(colorMixModel?.parts, [
{
name: InlineEditor.ColorMixModel.PartName.InterpolationMethod,
value: 'in srgb',
},
{
name: InlineEditor.ColorMixModel.PartName.Param,
value: [{
name: InlineEditor.ColorMixModel.PartName.Value,
value: 'var(--a)',
}],
},
{
name: InlineEditor.ColorMixModel.PartName.Param,
value: [{
name: InlineEditor.ColorMixModel.PartName.Value,
value: 'blue',
}],
},
]);
});
it('should parse with a variable in second position', () => {
const colorMixModel = InlineEditor.ColorMixModel.ColorMixModel.parse('color-mix(in srgb, blue, var(--a))');
assert.deepEqual(colorMixModel?.parts, [
{
name: InlineEditor.ColorMixModel.PartName.InterpolationMethod,
value: 'in srgb',
},
{
name: InlineEditor.ColorMixModel.PartName.Param,
value: [{
name: InlineEditor.ColorMixModel.PartName.Value,
value: 'blue',
}],
},
{
name: InlineEditor.ColorMixModel.PartName.Param,
value: [{
name: InlineEditor.ColorMixModel.PartName.Value,
value: 'var(--a)',
}],
},
]);
});
it('should parse with a color defined with rgba', () => {
const colorMixModel = InlineEditor.ColorMixModel.ColorMixModel.parse('color-mix(in srgb, rgba(0 0 0 / 1), red)');
assert.deepEqual(colorMixModel?.parts, [
{
name: InlineEditor.ColorMixModel.PartName.InterpolationMethod,
value: 'in srgb',
},
{
name: InlineEditor.ColorMixModel.PartName.Param,
value: [{
name: InlineEditor.ColorMixModel.PartName.Value,
value: 'rgba(0 0 0 / 1)',
}],
},
{
name: InlineEditor.ColorMixModel.PartName.Param,
value: [{
name: InlineEditor.ColorMixModel.PartName.Value,
value: 'red',
}],
},
]);
});
it('should parse with a color defined with color-mix', () => {
const colorMixModel =
InlineEditor.ColorMixModel.ColorMixModel.parse('color-mix(in srgb, color-mix(in lch, red, blue), red)');
assert.deepEqual(colorMixModel?.parts, [
{
name: InlineEditor.ColorMixModel.PartName.InterpolationMethod,
value: 'in srgb',
},
{
name: InlineEditor.ColorMixModel.PartName.Param,
value: [{
name: InlineEditor.ColorMixModel.PartName.Value,
value: 'color-mix(in lch, red, blue)',
}],
},
{
name: InlineEditor.ColorMixModel.PartName.Param,
value: [{
name: InlineEditor.ColorMixModel.PartName.Value,
value: 'red',
}],
},
]);
});
it('should parse with a color defined with hex values', () => {
const colorMixModel = InlineEditor.ColorMixModel.ColorMixModel.parse('color-mix(in srgb, #000000, red)');
assert.deepEqual(colorMixModel?.parts, [
{
name: InlineEditor.ColorMixModel.PartName.InterpolationMethod,
value: 'in srgb',
},
{
name: InlineEditor.ColorMixModel.PartName.Param,
value: [{
name: InlineEditor.ColorMixModel.PartName.Value,
value: '#000000',
}],
},
{
name: InlineEditor.ColorMixModel.PartName.Param,
value: [{
name: InlineEditor.ColorMixModel.PartName.Value,
value: 'red',
}],
},
]);
});
it('should parse with hue interpolation case', () => {
const colorMixModel =
InlineEditor.ColorMixModel.ColorMixModel.parse('color-mix(in srgb increasing hue, red, blue)');
assert.deepEqual(colorMixModel?.parts, [
{
name: InlineEditor.ColorMixModel.PartName.InterpolationMethod,
value: 'in srgb increasing hue',
},
{
name: InlineEditor.ColorMixModel.PartName.Param,
value: [{
name: InlineEditor.ColorMixModel.PartName.Value,
value: 'red',
}],
},
{
name: InlineEditor.ColorMixModel.PartName.Param,
value: [{
name: InlineEditor.ColorMixModel.PartName.Value,
value: 'blue',
}],
},
]);
});
});
describe('mixed with percentages', () => {
it('percentage on the right', () => {
const colorMixModel = InlineEditor.ColorMixModel.ColorMixModel.parse('color-mix(in srgb, red 20%, blue)');
assert.deepEqual(colorMixModel?.parts, [
{
name: InlineEditor.ColorMixModel.PartName.InterpolationMethod,
value: 'in srgb',
},
{
name: InlineEditor.ColorMixModel.PartName.Param,
value: [
{
name: InlineEditor.ColorMixModel.PartName.Value,
value: 'red',
},
{
name: InlineEditor.ColorMixModel.PartName.Percentage,
value: '20%',
},
],
},
{
name: InlineEditor.ColorMixModel.PartName.Param,
value: [{
name: InlineEditor.ColorMixModel.PartName.Value,
value: 'blue',
}],
},
]);
});
it('percentage on the left', () => {
const colorMixModel = InlineEditor.ColorMixModel.ColorMixModel.parse('color-mix(in srgb, 20% red, blue)');
assert.deepEqual(colorMixModel?.parts, [
{
name: InlineEditor.ColorMixModel.PartName.InterpolationMethod,
value: 'in srgb',
},
{
name: InlineEditor.ColorMixModel.PartName.Param,
value: [
{
name: InlineEditor.ColorMixModel.PartName.Percentage,
value: '20%',
},
{
name: InlineEditor.ColorMixModel.PartName.Value,
value: 'red',
},
],
},
{
name: InlineEditor.ColorMixModel.PartName.Param,
value: [{
name: InlineEditor.ColorMixModel.PartName.Value,
value: 'blue',
}],
},
]);
});
});
describe('parse in wrong syntax', () => {
it('should return null when color interpolation text does not start with in', () => {
assert.isNull(InlineEditor.ColorMixModel.ColorMixModel.parse('color-mix(srgb, red red, blue)'));
});
it('should return null when there are multiple values in one part', () => {
assert.isNull(InlineEditor.ColorMixModel.ColorMixModel.parse('color-mix(in srgb, red red, blue)'));
});
it('should return null when there are percentages values in one part', () => {
assert.isNull(InlineEditor.ColorMixModel.ColorMixModel.parse('color-mix(in srgb, red 20% 20%, blue)'));
});
});
});
@@ -10,16 +10,18 @@ import {describeWithLocale} from '../../../../helpers/EnvironmentHelpers.js';
const {assert} = chai;
function createSwatch(text: string) {
function createSwatch(text: string, firstColor: string, secondColor: string) {
const swatch = new InlineEditor.ColorMixSwatch.ColorMixSwatch();
renderElementIntoDOM(swatch);
swatch.setText(text);
swatch.setColorMixText(text);
swatch.setFirstColor(firstColor);
swatch.setSecondColor(secondColor);
return swatch;
}
describeWithLocale('ColorMixSwatch', () => {
it('should render color-mix swatch with icon and text when the syntax is correct', () => {
const swatch = createSwatch('color-mix(in srgb, red, blue)');
const swatch = createSwatch('color-mix(in srgb, red, blue)', 'red', 'blue');
const swatchIcon = swatch.shadowRoot?.querySelector('.swatch-icon');
@@ -27,12 +29,13 @@ describeWithLocale('ColorMixSwatch', () => {
assert.isNotNull(swatchIcon);
});
it('should render color-mix swatch with only text when the syntax is not correct', () => {
const swatch = createSwatch('color-mix(in srgb not, blue)');
it('should changing the second color work correctly when the colors are the same', () => {
const swatch = createSwatch('color-mix(in srgb, red, red)', 'red', 'red');
swatch.setSecondColor('blue');
const swatchIcon = swatch.shadowRoot?.querySelector('.swatch-icon');
assert.strictEqual(swatch.shadowRoot?.textContent?.trim(), 'color-mix(in srgb not, blue)');
assert.isNull(swatchIcon);
assert.strictEqual(swatch.shadowRoot?.textContent?.trim(), 'color-mix(in srgb, red, blue)');
assert.isNotNull(swatchIcon);
});
});