diff --git a/config/gni/devtools_grd_files.gni b/config/gni/devtools_grd_files.gni index 0b87570eea..479d245265 100644 --- a/config/gni/devtools_grd_files.gni +++ b/config/gni/devtools_grd_files.gni @@ -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", diff --git a/front_end/core/common/Color.ts b/front_end/core/common/Color.ts index bb9c6c5e7e..8c5bc49dfe 100644 --- a/front_end/core/common/Color.ts +++ b/front_end/core/common/Color.ts @@ -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*(?.+)\s*,\s*(?.+)\s*\)/g; export const enum Format { Nickname = 'nickname', HEX = 'hex', diff --git a/front_end/core/platform/string-utilities.ts b/front_end/core/platform/string-utilities.ts index 78987459f2..49cac6f382 100644 --- a/front_end/core/platform/string-utilities.ts +++ b/front_end/core/platform/string-utilities.ts @@ -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); +}; diff --git a/front_end/panels/elements/StylePropertyTreeElement.ts b/front_end/panels/elements/StylePropertyTreeElement.ts index 639bfdf3c1..1ea146a325 100644 --- a/front_end/panels/elements/StylePropertyTreeElement.ts +++ b/front_end/panels/elements/StylePropertyTreeElement.ts @@ -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; } diff --git a/front_end/panels/elements/StylesSidebarPane.ts b/front_end/panels/elements/StylesSidebarPane.ts index 4f5253f6a2..fb04ac3642 100644 --- a/front_end/panels/elements/StylesSidebarPane.ts +++ b/front_end/panels/elements/StylesSidebarPane.ts @@ -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 diff --git a/front_end/ui/legacy/components/inline_editor/BUILD.gn b/front_end/ui/legacy/components/inline_editor/BUILD.gn index 7aca5f4c4e..32e2b3af27 100644 --- a/front_end/ui/legacy/components/inline_editor/BUILD.gn +++ b/front_end/ui/legacy/components/inline_editor/BUILD.gn @@ -38,6 +38,7 @@ devtools_module("inline_editor") { "CSSLengthUtils.ts", "CSSShadowEditor.ts", "CSSShadowModel.ts", + "ColorMixModel.ts", "ColorMixSwatch.ts", "ColorSwatch.ts", "FontEditor.ts", diff --git a/front_end/ui/legacy/components/inline_editor/ColorMixModel.ts b/front_end/ui/legacy/components/inline_editor/ColorMixModel.ts new file mode 100644 index 0000000000..701e357db2 --- /dev/null +++ b/front_end/ui/legacy/components/inline_editor/ColorMixModel.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( , [ && ? ]#{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 + 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[], + }, +]; diff --git a/front_end/ui/legacy/components/inline_editor/ColorMixSwatch.ts b/front_end/ui/legacy/components/inline_editor/ColorMixSwatch.ts index cbbf29ba4a..bdd8f71f8c 100644 --- a/front_end/ui/legacy/components/inline_editor/ColorMixSwatch.ts +++ b/front_end/ui/legacy/components/inline_editor/ColorMixSwatch.ts @@ -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*,(?.+),(?.+)\)/); - 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`
- -
${this.text}`, + + ${this.colorMixText}`, this.shadow, {host: this}); // clang-format on } diff --git a/front_end/ui/legacy/components/inline_editor/inline_editor.ts b/front_end/ui/legacy/components/inline_editor/inline_editor.ts index a1d57490f3..89b0f0182a 100644 --- a/front_end/ui/legacy/components/inline_editor/inline_editor.ts +++ b/front_end/ui/legacy/components/inline_editor/inline_editor.ts @@ -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, diff --git a/test/unittests/front_end/core/platform/StringUtilities_test.ts b/test/unittests/front_end/core/platform/StringUtilities_test.ts index 188cca16e7..60f18baf00 100644 --- a/test/unittests/front_end/core/platform/StringUtilities_test.ts +++ b/test/unittests/front_end/core/platform/StringUtilities_test.ts @@ -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'); + }); + }); }); diff --git a/test/unittests/front_end/panels/elements/StylePropertyTreeElement_test.ts b/test/unittests/front_end/panels/elements/StylePropertyTreeElement_test.ts index 76f28eb972..c836d7d1a0 100644 --- a/test/unittests/front_end/panels/elements/StylePropertyTreeElement_test.ts +++ b/test/unittests/front_end/panels/elements/StylePropertyTreeElement_test.ts @@ -19,7 +19,20 @@ const mockCssStyleDeclaration = { const mockMatchedStyles = { computeValue: () => null, - availableCSSVariables: () => [], + computeSingleVariableValue: (_: unknown, param: string) => { + const mockVariableMap: Record = { + '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)')); + }); }); }); diff --git a/test/unittests/front_end/ui/legacy/components/inline_editor/BUILD.gn b/test/unittests/front_end/ui/legacy/components/inline_editor/BUILD.gn index d8eb7a923e..b2469a6771 100644 --- a/test/unittests/front_end/ui/legacy/components/inline_editor/BUILD.gn +++ b/test/unittests/front_end/ui/legacy/components/inline_editor/BUILD.gn @@ -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", diff --git a/test/unittests/front_end/ui/legacy/components/inline_editor/ColorMixModel_test.ts b/test/unittests/front_end/ui/legacy/components/inline_editor/ColorMixModel_test.ts new file mode 100644 index 0000000000..6c2463f92c --- /dev/null +++ b/test/unittests/front_end/ui/legacy/components/inline_editor/ColorMixModel_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)')); + }); + }); +}); diff --git a/test/unittests/front_end/ui/legacy/components/inline_editor/ColorMixSwatch_test.ts b/test/unittests/front_end/ui/legacy/components/inline_editor/ColorMixSwatch_test.ts index b4fabce093..2a06f801a0 100644 --- a/test/unittests/front_end/ui/legacy/components/inline_editor/ColorMixSwatch_test.ts +++ b/test/unittests/front_end/ui/legacy/components/inline_editor/ColorMixSwatch_test.ts @@ -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); }); });