Skip to content

Commit d5edbb4

Browse files
committed
fix(core): reset inherited children when an InheritedProperty is unset
The reset branch of the cascade called the setter factory `setFunc` instead of a setter, so it built a new function and never wrote to the child. Descendants that had inherited the old value kept it after the source was reset to its default.
1 parent 4e9fea5 commit d5edbb4

2 files changed

Lines changed: 142 additions & 1 deletion

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -669,7 +669,7 @@ export class InheritedProperty<T extends ViewBase, U> extends Property<T, U> imp
669669
const childValueSource = child[sourceKey] || ValueSource.Default;
670670
if (reset) {
671671
if (childValueSource === ValueSource.Inherited) {
672-
setFunc.call(child, unsetValue);
672+
setInheritedValue.call(child, unsetValue);
673673
}
674674
} else {
675675
if (childValueSource <= ValueSource.Inherited) {
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { describe, it, expect } from 'vitest';
2+
3+
import { InheritedProperty, unsetValue } from './index';
4+
import { StackLayout } from '../../layouts/stack-layout';
5+
import { Label } from '../../label';
6+
7+
// Mirrors the private `ValueSource` enum in ./index.
8+
const ValueSource = {
9+
Default: 0,
10+
Inherited: 1,
11+
Css: 2,
12+
Local: 3,
13+
};
14+
15+
class TestView extends StackLayout {
16+
public testInherited: string;
17+
18+
public createNativeView(): Object {
19+
return {};
20+
}
21+
}
22+
23+
const testInheritedProperty = new InheritedProperty<TestView, string>({
24+
name: 'testInherited',
25+
defaultValue: undefined,
26+
});
27+
testInheritedProperty.register(TestView);
28+
29+
function valueSourceOf(view: any): number {
30+
return view[testInheritedProperty.sourceKey] || ValueSource.Default;
31+
}
32+
33+
function tree(): { root: TestView; child: TestView; grandChild: TestView } {
34+
const root = new TestView();
35+
const child = new TestView();
36+
const grandChild = new TestView();
37+
38+
root.addChild(child);
39+
child.addChild(grandChild);
40+
41+
return { root, child, grandChild };
42+
}
43+
44+
function recordChanges(view: any, eventName: string): unknown[] {
45+
const values: unknown[] = [];
46+
view.on(eventName, (data: any) => values.push(data.value));
47+
48+
return values;
49+
}
50+
51+
describe('InheritedProperty', () => {
52+
it('propagates a value down the tree', () => {
53+
const { root, child, grandChild } = tree();
54+
55+
root.testInherited = 'a';
56+
57+
expect(child.testInherited).toBe('a');
58+
expect(grandChild.testInherited).toBe('a');
59+
expect(valueSourceOf(root)).toBe(ValueSource.Local);
60+
expect(valueSourceOf(child)).toBe(ValueSource.Inherited);
61+
expect(valueSourceOf(grandChild)).toBe(ValueSource.Inherited);
62+
});
63+
64+
it('resets inheriting descendants when the value is unset', () => {
65+
const { root, child, grandChild } = tree();
66+
root.testInherited = 'a';
67+
68+
const childChanges = recordChanges(child, 'testInheritedChange');
69+
const grandChildChanges = recordChanges(grandChild, 'testInheritedChange');
70+
71+
root.testInherited = unsetValue;
72+
73+
expect(root.testInherited).toBeUndefined();
74+
expect(child.testInherited).toBeUndefined();
75+
expect(grandChild.testInherited).toBeUndefined();
76+
expect(valueSourceOf(root)).toBe(ValueSource.Default);
77+
expect(valueSourceOf(child)).toBe(ValueSource.Default);
78+
expect(valueSourceOf(grandChild)).toBe(ValueSource.Default);
79+
expect(childChanges).toEqual([undefined]);
80+
expect(grandChildChanges).toEqual([undefined]);
81+
});
82+
83+
it("resets inheriting descendants when the value is set to 'initial'", () => {
84+
const { root, child, grandChild } = tree();
85+
root.testInherited = 'a';
86+
87+
root.testInherited = <any>'initial';
88+
89+
expect(child.testInherited).toBeUndefined();
90+
expect(grandChild.testInherited).toBeUndefined();
91+
});
92+
93+
it('leaves a descendant that holds a local value untouched', () => {
94+
const { root, child, grandChild } = tree();
95+
root.testInherited = 'a';
96+
child.testInherited = 'local';
97+
98+
root.testInherited = unsetValue;
99+
100+
expect(child.testInherited).toBe('local');
101+
expect(valueSourceOf(child)).toBe(ValueSource.Local);
102+
expect(grandChild.testInherited).toBe('local');
103+
expect(valueSourceOf(grandChild)).toBe(ValueSource.Inherited);
104+
});
105+
106+
it('propagates again after a reset', () => {
107+
const { root, child, grandChild } = tree();
108+
root.testInherited = 'a';
109+
root.testInherited = unsetValue;
110+
111+
root.testInherited = 'b';
112+
113+
expect(child.testInherited).toBe('b');
114+
expect(grandChild.testInherited).toBe('b');
115+
expect(valueSourceOf(grandChild)).toBe(ValueSource.Inherited);
116+
});
117+
});
118+
119+
describe('bindingContext', () => {
120+
it('is reset on descendants when the root unsets it', () => {
121+
const root = new StackLayout();
122+
const container = new StackLayout();
123+
const label = new Label();
124+
root.addChild(container);
125+
container.addChild(label);
126+
127+
const context = { name: 'a' };
128+
root.bindingContext = context;
129+
expect(container.bindingContext).toBe(context);
130+
expect(label.bindingContext).toBe(context);
131+
132+
const labelChanges = recordChanges(label, 'bindingContextChange');
133+
134+
root.bindingContext = unsetValue;
135+
136+
expect(root.bindingContext).toBeUndefined();
137+
expect(container.bindingContext).toBeUndefined();
138+
expect(label.bindingContext).toBeUndefined();
139+
expect(labelChanges).toEqual([undefined]);
140+
});
141+
});

0 commit comments

Comments
 (0)