Skip to content

Commit 5fe24d1

Browse files
authored
feat(base): propagate theme and language changes across runtimes (#13296)
1 parent ca81ad3 commit 5fe24d1

4 files changed

Lines changed: 149 additions & 2 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { fireConfigChange, attachConfigChange, getSharedValue } from "../../src/config/ConfigurationSync.js";
2+
import { setTheme, getTheme } from "../../src/config/Theme.js";
3+
import { setLanguage, getLanguage } from "../../src/config/Language.js";
4+
import EventProvider from "../../src/EventProvider.js";
5+
import getSharedResource from "../../src/getSharedResource.js";
6+
7+
describe("ConfigurationSync", () => {
8+
describe("Shared value storage", () => {
9+
it("fireConfigChange stores values readable via getSharedValue", () => {
10+
fireConfigChange("testSetting", "testValue");
11+
12+
cy.wrap({ getSharedValue })
13+
.invoke("getSharedValue", "testSetting")
14+
.should("equal", "testValue");
15+
});
16+
17+
it("getSharedValue returns undefined for unknown settings", () => {
18+
cy.wrap({ getSharedValue })
19+
.invoke("getSharedValue", "nonExistent")
20+
.should("equal", undefined);
21+
});
22+
});
23+
24+
describe("Skip-guard", () => {
25+
it("handler is NOT called when the same runtime fires", () => {
26+
const handler = cy.stub().as("handler");
27+
attachConfigChange("skipTest", handler);
28+
29+
fireConfigChange("skipTest", "value");
30+
31+
cy.get("@handler").should("not.have.been.called");
32+
});
33+
});
34+
35+
describe("Cross-runtime handler", () => {
36+
it("handler is called only for its own setting name", () => {
37+
const handlerA = cy.stub().as("handlerA");
38+
const handlerB = cy.stub().as("handlerB");
39+
attachConfigChange("settingA", handlerA);
40+
attachConfigChange("settingB", handlerB);
41+
42+
// Simulate a cross-runtime fire by calling the shared EventProvider directly,
43+
// bypassing the skip-guard that fireConfigChange sets for the current runtime.
44+
const ep = getSharedResource("ConfigChange.eventProvider", new EventProvider());
45+
ep.fireEvent("configChange", { name: "settingA", value: "cross-value" });
46+
47+
cy.get("@handlerA").should("have.been.calledOnce").and("have.been.calledWith", "cross-value");
48+
cy.get("@handlerB").should("not.have.been.called");
49+
});
50+
});
51+
52+
describe("Theme integration", () => {
53+
it("setTheme stores value in shared map", () => {
54+
cy.wrap({ setTheme })
55+
.invoke("setTheme", "sap_horizon_hcb");
56+
57+
cy.wrap({ getSharedValue })
58+
.invoke("getSharedValue", "theme")
59+
.should("equal", "sap_horizon_hcb");
60+
});
61+
});
62+
63+
describe("Language integration", () => {
64+
it("setLanguage stores value in shared map", () => {
65+
cy.wrap({ setLanguage })
66+
.invoke("setLanguage", "de");
67+
68+
cy.wrap({ getSharedValue })
69+
.invoke("getSharedValue", "language")
70+
.should("equal", "de");
71+
});
72+
});
73+
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import getSharedResource from "../getSharedResource.js";
2+
import EventProvider from "../EventProvider.js";
3+
4+
type ConfigChangeDetail = { name: string; value: unknown };
5+
6+
const getEventProvider = () => getSharedResource("ConfigChange.eventProvider", new EventProvider<ConfigChangeDetail, void>());
7+
const getSharedValues = () => getSharedResource<Record<string, unknown>>("ConfigChange.values", {});
8+
9+
const CONFIG_CHANGE = "configChange";
10+
11+
// Module-level skip flags — each runtime copy has its own Set,
12+
// so a runtime's own handler correctly skips when it fires.
13+
const skipFlags = new Set<string>();
14+
15+
/**
16+
* Stores value in shared map and fires a cross-runtime config change event.
17+
* The firing runtime's own handler is skipped via the skip-guard pattern.
18+
*/
19+
const fireConfigChange = (name: string, value: unknown): void => {
20+
getSharedValues()[name] = value;
21+
22+
skipFlags.add(name);
23+
try {
24+
getEventProvider().fireEvent(CONFIG_CHANGE, { name, value });
25+
} finally {
26+
skipFlags.delete(name);
27+
}
28+
};
29+
30+
/**
31+
* Registers a per-setting cross-runtime listener.
32+
* The handler is only called when another runtime fires the change.
33+
*/
34+
const attachConfigChange = (name: string, handler: (value: any) => void): void => { // eslint-disable-line
35+
getEventProvider().attachEvent(CONFIG_CHANGE, (detail: ConfigChangeDetail) => {
36+
if (detail.name === name && !skipFlags.has(name)) {
37+
handler(detail.value);
38+
}
39+
});
40+
};
41+
42+
/**
43+
* Reads the last-set value from the shared values map.
44+
* Used by late-booting runtimes to pick up values already set by others.
45+
*/
46+
const getSharedValue = <T>(name: string): T | undefined => {
47+
return getSharedValues()[name] as T | undefined;
48+
};
49+
50+
export { fireConfigChange, attachConfigChange, getSharedValue };

packages/base/src/config/Language.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { reRenderAllUI5Elements } from "../Render.js";
77
import { DEFAULT_LANGUAGE } from "../generated/AssetParameters.js";
88
import { isBooted } from "../Boot.js";
99
import { attachConfigurationReset } from "./ConfigurationReset.js";
10+
import { fireConfigChange, attachConfigChange, getSharedValue } from "./ConfigurationSync.js";
1011

1112
let curLanguage: string | undefined;
1213
let fetchDefaultLanguage: boolean | undefined;
@@ -25,6 +26,17 @@ attachConfigurationReset(() => {
2526
// will trigger a re-render of all language-aware components.
2627
let languageChangePending = false;
2728

29+
attachConfigChange("language", (language: string) => {
30+
curLanguage = language;
31+
languageChangePending = true;
32+
fireLanguageChange(language).then(() => {
33+
languageChangePending = false;
34+
if (isBooted()) {
35+
reRenderAllUI5Elements({ languageAware: true });
36+
}
37+
});
38+
});
39+
2840
const getLanguageChangePending = () => languageChangePending;
2941

3042
/**
@@ -34,7 +46,7 @@ const getLanguageChangePending = () => languageChangePending;
3446
*/
3547
const getLanguage = (): string | undefined => {
3648
if (curLanguage === undefined) {
37-
curLanguage = getConfiguredLanguage();
49+
curLanguage = getSharedValue<string>("language") ?? getConfiguredLanguage();
3850
}
3951
return curLanguage;
4052
};
@@ -55,6 +67,8 @@ const setLanguage = async (language: string): Promise<void> => {
5567
languageChangePending = true;
5668
curLanguage = language;
5769

70+
fireConfigChange("language", language);
71+
5872
await fireLanguageChange(language);
5973

6074
languageChangePending = false;

packages/base/src/config/Theme.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import getThemeDesignerTheme from "../theming/getThemeDesignerTheme.js";
55
import { DEFAULT_THEME, SUPPORTED_THEMES } from "../generated/AssetParameters.js";
66
import { boot, isBooted } from "../Boot.js";
77
import { attachConfigurationReset } from "./ConfigurationReset.js";
8+
import { fireConfigChange, attachConfigChange, getSharedValue } from "./ConfigurationSync.js";
89

910
let curTheme: string | undefined;
1011
let curBaseTheme: string | undefined;
@@ -13,14 +14,21 @@ attachConfigurationReset(() => {
1314
curTheme = undefined;
1415
});
1516

17+
attachConfigChange("theme", (theme: string) => {
18+
curTheme = theme;
19+
if (isBooted()) {
20+
applyTheme(curTheme).then(() => reRenderAllUI5Elements({ themeAware: true }));
21+
}
22+
});
23+
1624
/**
1725
* Returns the current theme.
1826
* @public
1927
* @returns {string} the current theme name
2028
*/
2129
const getTheme = (): string => {
2230
if (curTheme === undefined) {
23-
curTheme = getConfiguredTheme();
31+
curTheme = getSharedValue<string>("theme") ?? getConfiguredTheme();
2432
}
2533

2634
return curTheme;
@@ -39,6 +47,8 @@ const setTheme = async (theme: string): Promise<void> => {
3947

4048
curTheme = theme;
4149

50+
fireConfigChange("theme", theme);
51+
4252
if (isBooted()) {
4353
// Update CSS Custom Properties
4454
await applyTheme(curTheme);

0 commit comments

Comments
 (0)