Skip to content

Commit 447ce6c

Browse files
committed
fix(core): apply systemAppearanceChanged() while automatic changes are off
Application.systemAppearanceChanged(rootView, appearance) did nothing while setAutoSystemAppearanceChanged(false) was in effect, so an app that turned off automatic changes to choose its own appearance had to turn them back on around each call. The flag now gates only the automatic path, where the system's appearance changes; the event still fires there. An explicit call to systemAppearanceChanged() applies the classes.
1 parent f98fc5b commit 447ce6c

2 files changed

Lines changed: 153 additions & 3 deletions

File tree

‎packages/core/application/application-common.ts‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1030,7 +1030,11 @@ export class ApplicationCommon {
10301030
return;
10311031
}
10321032
this._systemAppearance = value;
1033-
this.systemAppearanceChanged(this.getRootView(), value);
1033+
// `autoSystemAppearanceChanged` opts out of the CSS classes only: the event
1034+
// still fires so apps driving their own theming can react to it.
1035+
if (this.autoSystemAppearanceChanged) {
1036+
this.systemAppearanceChanged(this.getRootView(), value);
1037+
}
10341038
this.notify(<SystemAppearanceChangedEventData>{
10351039
eventName: this.systemAppearanceChangedEvent,
10361040
android: this.android,
@@ -1055,12 +1059,15 @@ export class ApplicationCommon {
10551059
}
10561060

10571061
/**
1058-
* Updates root view classes including those of modals
1062+
* Updates root view classes including those of modals.
1063+
* Applies them even when automatic updates are disabled with
1064+
* `setAutoSystemAppearanceChanged(false)`, so an app can set its own
1065+
* appearance.
10591066
* @param rootView the root view
10601067
* @param newSystemAppearance the new appearance change
10611068
*/
10621069
systemAppearanceChanged(rootView: View, newSystemAppearance: 'dark' | 'light'): void {
1063-
if (!rootView || !this.autoSystemAppearanceChanged) {
1070+
if (!rootView) {
10641071
return;
10651072
}
10661073

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2+
3+
// `application.android.ts` registers deferred event wiring while it loads, through a hook the
4+
// `NativeScriptGlobals` stub in `vitest.setup.ts` does not carry. Hoisted so it lands before
5+
// the imports below.
6+
vi.hoisted(() => {
7+
(globalThis as any).NativeScriptGlobals.addEventWiring = () => {};
8+
});
9+
10+
import { CoreTypes } from '../core-types';
11+
import { Observable } from '../data/observable';
12+
import type { View } from '../ui/core/view';
13+
import { ApplicationCommon } from './application-common';
14+
import { AndroidApplication } from './application.android';
15+
import { setNativeApp } from './helpers-common';
16+
17+
/** See application-registry.spec.ts: makes the application's events observable. */
18+
function installApplicationEventBus(): Observable {
19+
const events = new Observable();
20+
const bus = (global.NativeScriptGlobals as any).events;
21+
22+
bus.on = events.on.bind(events);
23+
bus.once = events.once.bind(events);
24+
bus.off = events.off.bind(events);
25+
bus.notify = events.notify.bind(events);
26+
bus.hasListeners = events.hasListeners.bind(events);
27+
28+
return events;
29+
}
30+
31+
function createRootView() {
32+
return {
33+
cssClasses: new Set<string>(),
34+
_onCssStateChange: vi.fn(),
35+
_getRootModalViews: () => [],
36+
};
37+
}
38+
39+
class TestApplication extends ApplicationCommon {
40+
rootView = createRootView();
41+
42+
getRootView(): View {
43+
return this.rootView as unknown as View;
44+
}
45+
46+
getOrientation(): 'portrait' | 'landscape' | 'unknown' {
47+
return 'portrait';
48+
}
49+
50+
getSystemAppearance(): 'dark' | 'light' | null {
51+
return 'light';
52+
}
53+
54+
getLayoutDirection(): CoreTypes.LayoutDirectionType | null {
55+
return CoreTypes.LayoutDirection.ltr;
56+
}
57+
58+
changeSystemAppearance(value: 'dark' | 'light') {
59+
this.setSystemAppearance(value);
60+
}
61+
}
62+
63+
describe('system appearance', () => {
64+
let app: TestApplication;
65+
66+
beforeEach(() => {
67+
installApplicationEventBus();
68+
app = new TestApplication();
69+
});
70+
71+
afterEach(() => {
72+
app.setAutoSystemAppearanceChanged(true);
73+
installApplicationEventBus();
74+
});
75+
76+
it('applies the appearance an app sets while automatic changes are off', () => {
77+
app.setAutoSystemAppearanceChanged(false);
78+
79+
app.systemAppearanceChanged(app.getRootView(), 'dark');
80+
81+
expect(app.rootView.cssClasses.has('ns-dark')).toBe(true);
82+
expect(app.rootView._onCssStateChange).toHaveBeenCalled();
83+
});
84+
85+
it('leaves the classes alone when the system changes while automatic changes are off', () => {
86+
const changes: string[] = [];
87+
app.on(app.systemAppearanceChangedEvent, (args: any) => changes.push(args.newValue));
88+
app.setAutoSystemAppearanceChanged(false);
89+
90+
app.changeSystemAppearance('dark');
91+
92+
expect(app.rootView.cssClasses.has('ns-dark')).toBe(false);
93+
expect(changes).toEqual(['dark']);
94+
});
95+
96+
it('applies system changes while automatic changes are on', () => {
97+
app.changeSystemAppearance('dark');
98+
99+
expect(app.rootView.cssClasses.has('ns-dark')).toBe(true);
100+
});
101+
});
102+
103+
describe('Android application traits before the app has started', () => {
104+
const UI_MODE_NIGHT_YES = 0x20;
105+
let globals: Record<string, any>;
106+
107+
beforeEach(() => {
108+
installApplicationEventBus();
109+
globals = { android: (global as any).android };
110+
(global as any).android = {
111+
...globals.android,
112+
content: {
113+
...globals.android?.content,
114+
res: {
115+
Configuration: {
116+
UI_MODE_NIGHT_MASK: 0x30,
117+
UI_MODE_NIGHT_UNDEFINED: 0x00,
118+
UI_MODE_NIGHT_NO: 0x10,
119+
UI_MODE_NIGHT_YES,
120+
ORIENTATION_PORTRAIT: 1,
121+
ORIENTATION_LANDSCAPE: 2,
122+
},
123+
},
124+
},
125+
};
126+
const configuration = { uiMode: 0x01 | UI_MODE_NIGHT_YES, orientation: 2 };
127+
setNativeApp({ getResources: () => ({ getConfiguration: () => configuration }) } as any);
128+
});
129+
130+
afterEach(() => {
131+
(global as any).android = globals.android;
132+
setNativeApp(undefined);
133+
installApplicationEventBus();
134+
});
135+
136+
it('reads them from the native application', () => {
137+
const app = new AndroidApplication();
138+
139+
expect(app.context).toBeUndefined();
140+
expect(app.systemAppearance()).toBe('dark');
141+
expect(app.orientation()).toBe('landscape');
142+
});
143+
});

0 commit comments

Comments
 (0)