Skip to content

Commit 09471a1

Browse files
authored
chore(framework): apply css variables on custom element (#12928)
Enable customization of **--sap*** theming CSS variables at the component level (part one: style preparation and loading mechanism). Example: ```html <ui5-button style="--sapButton_Background: red"> ``` To support all use cases, internal **--ui5*** CSS variables must be defined on the custom element using the `:host` selector, not on the document using the `:root` selector. For the same reason, all content density–related variables and direction-related variables must also be defined on `:host`. Direction-related variables are now set using the `:dir()` selector. For content density–related variables, there was no fully working solution before. CSS container style queries are not fully supported, and the classes and attributes used by application developers are applied outside the shadow root. This PR adds a mechanism to handle this. CSS container style queries are now used and, at build time, they are transpiled into a working solution. To define variables, use the following syntax: ```css @container style(--ui5_content_density: compact) { :host { ... } } ``` All of this behavior is controlled by the `cssVariablesTarget` flag (accepted values: `"host"` and `"root"`). The flag is configured at the package level via `package-script.js`. When this flag is enabled, all `*-parameters.css` files must be migrated to define variables on `:host` instead of `:root`. For direction-related CSS variables, the `:dir()` selector is used. For content density–related variables, the new container style query syntax is used. Since CSS variables are now defined at the component level, CSS variable scoping is no longer needed. As a result, the `getScopedVarName` function is no longer required.
1 parent 86f6850 commit 09471a1

16 files changed

Lines changed: 339 additions & 41 deletions

File tree

packages/base/src/asset-registries/Themes.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,17 @@ import { fireThemeRegistered } from "../theming/ThemeRegistered.js";
55
type ThemeData = string;
66
type ThemeLoader = (themeName: string) => Promise<string>;
77

8+
type CSSVariablesTarget = "root" | "host";
9+
810
const themeStyles = new Map<string, string>();
911
const loaders = new Map<string, ThemeLoader>();
1012
const customLoaders = new Map<string, ThemeLoader>();
11-
const registeredPackages = new Set<string>();
13+
const registeredPackages = new Map<string, { cssVariablesTarget: CSSVariablesTarget }>();
1214
const registeredThemes = new Set<string>();
1315

14-
const registerThemePropertiesLoader = (packageName: string, themeName: string, loader: ThemeLoader) => {
16+
const registerThemePropertiesLoader = (packageName: string, themeName: string, loader: ThemeLoader, cssVariablesTarget: CSSVariablesTarget = "root") => {
1517
loaders.set(`${packageName}/${themeName}`, loader);
16-
registeredPackages.add(packageName);
18+
registeredPackages.set(packageName, { cssVariablesTarget });
1719
registeredThemes.add(themeName);
1820
fireThemeRegistered(themeName);
1921
};
Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,34 @@
11
:root {
2-
--_ui5_content_density:cozy;
2+
/*
3+
Use a non-existent variable to always trigger the fallback.
4+
Because this variable is never defined, the fallback is used
5+
in all cases.
6+
7+
Using `initial` as a fallback can work only for properties
8+
that treat it as an invalid value. To avoid this inconsistency,
9+
we intentionally reference an undefined variable, which is
10+
always considered invalid and undefined.
11+
*/
12+
--_ui5-cozy-size: var(--_ui5-f2d95f8);
13+
14+
--_ui5-compact-size: ;
15+
--_ui5_content_density: cozy;
316
}
417

518
[data-ui5-compact-size],
619
.ui5-content-density-compact,
720
.sapUiSizeCompact {
8-
--_ui5_content_density:compact;
9-
}
21+
--_ui5-cozy-size: ;
22+
/*
23+
Use a non-existent variable to always trigger the fallback.
24+
Because this variable is never defined, the fallback is used
25+
in all cases.
26+
27+
Using `initial` as a fallback can work only for properties
28+
that treat it as an invalid value. To avoid this inconsistency,
29+
we intentionally reference an undefined variable, which is
30+
always considered invalid and undefined.
31+
*/
32+
--_ui5-compact-size: var(--_ui5-f2d95f8);
33+
--_ui5_content_density: compact;
34+
}

packages/base/src/theming/applyTheme.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { setBaseTheme } from "../config/Theme.js";
88
import type OpenUI5Support from "../features/OpenUI5Support.js";
99
import { DEFAULT_THEME } from "../generated/AssetParameters.js";
1010
import { getCurrentRuntimeIndex } from "../Runtimes.js";
11+
import { updateComponentStyles } from "./componentStyles.js";
1112

1213
// eslint-disable-next-line
1314
export let _lib = "ui5";
@@ -39,14 +40,18 @@ const deleteThemeBase = () => {
3940
const loadComponentPackages = async (theme: string, externalThemeName?: string) => {
4041
const registeredPackages = getRegisteredPackages();
4142

42-
const packagesStylesPromises = [...registeredPackages].map(async packageName => {
43+
const packagesStylesPromises = [...registeredPackages.entries()].map(async ([packageName, { cssVariablesTarget }]) => {
4344
if (packageName === BASE_THEME_PACKAGE) {
4445
return;
4546
}
4647

4748
const cssData = await getThemeProperties(packageName, theme, externalThemeName);
4849
if (cssData) {
49-
createOrUpdateStyle(cssData, `data-ui5-component-properties-${getCurrentRuntimeIndex()}`, packageName);
50+
if (cssVariablesTarget === "root") {
51+
createOrUpdateStyle(cssData, `data-ui5-component-properties-${getCurrentRuntimeIndex()}`, packageName);
52+
} else if (cssVariablesTarget === "host") {
53+
updateComponentStyles(packageName, cssData);
54+
}
5055
}
5156
});
5257

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
const packageMap = new Map<string, string>();
2+
let componentsStyleSheet: CSSStyleSheet;
3+
4+
const getComponentStyles = () => {
5+
if (!componentsStyleSheet) {
6+
componentsStyleSheet = new CSSStyleSheet();
7+
}
8+
9+
return componentsStyleSheet;
10+
};
11+
12+
const updateComponentStyles = (packageName: string, content: string) => {
13+
packageMap.set(packageName, content);
14+
15+
const combinedStyles = Array.from(packageMap.values()).join("\n");
16+
getComponentStyles().replaceSync(combinedStyles);
17+
};
18+
19+
export {
20+
getComponentStyles,
21+
updateComponentStyles,
22+
};

packages/base/src/updateShadowRoot.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import getConstructableStyle from "./theming/getConstructableStyle.js";
2+
import { getComponentStyles } from "./theming/componentStyles.js";
23
import type UI5Element from "./UI5Element.js";
34

45
/**
@@ -14,7 +15,7 @@ const updateShadowRoot = (element: UI5Element) => {
1415
return;
1516
}
1617

17-
shadowRoot.adoptedStyleSheets = getConstructableStyle(ctor);
18+
shadowRoot.adoptedStyleSheets = [getComponentStyles(), ...getConstructableStyle(ctor)];
1819
ctor.renderer(element, shadowRoot);
1920
};
2021

packages/fiori/src/themes/base/rtl-parameters.css

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
:root,
2-
:dir(ltr) {
1+
:root {
32
--_ui5_timeline_scroll_container_offset: 0.5rem;
43
--_ui5_shellbar_notification_btn_count_offset: 0.125rem;
54
--_ui5_side_navigation_item_expand_icon_hover_left: auto;

packages/main/package-scripts.cjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const options = {
55
portStep: 2,
66
noWatchTS: true,
77
dev: true,
8+
// cssVariablesTarget: "host",
89
internal: {
910
cypress_code_coverage: false,
1011
},

packages/main/src/themes/base/rtl-parameters.css

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
:root,
2-
:dir(ltr) {
1+
:root {
32
--_ui5_rotation_90deg: rotate(90deg);
43
--_ui5_rotation_minus_90deg: rotate(-90deg);
54

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<!DOCTYPE html>
2+
<html>
3+
4+
<head>
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6+
<meta charset="utf-8">
7+
8+
<title>Button</title>
9+
<script>
10+
// delete Document.prototype.adoptedStyleSheets
11+
</script>
12+
13+
14+
<script src="%VITE_BUNDLE_PATH%" type="module"></script>
15+
16+
<link rel="stylesheet" type="text/css" href="./styles/Button.css">
17+
18+
</head>
19+
20+
<body class="button1auto">
21+
<h1>Combinations</h1>
22+
<div>
23+
<h2>Normal button</h2>
24+
<p>Should be red</p>
25+
<ui5-button>Click me</ui5-button>
26+
</div>
27+
28+
<div>
29+
<h2>Cozy</h2>
30+
<p>Should be red</p>
31+
<ui5-button>Click me</ui5-button>
32+
</div>
33+
<div data-ui5-compact-size>
34+
<h2>Compact with data-ui5-compact-size</h2>
35+
<p>Should be lightblue</p>
36+
<ui5-button>Click me</ui5-button>
37+
</div>
38+
<div class="ui5-content-density-compact">
39+
<h2>Compact with .ui5-content-density-compact</h2>
40+
<p>Should be lightblue</p>
41+
<ui5-button>Click me</ui5-button>
42+
</div>
43+
<div class="sapUiSizeCompact">
44+
<h2>Compact with .sapUiSizeCompact</h2>
45+
<p>Should be lightblue</p>
46+
<ui5-button>Click me</ui5-button>
47+
</div>
48+
<div dir="ltr">
49+
<h2>LTR</h2>
50+
<p>Should be red</p>
51+
<ui5-button>Click me</ui5-button>
52+
</div>
53+
<div dir="rtl">
54+
<h2>RTL</h2>
55+
<p>Should be green</p>
56+
<ui5-button>Click me</ui5-button>
57+
</div>
58+
</body>
59+
60+
</html>

packages/main/test/pages/kitchen-scripts.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,11 @@ document.addEventListener("DOMContentLoaded", function(event) {
5151
}, false);
5252

5353
btnCompact.addEventListener('click', function(e) {
54-
if (document.body.className.includes(COMPACT_CLASS)) {
55-
return document.body.className = "";
54+
if (document.body.classList.contains(COMPACT_CLASS)) {
55+
return document.body.classList.remove(COMPACT_CLASS);
5656
}
5757

58-
document.body.className += COMPACT_CLASS;
58+
document.body.classList.add(COMPACT_CLASS);
5959
}, false);
6060

6161
btnTheme.addEventListener('click', function(e) {

0 commit comments

Comments
 (0)