Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions apps/automated/src/ui/styling/value-source-tests.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as helper from '../../ui-helper';
import * as TKUnit from '../../tk-unit';
import { Color, Button, StackLayout, unsetValue } from '@nativescript/core';

export var test_value_Inherited_after_unset = function () {
Expand Down Expand Up @@ -51,5 +50,5 @@ export function test_value_Local_stronger_than_Css() {
btn.style.color = new Color('#0000FF');
helper.assertViewColor(btn, '#0000FF');
btn.style.color = unsetValue;
TKUnit.assertEqual(btn.style.color, undefined, 'style.color should be undefined when set locally.');
helper.assertViewColor(btn, '#FF0000');
}
211 changes: 211 additions & 0 deletions packages/core/ui/core/properties/css-property-local-reset.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import { describe, it, expect } from 'vitest';

import { CssProperty, unsetValue } from '.';
import { View } from '../view';
import { Label } from '../../label';
import { StackLayout } from '../../layouts/stack-layout';
import { Style } from '../../styling/style';
import { StyleScope } from '../../styling/style-scope';
import { marginTopProperty } from '../../styling/style-properties';
import { PropertyChangeData } from '../../../data/observable';

const NATIVE_DEFAULT = 'native-default';

class NativeView extends View {
public nativeWrites: unknown[] = [];

public createNativeView() {
return {};
}
}

const nativeTestProperty = new CssProperty<Style, string>({
name: 'nativeTest',
cssName: 'native-test',
defaultValue: 'default',
});
nativeTestProperty.register(Style);

NativeView.prototype[nativeTestProperty.getDefault] = () => NATIVE_DEFAULT;
NativeView.prototype[nativeTestProperty.setNative] = function (this: NativeView, value: unknown) {
this.nativeWrites.push(value);
};

function loaded<T extends View>(css: string, view: T, parent: View = new StackLayout()): T {
const scope = new StyleScope();
scope.css = css;

(<any>parent)._styleScope = scope;
(<any>parent)._cssState.onLoaded();

parent._addView(view);
(<any>view)._styleScope = scope;
(<any>view)._cssState.onLoaded();

return view;
}

/** Lets the setters reach the native view without creating one. */
function withNativeUpdates<T extends View>(view: T): T {
(<any>view)._suspendNativeUpdatesCount = 0;

return view;
}

describe('CssProperty local reset', () => {
it.each([
['unsetValue', unsetValue],
["'unset'", 'unset'],
["'initial'", 'initial'],
["''", ''],
])('restores the css value when reset with %s', (_name, reset) => {
const view = loaded('label { margin-top: 4; }', new Label());

view.style.marginTop = 20;
view.style.marginTop = <any>reset;

expect(view.style.marginTop).toBe(4);
});

it('restores the css value when reset through the view', () => {
const view = loaded('label { margin-top: 4; }', new Label());

view.marginTop = 20;
view.marginTop = <any>unsetValue;

expect(view.marginTop).toBe(4);
});

it('restores a css value written through a shorthand', () => {
const view = loaded('label { margin: 4; }', new Label());

view.style.marginTop = 20;
view.style.marginTop = unsetValue;

expect(view.style.marginTop).toBe(4);
});

it('restores an evaluated css variable', () => {
const view = loaded('label { --gap: 4; margin-top: var(--gap); }', new Label());

view.style.marginTop = 20;
view.style.marginTop = unsetValue;

expect(view.style.marginTop).toBe(4);
});

it('restores a css value the local value shadowed before css ran', () => {
const view = new Label();
view.style.marginTop = 20;
loaded('label { margin-top: 4; }', view);
expect(view.style.marginTop).toBe(20);

view.style.marginTop = unsetValue;

expect(view.style.marginTop).toBe(4);
});

it('falls back to the default when no css rule sets the property', () => {
const view = loaded('label { color: red; }', new Label());

view.style.marginTop = 20;
view.style.marginTop = unsetValue;

expect(view.style.marginTop).toBe(marginTopProperty.defaultValue);
});

it('lets the css own the property again', () => {
const view = loaded('label { margin-top: 4; } .wide { margin-top: 8; }', new Label());

view.style.marginTop = 20;
view.style.marginTop = unsetValue;
view.className = 'wide';
(<any>view)._cssState.onChange();
(<any>view)._cssState.onLoaded();

expect(view.style.marginTop).toBe(8);
});

it('notifies the css value as a script change', () => {
const view = loaded('label { margin-top: 4; }', new Label());
view.style.marginTop = 20;

const events: PropertyChangeData[] = [];
view.style.on('marginTopChange', (data: PropertyChangeData) => events.push(data));

view.style.marginTop = unsetValue;

expect(events.map((e) => [e.value, e.oldValue, e.origin])).toEqual([[4, 20, 'script']]);
});

it('writes the css value to the native view', () => {
const view = withNativeUpdates(loaded('* { native-test: from-css; }', new NativeView()));

view.style['nativeTest'] = 'local';
view.style['nativeTest'] = unsetValue;

expect(view.nativeWrites).toEqual(['local', 'from-css']);
expect(view.style['nativeTest']).toBe('from-css');
});

it('restores the native default when no css rule sets the property', () => {
const view = withNativeUpdates(loaded('* { color: red; }', new NativeView()));

view.style['nativeTest'] = 'local';
view.style['nativeTest'] = unsetValue;

expect(view.nativeWrites).toEqual(['local', NATIVE_DEFAULT]);
expect(view.style['nativeTest']).toBe('default');
});
});

describe('InheritedCssProperty local reset', () => {
it('restores the css value', () => {
const view = loaded('label { color: red; }', new Label());

view.style.color = <any>'blue';
view.style.color = unsetValue;

expect(view.style.color.toString()).toBe('#FF0000');
});

it('prefers the css value over the inherited one', () => {
const parent = new StackLayout();
parent.style.color = <any>'green';
const view = loaded('label { color: red; }', new Label(), parent);

view.style.color = <any>'blue';
view.style.color = unsetValue;

expect(view.style.color.toString()).toBe('#FF0000');
});

it('inherits again when no css rule sets the property', () => {
const parent = new StackLayout();
parent.style.color = <any>'green';
const view = loaded('label { margin-top: 4; }', new Label(), parent);

view.style.color = <any>'blue';
view.style.color = unsetValue;

expect(view.style.color.toString()).toBe('#008000');
});

it('passes the restored css value down to inheriting children', () => {
const middle = loaded('.middle { color: red; }', new StackLayout());
middle.className = 'middle';
(<any>middle)._cssState.onChange();
(<any>middle)._cssState.onLoaded();

const child = new Label();
middle.addChild(child);

middle.style.color = <any>'blue';
expect(child.style.color.toString()).toBe('#0000FF');

middle.style.color = unsetValue;

expect(middle.style.color.toString()).toBe('#FF0000');
expect(child.style.color.toString()).toBe('#FF0000');
});
});
43 changes: 37 additions & 6 deletions packages/core/ui/core/properties/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ const cssValueSourceKeys: Record<string, symbol> = Object.create(null);
/**
* Whether the style still carries what the cascade last wrote for the property.
* A `CssProperty` keeps one value: a local value both suppresses the css write and
* takes the slot, so clearing it leaves the property at its default with the css
* value gone. The cascade has to write it again rather than skip it as unchanged.
* takes the slot, so the style can stop carrying what the cascade recorded. The
* cascade has to write it again rather than skip it as unchanged.
*/
export function _isCssValueStillApplied(style: unknown, cssLocalName: string, value: unknown): boolean {
const sourceKey = cssValueSourceKeys[cssLocalName];
Expand All @@ -55,6 +55,16 @@ export function _isCssValueStillApplied(style: unknown, cssLocalName: string, va
return style[sourceKey] === (isResetValue(value) ? undefined : ValueSource.Css);
}

/**
* What a released local value falls back to: the value the cascade last resolved for
* the property, or `undefined` when it resolved none.
*/
function cascadedCssValue(view: ViewBase, cssLocalName: string): unknown {
const value = view._cssState?._getAppliedPropertyValue(cssLocalName);

return value === '' || isResetValue(value) ? undefined : value;
}

function print(map) {
const symbols = Object.getOwnPropertySymbols(map);
for (const symbol of symbols) {
Expand Down Expand Up @@ -780,14 +790,24 @@ export class CssProperty<T extends Style, U> {

this._localValueVersion++;

const reset = isResetValue(newValue) || newValue === '';
let reset = isResetValue(newValue) || newValue === '';
let valueSource = ValueSource.Local;
let value: U;

if (reset) {
const cssValue = cascadedCssValue(view, property.cssLocalName);
if (cssValue !== undefined) {
newValue = <U | string>cssValue;
valueSource = ValueSource.Css;
reset = false;
}
}

if (reset) {
value = defaultValue;
delete this[sourceKey];
} else {
this[sourceKey] = ValueSource.Local;
this[sourceKey] = valueSource;
value = valueConverter && typeof newValue === 'string' ? (overrideConverter ? valueConverter.call(this, newValue) : valueConverter(newValue)) : <U>newValue;
}

Expand Down Expand Up @@ -1210,6 +1230,7 @@ export class InheritedCssProperty<T extends Style, U> extends CssProperty<T, U>
const getDefault = this.getDefault;
const setNative = this.setNative;
const defaultValueKey = this.defaultValueKey;
const cssLocalName = this.cssLocalName;
const eventName = propertyName + 'Change';
const defaultValue: U = options.defaultValue;

Expand Down Expand Up @@ -1253,7 +1274,17 @@ export class InheritedCssProperty<T extends Style, U> extends CssProperty<T, U>
this._localValueVersion++;
}

const reset = isResetValue(boxedValue) || boxedValue === '';
let reset = isResetValue(boxedValue) || boxedValue === '';
let newValueSource = valueSource;
if (reset && isLocalWrite) {
const cssValue = cascadedCssValue(view, cssLocalName);
if (cssValue !== undefined) {
boxedValue = cssValue;
newValueSource = ValueSource.Css;
reset = false;
}
}

const currentValueSource: number = this[sourceKey] || ValueSource.Default;

if (reset) {
Expand Down Expand Up @@ -1286,7 +1317,7 @@ export class InheritedCssProperty<T extends Style, U> extends CssProperty<T, U>
unsetNativeValue = true;
}
} else {
this[sourceKey] = valueSource;
this[sourceKey] = newValueSource;
if (valueConverter && typeof boxedValue === 'string') {
value = overrideConverter ? valueConverter.call(this, boxedValue) : valueConverter(boxedValue);
} else {
Expand Down
8 changes: 8 additions & 0 deletions packages/core/ui/styling/style-scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,14 @@ export class CssState {
return this.viewRef.get()._styleScope.getSelectorsVersion() === this._appliedSelectorsVersion;
}

/**
* The value the cascade last resolved for a longhand, `undefined` if it resolved none.
* Recorded even while a local value suppresses the write.
*/
public _getAppliedPropertyValue(property: string): unknown {
return this._appliedPropertyValues[property];
}

public onLoaded(): void {
if (this._matchInvalid) {
this.updateMatch();
Expand Down
Loading