Skip to content

Commit 43ff5de

Browse files
authored
feat(framework): scope theming css variables with component packages (UI5#12491)
Scope and package `--sap` CSS variables with the web components package to improve theming reliability and prevent conflicts with external theme dependencies. Some applications and custom themes rely on older versions of `@sap-theming/theming-base-content`, resulting in missing CSS variables when detected as external theming. This change ensures components have access to defined variables from newer versions when variables are not added externally, while allowing external variables to override defaults when present. - Prefix all CSS variables used variables from `@sap-theming/theming-base-content` with `--ui5-` to scope - Use default values from `@sap-theming/theming-base-content` as fallbacks per theme - each CSS variable is derived from the original name with default value per theme from theming base content as fallback - Apply `--sap` CSS variables only when externally defined - Decouple visual theme determination from component theme configuration - the visual active theme is determined by the loaded theming base content, independent of the theme set for web components. The components' theme setting only affects internal CSS variable mapping ## Theme Behavior Expectation *Note: The following behavior applies when external CSS variables are detected when library is booting or when the theme is changing* **Standard Theme (sap_horizon):** - SAP variables: `sap_horizon` - Components: `sap_horizon` **Custom Theme (my_custom_theme extends sap_horizon):** - SAP variables: `my_custom_theme` - Components: `sap_horizon` (maps to base theme) **Custom Theme (my_custom_theme extends sap_horizon, used theme is sap_fiori_3):** - SAP variables: `my_custom_theme` - Components: `sap_fiori_3` Fixes: UI5#12446
1 parent be87856 commit 43ff5de

File tree

11 files changed

+241
-71
lines changed

11 files changed

+241
-71
lines changed

packages/base/lib/generate-styles/index.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
import fs from 'fs/promises';
22
import path from "path";
33
import CleanCSS from "clean-css";
4+
import { processComponentPackageFile } from '@ui5/webcomponents-tools/lib/css-processors/css-processor-themes.mjs';
45
import { pathToFileURL } from "url";
56

67
const generate = async () => {
8+
const packageJSON = JSON.parse(await fs.readFile("./package.json"))
79
await fs.mkdir("src/generated/css/", { recursive: true });
810

911
const files = (await fs.readdir("src/css/")).filter(file => file.endsWith(".css"));
1012
const filesPromises = files.map(async file => {
11-
let content = await fs.readFile(path.join("src/css/", file));
13+
const filePath = path.join("src/css/", file);
14+
let content = await fs.readFile(filePath);
1215
const res = new CleanCSS().minify(`${content}`);
13-
content = `export default \`${res.styles}\`;`;
16+
17+
// Scope used variables
18+
content = await processComponentPackageFile({ text: res.styles, path: filePath }, packageJSON);
19+
20+
content = `export default \`${content}\`;`;
21+
1422
return fs.writeFile(path.join("src/generated/css/", `${file}.ts`), content);
1523
});
1624

packages/base/src/theming/applyTheme.ts

Lines changed: 18 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import { getThemeProperties, getRegisteredPackages, isThemeRegistered } from "../asset-registries/Themes.js";
2-
import { removeStyle, createOrUpdateStyle } from "../ManagedStyles.js";
2+
import { createOrUpdateStyle } from "../ManagedStyles.js";
33
import getThemeDesignerTheme from "./getThemeDesignerTheme.js";
44
import { fireThemeLoaded } from "./ThemeLoaded.js";
5-
import { getFeature } from "../FeaturesRegistry.js";
65
import { attachCustomThemeStylesToHead, getThemeRoot } from "../config/ThemeRoot.js";
7-
import type OpenUI5Support from "../features/OpenUI5Support.js";
86
import { DEFAULT_THEME } from "../generated/AssetParameters.js";
97
import { getCurrentRuntimeIndex } from "../Runtimes.js";
108

@@ -31,10 +29,6 @@ const loadThemeBase = async (theme: string) => {
3129
}
3230
};
3331

34-
const deleteThemeBase = () => {
35-
removeStyle("data-ui5-theme-properties", BASE_THEME_PACKAGE);
36-
};
37-
3832
const loadComponentPackages = async (theme: string, externalThemeName?: string) => {
3933
const registeredPackages = getRegisteredPackages();
4034

@@ -53,42 +47,34 @@ const loadComponentPackages = async (theme: string, externalThemeName?: string)
5347
};
5448

5549
const detectExternalTheme = async (theme: string) => {
50+
if (getThemeRoot()) {
51+
await attachCustomThemeStylesToHead(theme);
52+
}
53+
5654
// If theme designer theme is detected, use this
5755
const extTheme = getThemeDesignerTheme();
5856
if (extTheme) {
5957
return extTheme;
6058
}
61-
62-
// If OpenUI5Support is enabled, try to find out if it loaded variables
63-
const openUI5Support = getFeature<typeof OpenUI5Support>("OpenUI5Support");
64-
if (openUI5Support && openUI5Support.isOpenUI5Detected()) {
65-
const varsLoaded = openUI5Support.cssVariablesLoaded();
66-
if (varsLoaded) {
67-
return {
68-
themeName: openUI5Support.getConfigurationSettingsObject()?.theme, // just themeName
69-
baseThemeName: "", // baseThemeName is only relevant for custom themes
70-
};
71-
}
72-
} else if (getThemeRoot()) {
73-
await attachCustomThemeStylesToHead(theme);
74-
75-
return getThemeDesignerTheme();
76-
}
7759
};
7860

7961
const applyTheme = async (theme: string) => {
62+
// Detect external theme if available (e.g., from theme designer or custom theme root)
8063
const extTheme = await detectExternalTheme(theme);
8164

82-
// Only load theme_base properties if there is no externally loaded theme, or there is, but it is not being loaded
83-
if (!extTheme || theme !== extTheme.themeName) {
84-
await loadThemeBase(theme);
85-
} else {
86-
deleteThemeBase();
87-
}
88-
89-
// Always load component packages properties. For non-registered themes, try with the base theme, if any
65+
// Determine which theme to use for component packages:
66+
// 1. If the requested theme is registered, use it directly
67+
// 2. If external theme exists, use its base theme (e.g., "my_custom_theme" extends "sap_fiori_3")
68+
// 3. Otherwise, fallback to the default theme
9069
const packagesTheme = isThemeRegistered(theme) ? theme : extTheme && extTheme.baseThemeName;
91-
await loadComponentPackages(packagesTheme || DEFAULT_THEME, extTheme && extTheme.themeName === theme ? theme : undefined);
70+
const effectiveTheme = packagesTheme || DEFAULT_THEME;
71+
72+
// Load base theme properties
73+
await loadThemeBase(effectiveTheme);
74+
75+
// Load component-specific theme properties
76+
// Pass external theme name only if it matches the requested theme to avoid conflicts
77+
await loadComponentPackages(effectiveTheme, extTheme && extTheme.themeName === theme ? theme : undefined);
9278

9379
fireThemeLoaded(theme);
9480
};
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<!DOCTYPE html>
2+
<html>
3+
4+
<head>
5+
<meta charset="utf-8">
6+
<title>Theming</title>
7+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
8+
<meta charset="utf-8">
9+
<script src="../%VITE_BUNDLE_PATH%" type="module"></script>
10+
</head>
11+
12+
<body>
13+
<p><strong>Test Page 1:</strong> Default theming - Tests the component with default theme settings without any
14+
external styles or theme changes.</p>
15+
<p>Expected theme <strong>sap_horizon</strong></p>
16+
17+
<ui5-button>Some button</ui5-button>
18+
</body>
19+
20+
</html>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<!DOCTYPE html>
2+
<html>
3+
4+
<head>
5+
<meta charset="utf-8">
6+
<title>Theming</title>
7+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
8+
<meta charset="utf-8">
9+
<script src="../%VITE_BUNDLE_PATH%" type="module"></script>
10+
</head>
11+
12+
<body>
13+
<p><strong>Test Page 6:</strong> Theme change without external styles - Tests programmatic theme switching
14+
behavior without any external CSS interference to verify pure theme transition functionality.</p>
15+
<p>Expected theme <strong>sap_horizon_hcb</strong></p>
16+
<ui5-button>Some button</ui5-button>
17+
18+
<script type="module">
19+
setTimeout(() => {
20+
window["sap-ui-webcomponents-bundle"].configuration.setTheme("sap_horizon_hcb");
21+
}, 1000);
22+
</script>
23+
</body>
24+
25+
</html>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<!DOCTYPE html>
2+
<html>
3+
4+
<head>
5+
<meta charset="utf-8">
6+
<title>Theming</title>
7+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
8+
<meta charset="utf-8">
9+
<link rel="stylesheet"
10+
href="https://cdn.jsdelivr.net/npm/@sap-theming/theming-base-content/content/Base/baseLib/sap_belize/css_variables.css">
11+
<script src="../%VITE_BUNDLE_PATH%" type="module"></script>
12+
</head>
13+
14+
<body>
15+
<p><strong>Test Page 2:</strong> Default theming with preloaded external styles - Tests how components behave when
16+
external CSS is loaded before component initialization.</p>
17+
<p>Expected theme <strong>sap_belize</strong></p>
18+
19+
<ui5-button>Some button</ui5-button>
20+
</body>
21+
22+
</html>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<!DOCTYPE html>
2+
<html>
3+
4+
<head>
5+
<meta charset="utf-8">
6+
<title>Theming</title>
7+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
8+
<meta charset="utf-8">
9+
<script src="../%VITE_BUNDLE_PATH%" type="module"></script>
10+
</head>
11+
12+
<body>
13+
<p><strong>Test Page 3:</strong> Default theming with external styles loaded later - Tests the impact of external
14+
CSS loaded after component initialization on styling.</p>
15+
<p>Expected theme <strong>sap_belize</strong></p>
16+
17+
<ui5-button>Some button</ui5-button>
18+
<script>
19+
setTimeout(() => {
20+
const link = document.createElement("link");
21+
link.rel = "stylesheet";
22+
link.href = "https://cdn.jsdelivr.net/npm/@sap-theming/theming-base-content/content/Base/baseLib/sap_belize/css_variables.css";
23+
document.head.appendChild(link);
24+
}, 3000);
25+
</script>
26+
</body>
27+
28+
</html>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<!DOCTYPE html>
2+
<html>
3+
4+
<head>
5+
<meta charset="utf-8">
6+
<title>Theming</title>
7+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
8+
<meta charset="utf-8">
9+
<link rel="stylesheet"
10+
href="https://cdn.jsdelivr.net/npm/@sap-theming/theming-base-content/content/Base/baseLib/sap_belize/css_variables.css">
11+
<script src="../%VITE_BUNDLE_PATH%" type="module"></script>
12+
</head>
13+
14+
<body>
15+
<p><strong>Test Page 4:</strong> Default theming with theme change and preloaded external styles - Tests theme
16+
switching behavior when external CSS is already present in the DOM.</p>
17+
<p>Expected theme <strong>sap_belize</strong></p>
18+
19+
<ui5-button>Some button</ui5-button>
20+
<script type="module">
21+
setTimeout(() => {
22+
window["sap-ui-webcomponents-bundle"].configuration.setTheme("sap_horizon_hcb");
23+
}, 1000);
24+
</script>
25+
</body>
26+
27+
</html>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<!DOCTYPE html>
2+
<html>
3+
4+
<head>
5+
<meta charset="utf-8">
6+
<title>Theming</title>
7+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
8+
<meta charset="utf-8">
9+
<script src="../%VITE_BUNDLE_PATH%" type="module"></script>
10+
</head>
11+
12+
<body>
13+
<p><strong>Test Page 5:</strong> Default theming with theme change and external styles loaded later - Tests theme
14+
switching followed by external CSS injection to verify style resolution order.</p>
15+
<p>Expected theme <strong>sap_belize</strong></p>
16+
<ui5-button>Some button</ui5-button>
17+
18+
<script type="module">
19+
setTimeout(() => {
20+
window["sap-ui-webcomponents-bundle"].configuration.setTheme("sap_horizon_hcb");
21+
setTimeout(() => {
22+
const link = document.createElement("link");
23+
link.rel = "stylesheet";
24+
link.href = "https://cdn.jsdelivr.net/npm/@sap-theming/theming-base-content/content/Base/baseLib/sap_belize/css_variables.css";
25+
document.head.appendChild(link);
26+
}, 1000);
27+
}, 1000);
28+
</script>
29+
</body>
30+
31+
</html>

packages/tools/lib/css-processors/css-processor-components.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as fs from "fs";
44
import * as path from "path";
55
import { writeFile, mkdir } from "fs/promises";
66
import chokidar from "chokidar";
7-
import scopeVariables from "./scope-variables.mjs";
7+
import {scopeUi5Variables} from "./scope-variables.mjs";
88
import { writeFileIfChanged, getFileContent } from "./shared.mjs";
99
import { pathToFileURL } from "url";
1010

@@ -24,7 +24,7 @@ const generate = async (argv) => {
2424
build.onEnd(result => {
2525
result.outputFiles.forEach(async f => {
2626
// scoping
27-
let newText = scopeVariables(f.text, packageJSON);
27+
let newText = scopeUi5Variables(f.text, packageJSON);
2828
newText = newText.replaceAll(/\\/g, "\\\\"); // Escape backslashes as they might appear in css rules
2929
await mkdir(path.dirname(f.path), { recursive: true });
3030
writeFile(f.path, newText);

packages/tools/lib/css-processors/css-processor-themes.mjs

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,45 @@ import { writeFile, mkdir } from "fs/promises";
66
import postcss from "postcss";
77
import combineDuplicatedSelectors from "../postcss-combine-duplicated-selectors/index.js"
88
import { writeFileIfChanged, getFileContent } from "./shared.mjs";
9-
import scopeVariables from "./scope-variables.mjs";
9+
import { scopeUi5Variables, scopeThemingVariables } from "./scope-variables.mjs";
1010
import { pathToFileURL } from "url";
1111

12-
const generate = async (argv) => {
12+
async function processThemingPackageFile(f) {
13+
const selector = ':root';
14+
const newRule = postcss.rule({ selector });
15+
const result = await postcss().process(f.text);
16+
17+
result.root.walkRules(selector, rule => {
18+
for (const decl of rule.nodes) {
19+
if (decl.type !== 'decl' ) {
20+
continue;
21+
} else if (decl.prop.startsWith('--sapFontUrl')) {
22+
continue;
23+
} else if (!decl.prop.startsWith('--sap')) {
24+
newRule.append(decl.clone());
25+
} else {
26+
const originalProp = decl.prop;
27+
const originalValue = decl.value;
28+
29+
newRule.append(decl.clone({ prop: originalProp.replace("--sap", "--ui5-sap"), value: `var(${originalProp}, ${originalValue})` }));
30+
}
31+
}
32+
});
33+
34+
return newRule.toString();
35+
};
36+
37+
async function processComponentPackageFile(f, packageJSON) {
38+
let result = await postcss(combineDuplicatedSelectors).process(f.text);
39+
40+
result = scopeUi5Variables(result.css, packageJSON, f.path);
41+
42+
result = scopeThemingVariables(result);
43+
44+
return result;
45+
}
46+
47+
async function generate(argv) {
1348
const tsMode = process.env.UI5_TS === "true";
1449
const extension = tsMode ? ".css.ts" : ".css.js";
1550

@@ -20,37 +55,14 @@ const generate = async (argv) => {
2055
]);
2156
const restArgs = argv.slice(2);
2257

23-
const processThemingPackageFile = async (f) => {
24-
const selector = ':root';
25-
const result = await postcss().process(f.text);
26-
27-
const newRule = postcss.rule({ selector });
28-
29-
result.root.walkRules(selector, rule => {
30-
rule.walkDecls(decl => {
31-
if (!decl.prop.startsWith('--sapFontUrl')) {
32-
newRule.append(decl.clone());
33-
}
34-
});
35-
});
36-
37-
return newRule.toString();
38-
};
39-
40-
const processComponentPackageFile = async (f) => {
41-
const result = await postcss(combineDuplicatedSelectors).process(f.text);
42-
43-
return scopeVariables(result.css, packageJSON, f.path);
44-
}
45-
4658
let scopingPlugin = {
4759
name: 'scoping',
4860
setup(build) {
4961
build.initialOptions.write = false;
5062

5163
build.onEnd(result => {
5264
result.outputFiles.forEach(async f => {
53-
let newText = f.path.includes("packages/theming") ? await processThemingPackageFile(f) : await processComponentPackageFile(f);
65+
let newText = f.path.includes("packages/theming") ? await processThemingPackageFile(f) : await processComponentPackageFile(f, packageJSON);
5466

5567
await mkdir(path.dirname(f.path), { recursive: true });
5668
writeFile(f.path, newText);
@@ -99,4 +111,8 @@ if (import.meta.url === fileUrl) {
99111

100112
export default {
101113
_ui5mainFn: generate
114+
}
115+
116+
export {
117+
processComponentPackageFile
102118
}

0 commit comments

Comments
 (0)