Skip to content

Commit 25ed6b5

Browse files
committed
feat(core): Enhanced CSS variables resolution
1 parent bca3452 commit 25ed6b5

File tree

3 files changed

+101
-61
lines changed

3 files changed

+101
-61
lines changed

apps/automated/src/ui/styling/style-tests.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1927,6 +1927,27 @@ export function test_css_variables() {
19271927
TKUnit.assertEqual((<Color>label.backgroundColor).hex, redColor, 'Label - background-color is red');
19281928
}
19291929

1930+
export function test_undefined_css_variable_invalidates_entire_expression() {
1931+
const page = helper.getClearCurrentPage();
1932+
1933+
const cssVarName = `--my-background-color-${Date.now()}`;
1934+
1935+
const stack = new StackLayout();
1936+
stack.css = `
1937+
Label.lab1 {
1938+
box-shadow: 10 10 5 12 var(${cssVarName});
1939+
color: black;
1940+
}`;
1941+
1942+
const label = new Label();
1943+
page.content = stack;
1944+
stack.addChild(label);
1945+
1946+
label.className = 'lab1';
1947+
1948+
TKUnit.assertEqual(label.style.boxShadow, undefined, 'the css variable is undefined');
1949+
}
1950+
19301951
export function test_css_calc_and_variables() {
19311952
const page = helper.getClearCurrentPage();
19321953

@@ -1969,6 +1990,7 @@ export function test_css_calc_and_variables() {
19691990

19701991
export function test_css_variable_fallback() {
19711992
const redColor = '#FF0000';
1993+
const greeColor = '#008000';
19721994
const blueColor = '#0000FF';
19731995
const limeColor = new Color('lime').hex;
19741996
const yellowColor = new Color('yellow').hex;
@@ -1996,7 +2018,7 @@ export function test_css_variable_fallback() {
19962018
},
19972019
{
19982020
className: 'undefined-css-variable-with-multiple-fallbacks',
1999-
expectedColor: limeColor,
2021+
expectedColor: greeColor,
20002022
},
20012023
{
20022024
className: 'undefined-css-variable-with-missing-fallback-value',
@@ -2036,8 +2058,7 @@ export function test_css_variable_fallback() {
20362058
}
20372059
20382060
.undefined-css-variable-with-multiple-fallbacks {
2039-
--my-fallback-var: lime;
2040-
color: var(--undefined-var, var(--my-fallback-var), yellow); /* resolved as color: lime; */
2061+
color: var(--undefined-var, var(--my-fallback-var), green); /* resolved as color: green; */
20412062
}
20422063
20432064
.undefined-css-variable-with-missing-fallback-value {

packages/core/ui/core/properties/index.ts

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export interface CssAnimationPropertyOptions<T, U> {
4848
const cssPropertyNames: string[] = [];
4949
const symbolPropertyMap = {};
5050
const cssSymbolPropertyMap = {};
51+
const cssErrorVarPlaceHolder = '&error_var';
5152

5253
const inheritableProperties = new Array<InheritedProperty<any, any>>();
5354
const inheritableCssProperties = new Array<InheritedCssProperty<any, any>>();
@@ -107,20 +108,12 @@ export function isCssWideKeyword(value: any): value is CoreTypes.CSSWideKeywords
107108
return value === 'initial' || value === 'inherit' || isCssUnsetValue(value);
108109
}
109110

110-
export function _evaluateCssVariableExpression(view: ViewBase, cssName: string, value: string): string {
111-
if (typeof value !== 'string') {
112-
return value;
113-
}
114-
115-
if (!isCssVariableExpression(value)) {
116-
// Value is not using css-variable(s)
117-
return value;
118-
}
119-
111+
export function _evaluateCssVariableExpression(view: ViewBase, value: string, onCssVarExpressionParse?: (cssVarName: string) => void): string {
120112
let output = value.trim();
121113

122-
// Evaluate every (and nested) css-variables in the value.
114+
// Evaluate every (and nested) css-variables in the value
123115
let lastValue: string;
116+
124117
while (lastValue !== output) {
125118
lastValue = output;
126119

@@ -140,31 +133,35 @@ export function _evaluateCssVariableExpression(view: ViewBase, cssName: string,
140133
.map((v) => v.trim())
141134
.filter((v) => !!v);
142135
const cssVariableName = matched.shift();
136+
137+
// Execute the callback early to allow operations like preloading missing variables
138+
if (onCssVarExpressionParse) {
139+
onCssVarExpressionParse(cssVariableName);
140+
}
141+
143142
let cssVariableValue = view.style.getCssVariable(cssVariableName);
144-
if (cssVariableValue === null && matched.length) {
145-
cssVariableValue = _evaluateCssVariableExpression(view, cssName, matched.join(', ')).split(',')[0];
143+
if (cssVariableValue == null && matched.length) {
144+
for (const cssVal of matched) {
145+
if (cssVal && !cssVal.includes(cssErrorVarPlaceHolder)) {
146+
cssVariableValue = cssVal;
147+
break;
148+
}
149+
}
146150
}
147151

148152
if (!cssVariableValue) {
149-
cssVariableValue = 'unset';
153+
cssVariableValue = cssErrorVarPlaceHolder;
150154
}
151155

152156
output = `${output.substring(0, idx)}${cssVariableValue}${output.substring(endIdx + 1)}`;
153157
}
154158

155-
return output;
159+
// If at least one variable failed to resolve, discard the whole expression
160+
return output.includes(cssErrorVarPlaceHolder) ? undefined : output;
156161
}
157162

158163
export function _evaluateCssCalcExpression(value: string) {
159-
if (typeof value !== 'string') {
160-
return value;
161-
}
162-
163-
if (isCssCalcExpression(value)) {
164-
return require('@csstools/css-calc').calc(_replaceKeywordsWithValues(_replaceDip(value)));
165-
} else {
166-
return value;
167-
}
164+
return require('@csstools/css-calc').calc(_replaceKeywordsWithValues(_replaceDip(value)));
168165
}
169166

170167
function _replaceDip(value: string) {

packages/core/ui/styling/style-scope.ts

Lines changed: 56 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -63,22 +63,29 @@ const kebabCasePattern = /-([a-z])/g;
6363
const pattern = /('|")(.*?)\1/;
6464

6565
/**
66-
* Evaluate css-variable and css-calc expressions
66+
* Evaluate css-variable and css-calc expressions.
6767
*/
68-
function evaluateCssExpressions(view: ViewBase, property: string, value: string) {
69-
const newValue = _evaluateCssVariableExpression(view, property, value);
70-
if (newValue === 'unset') {
71-
return unsetValue;
68+
function evaluateCssExpressions(view: ViewBase, property: string, value: string, onCssVarExpressionParse?: (cssVarName: string) => void) {
69+
if (typeof value !== 'string') {
70+
return value;
7271
}
7372

74-
value = newValue;
73+
if (isCssVariableExpression(value)) {
74+
const newValue = _evaluateCssVariableExpression(view, value, onCssVarExpressionParse);
75+
if (newValue === undefined) {
76+
return unsetValue;
77+
}
7578

76-
try {
77-
value = _evaluateCssCalcExpression(value);
78-
} catch (e) {
79-
Trace.write(`Failed to evaluate css-calc for property [${property}] for expression [${value}] to ${view}. ${e.stack}`, Trace.categories.Error, Trace.messageType.error);
79+
value = newValue;
80+
}
8081

81-
return unsetValue;
82+
if (isCssCalcExpression(value)) {
83+
try {
84+
value = _evaluateCssCalcExpression(value);
85+
} catch (e) {
86+
Trace.write(`Failed to evaluate css-calc for property [${property}] for expression [${value}] to ${view}. ${e.stack}`, Trace.categories.Error, Trace.messageType.error);
87+
return unsetValue;
88+
}
8289
}
8390

8491
return value;
@@ -710,48 +717,63 @@ export class CssState {
710717
const valuesToApply = {};
711718
const cssExpsProperties = {};
712719
const replacementFunc = (g) => g[1].toUpperCase();
720+
const onCssVarExpressionParse = (cssVarName: string) => {
721+
// If variable name is still in the property bag, parse its value and apply it to the view
722+
if (cssVarName in cssExpsProperties) {
723+
cssExpsPropsCallback(cssVarName);
724+
}
725+
};
726+
// This callback is also used for handling nested css variable expressions
727+
const cssExpsPropsCallback = (property: string) => {
728+
let value = cssExpsProperties[property];
713729

714-
for (const property in newPropertyValues) {
715-
const value = cleanupImportantFlags(newPropertyValues[property], property);
730+
// Remove the property first to avoid recalculating it in a later step using lazy loading
731+
delete cssExpsProperties[property];
716732

717-
const isCssExp = isCssVariableExpression(value) || isCssCalcExpression(value);
733+
value = evaluateCssExpressions(view, property, value, onCssVarExpressionParse);
718734

719-
if (isCssExp) {
720-
// we handle css exp separately because css vars must be evaluated first
721-
cssExpsProperties[property] = value;
722-
continue;
723-
}
724-
delete oldProperties[property];
725-
if (property in oldProperties && oldProperties[property] === value) {
726-
// Skip unchanged values
727-
continue;
735+
if (value === unsetValue) {
736+
delete newPropertyValues[property];
728737
}
738+
729739
if (isCssVariable(property)) {
730740
view.style.setScopedCssVariable(property, value);
731741
delete newPropertyValues[property];
732-
continue;
733742
}
743+
734744
valuesToApply[property] = value;
735-
}
736-
//we need to parse CSS vars first before evaluating css expressions
737-
for (const property in cssExpsProperties) {
745+
};
746+
747+
for (const property in newPropertyValues) {
748+
const value = cleanupImportantFlags(newPropertyValues[property], property);
749+
const isCssExp = isCssVariableExpression(value) || isCssCalcExpression(value);
750+
751+
// We can skip the unset part of old values since they will be overwritten by new values
738752
delete oldProperties[property];
739-
const value = evaluateCssExpressions(view, property, cssExpsProperties[property]);
740-
if (property in oldProperties && oldProperties[property] === value) {
741-
// Skip unchanged values
753+
754+
if (isCssExp) {
755+
// We handle css exp separately because css vars must be evaluated first
756+
cssExpsProperties[property] = value;
742757
continue;
743758
}
744-
if (value === unsetValue) {
745-
delete newPropertyValues[property];
746-
}
759+
747760
if (isCssVariable(property)) {
748761
view.style.setScopedCssVariable(property, value);
749762
delete newPropertyValues[property];
763+
continue;
750764
}
751-
752765
valuesToApply[property] = value;
753766
}
754767

768+
const cssExpsPropKeys = Object.keys(cssExpsProperties);
769+
770+
// We need to parse CSS vars first before evaluating css expressions
771+
for (const property of cssExpsPropKeys) {
772+
if (property in cssExpsProperties) {
773+
cssExpsPropsCallback(property);
774+
}
775+
}
776+
755777
// Unset removed values
756778
for (const property in oldProperties) {
757779
if (property in view.style) {

0 commit comments

Comments
 (0)