Skip to content

Commit 291829a

Browse files
authored
feat: framework-level support for CSS Custom Properties (#196)
1 parent 0934d70 commit 291829a

28 files changed

+3321
-87
lines changed

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,4 @@ yarn-debug.log*
3434
.yarn-integrity
3535

3636
# Ignore default target directory for the npm package 'ui5-schemas'
37-
.tmp
37+
.tmp

packages/base/src/sap/ui/webcomponents/base/Bootstrap.js

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import whenDOMReady from "./util/whenDOMReady";
22
import EventEnrichment from "./events/EventEnrichment";
33
import { insertIconFontFace } from "./IconFonts";
44
import DOMEventHandler from "./DOMEventHandler";
5+
import { initConfiguration } from "./Configuration";
6+
import { applyTheme } from "./Theming";
57
import whenPolyfillLoaded from "./compatibility/whenPolyfillLoaded";
68

79
EventEnrichment.run();
@@ -17,6 +19,8 @@ const Bootstrap = {
1719

1820
bootPromise = new Promise(async resolve => {
1921
await whenDOMReady();
22+
initConfiguration();
23+
applyTheme();
2024
insertIconFontFace();
2125
DOMEventHandler.start();
2226
await whenPolyfillLoaded();

packages/base/src/sap/ui/webcomponents/base/Configuration.js

+6-3
Original file line numberDiff line numberDiff line change
@@ -122,11 +122,14 @@ const applyConfigurations = () => {
122122
});
123123
};
124124

125-
parseConfigurationScript();
126-
parseURLParameters();
127-
applyConfigurations();
125+
const initConfiguration = () => {
126+
parseConfigurationScript();
127+
parseURLParameters();
128+
applyConfigurations();
129+
};
128130

129131
export {
132+
initConfiguration,
130133
getTheme,
131134
getRTL,
132135
getLanguage,

packages/base/src/sap/ui/webcomponents/base/Theming.js

+31-2
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,44 @@
11
import { getTheme, _setTheme } from "./Configuration";
22
import { getStyles } from "./theming/ThemeBundle";
33
import { getCustomCSS } from "./theming/CustomStyle";
4+
import { getThemeProperties } from "./theming/ThemeProperties";
5+
import { injectThemeProperties, updateWebComponentStyles } from "./theming/StyleInjection";
46

57
const themeChangeCallbacks = [];
68

9+
const getDefaultTheme = () => {
10+
return "sap_fiori_3";
11+
};
12+
713
const attachThemeChange = function attachThemeChange(callback) {
814
if (themeChangeCallbacks.indexOf(callback) === -1) {
915
themeChangeCallbacks.push(callback);
1016
}
1117
};
1218

13-
const setTheme = function setTheme(theme) {
19+
const applyTheme = async () => {
20+
let cssText = "";
21+
const theme = getTheme();
22+
23+
const defaultTheme = getDefaultTheme();
24+
if (theme !== defaultTheme) {
25+
cssText = await getThemeProperties("@ui5/webcomponents", theme);
26+
}
27+
injectThemeProperties(cssText);
28+
updateWebComponentStyles();
29+
};
30+
31+
const setTheme = async theme => {
1432
if (theme === getTheme()) {
1533
return;
1634
}
1735

36+
// Update configuration
1837
_setTheme(theme);
38+
39+
// Update CSS Custom Properties
40+
await applyTheme();
41+
1942
themeChangeCallbacks.forEach(callback => callback(theme));
2043
};
2144

@@ -35,4 +58,10 @@ const getEffectiveStyle = async (theme, styleUrls, tag) => {
3558
return cssText;
3659
};
3760

38-
export { attachThemeChange, setTheme, getEffectiveStyle };
61+
export {
62+
getDefaultTheme,
63+
attachThemeChange,
64+
applyTheme,
65+
setTheme,
66+
getEffectiveStyle,
67+
};

packages/base/src/sap/ui/webcomponents/base/WebComponent.js

-14
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import Integer from "./types/Integer";
66
import ControlRenderer from "./ControlRenderer";
77
import RenderScheduler from "./RenderScheduler";
88
import TemplateContext from "./TemplateContext";
9-
import { attachThemeChange } from "./Theming";
109
import State from "./State";
1110

1211
const metadata = {
@@ -50,19 +49,6 @@ class WebComponent extends HTMLElement {
5049
this._domRefReadyPromise._deferredResolve = deferredResolve;
5150

5251
this._monitoredChildProps = new Map();
53-
54-
// Only for native Shadow DOM, and only when present
55-
if (!window.ShadyDOM && !this.constructor.getMetadata().getNoShadowDOM()) {
56-
attachThemeChange(this._onThemeChange.bind(this));
57-
}
58-
}
59-
60-
_onThemeChange() {
61-
const klass = this.constructor;
62-
const tag = klass.getMetadata().getTag();
63-
const styleURLs = klass.getMetadata().getStyleUrl();
64-
65-
ShadowDOM.updateStyle(tag, this.shadowRoot, styleURLs);
6652
}
6753

6854
_whenShadowRootReady() {

packages/base/src/sap/ui/webcomponents/base/browsersupport/IE11.js

+3
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,8 @@ import "../thirdparty/fetch";
2626
// async - await
2727
import "regenerator-runtime/runtime";
2828

29+
// CSS Custom Properties
30+
import "../compatibility/CSSVarsSimulation";
31+
2932
// Plus all polyfills needed for Edge are also needed for IE11
3033
import "./Edge";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
let vars = new Map();
2+
3+
/**
4+
* Scans the given string, extracts all CSS vars from it and stores them internally
5+
* @param styleString - string containing CSS variables
6+
*/
7+
const findCSSVars = styleString => {
8+
vars = new Map();
9+
const couples = styleString.match(/--[^:)]+:\s*[^;}]+/g) || [];
10+
couples.forEach(couple => {
11+
const [varName, varValue] = couple.split(/:\s*/);
12+
vars.set(varName, varValue);
13+
});
14+
};
15+
16+
/**
17+
* Replaces all occurrences of CSS vars with their values (and fallback values)
18+
* @param styleString - string containing CSS selectors
19+
* @returns {*}
20+
*/
21+
const applyCSSVars = styleString => {
22+
// Replace all variables, with or without default value (default value removed too)
23+
vars.forEach((varValue, varName) => {
24+
const re = new RegExp(`var\\(\\s*${varName}.*?\\)`, "g");
25+
styleString = styleString.replace(re, varValue);
26+
});
27+
28+
// Replace all unresolved variables with their default values
29+
styleString = styleString.replace(/var\(.*?,\s*(.*?)\)/g, "$1");
30+
31+
return styleString;
32+
};
33+
34+
const CSSVarsSimulation = {
35+
findCSSVars,
36+
applyCSSVars,
37+
};
38+
39+
window.CSSVarsSimulation = CSSVarsSimulation;
40+
41+
export default CSSVarsSimulation;

packages/base/src/sap/ui/webcomponents/base/compatibility/ShadowDOM.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { getTheme, getRTL, getCompactSize } from "../Configuration";
22

3-
import StyleInjection from "../theming/StyleInjection";
3+
import { injectWebComponentStyle } from "../theming/StyleInjection";
44
import { registerStyle } from "../theming/ThemeBundle";
55

66
import setupBrowser from "../util/setupBrowser";
@@ -43,7 +43,7 @@ class ShadowDOM {
4343
if (window.ShadyDOM) {
4444
// inject the styles in the <head>
4545
const cssContent = await getEffectiveStyle(theme, styleUrls, tag);
46-
StyleInjection.createStyleTag(tag, styleUrls, cssContent);
46+
injectWebComponentStyle(tag, cssContent);
4747

4848
// Create the shadow DOM root span
4949
rootSpan = document.createElement("span");
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,69 @@
1-
import { getTheme } from "../Configuration";
2-
import { attachThemeChange, getEffectiveStyle } from "../Theming";
3-
4-
class StyleInjection {
5-
constructor() {
6-
this.tagNamesInHead = [];
7-
this.tagsToStyleUrls = new Map();
8-
attachThemeChange(this.updateStylesInHead.bind(this));
9-
}
1+
import createStyleInHead from "../util/createStyleInHead";
102

11-
createStyleTag(tagName, styleUrls, cssText) {
12-
if (this.tagNamesInHead.indexOf(tagName) !== -1) {
13-
return;
14-
}
3+
const injectedForTags = [];
154

16-
const style = document.createElement("style");
17-
style.type = "text/css";
18-
style.setAttribute("data-sap-source", tagName);
19-
style.innerHTML = cssText;
20-
document.head.appendChild(style);
5+
/**
6+
* Creates/updates a style element holding all CSS Custom Properties
7+
* @param cssText
8+
*/
9+
const injectThemeProperties = cssText => {
10+
// Needed for all browsers
11+
let styleElement = document.head.querySelector(`style[ui5-webcomponents-theme-properties]`);
12+
if (styleElement) {
13+
styleElement.textContent = cssText || ""; // in case of undefined
14+
} else {
15+
styleElement = createStyleInHead(cssText, { "ui5-webcomponents-theme-properties": "" });
16+
}
2117

22-
this.tagNamesInHead.push(tagName);
23-
this.tagsToStyleUrls.set(tagName, styleUrls);
18+
// IE only
19+
if (window.CSSVarsSimulation) {
20+
window.CSSVarsSimulation.findCSSVars(cssText);
2421
}
22+
};
2523

26-
async updateStylesInHead() {
27-
if (!window.ShadyDOM) {
28-
return;
29-
}
24+
/**
25+
* Creates a style element holding the CSS for a web component (and resolves CSS Custom Properties for IE)
26+
* @param tagName
27+
* @param cssText
28+
*/
29+
const injectWebComponentStyle = (tagName, cssText) => {
30+
if (!window.ShadyDOM) {
31+
return;
32+
}
3033

31-
const theme = getTheme();
32-
this.tagNamesInHead.forEach(async tagName => {
33-
const styleUrls = this.tagsToStyleUrls.get(tagName);
34-
const css = await getEffectiveStyle(theme, styleUrls, tagName);
34+
// Edge and IE
35+
if (injectedForTags.indexOf(tagName) !== -1) {
36+
return;
37+
}
38+
createStyleInHead(cssText, { "data-sap-source": tagName });
39+
injectedForTags.push(tagName);
3540

36-
const styleElement = document.head.querySelector(`style[data-sap-source="${tagName}"]`);
41+
// IE only
42+
if (window.CSSVarsSimulation) {
43+
const resolvedVarsCSS = window.CSSVarsSimulation.applyCSSVars(cssText);
44+
createStyleInHead(resolvedVarsCSS, { "data-sap-source-replaced-vars": tagName });
45+
}
46+
};
3747

38-
if (styleElement) {
39-
styleElement.innerHTML = css || ""; // in case of undefined
40-
} else {
41-
this.createStyleTag(tagName, styleUrls, css || "");
42-
}
43-
});
48+
/**
49+
* Updates the style elements holding the CSS for all web components by resolving the CSS Custom properties
50+
*/
51+
const updateWebComponentStyles = () => {
52+
if (!window.CSSVarsSimulation) {
53+
return;
4454
}
45-
}
4655

47-
export default new StyleInjection();
56+
// IE only
57+
injectedForTags.forEach(tagName => {
58+
const originalStyleElement = document.head.querySelector(`style[data-sap-source="${tagName}"]`);
59+
const replacedVarsStyleElement = document.head.querySelector(`style[data-sap-source-replaced-vars="${tagName}"]`);
60+
const resolvedVarsCSS = window.CSSVarsSimulation.applyCSSVars(originalStyleElement.textContent);
61+
replacedVarsStyleElement.textContent = resolvedVarsCSS;
62+
});
63+
};
64+
65+
export {
66+
injectThemeProperties,
67+
injectWebComponentStyle,
68+
updateWebComponentStyles,
69+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { fetchTextOnce } from "../util/FetchHelper";
2+
3+
const themeURLs = new Map();
4+
const propertiesStyles = new Map();
5+
6+
const registerThemeProperties = (packageName, themeName, data) => {
7+
if (data.includes(":root")) {
8+
// inlined content
9+
propertiesStyles.set(`${packageName}_${themeName}`, data);
10+
} else {
11+
// url for fetching
12+
themeURLs.set(`${packageName}_${themeName}`, data);
13+
}
14+
};
15+
16+
const getThemeProperties = async (packageName, themeName) => {
17+
const style = propertiesStyles.get(`${packageName}_${themeName}`);
18+
if (style) {
19+
return style;
20+
}
21+
22+
const data = await fetchThemeProperties(packageName, themeName);
23+
propertiesStyles.set(`${packageName}_${themeName}`, data);
24+
return data;
25+
};
26+
27+
const fetchThemeProperties = async (packageName, themeName) => {
28+
const url = themeURLs.get(`${packageName}_${themeName}`);
29+
return fetchTextOnce(url);
30+
};
31+
32+
export { registerThemeProperties, getThemeProperties };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Creates a <style> tag in the <head> tag
3+
* @param cssText - the CSS
4+
* @param attributes - optional attributes to add to the tag
5+
* @returns {HTMLElement}
6+
*/
7+
const createStyleInHead = (cssText, attributes = {}) => {
8+
const style = document.createElement("style");
9+
style.type = "text/css";
10+
11+
Object.entries(attributes).forEach(pair => style.setAttribute(...pair));
12+
13+
style.textContent = cssText;
14+
document.head.appendChild(style);
15+
return style;
16+
};
17+
18+
export default createStyleInHead;

packages/main/.eslintignore

+1
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ bundle.esm.js
88
bundle.es5.js
99
rollup.config*.js
1010
wdio.conf.js
11+
postcss.config.js

packages/main/bundle.esm.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import "@ui5/webcomponents-base/src/sap/ui/webcomponents/base/browsersupport/Edg
33

44
import "@ui5/webcomponents-base/src/sap/ui/webcomponents/base/shims/jquery-shim";
55
import "@ui5/webcomponents-base/src/sap/ui/webcomponents/base/events/PolymerGestures";
6+
import "./src/ThemePropertiesProvider";
67

78
import Gregorian from "@ui5/webcomponents-core/dist/sap/ui/core/date/Gregorian";
89
import Buddhist from "@ui5/webcomponents-core/dist/sap/ui/core/date/Buddhist";
@@ -62,4 +63,4 @@ import * as Theming from "@ui5/webcomponents-base/src/sap/ui/webcomponents/base/
6263
window["sap-ui-webcomponents-main-bundle"] = {
6364
configuration,
6465
Theming,
65-
};
66+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
const postcssImport = require('postcss-import');
2+
module.exports = {
3+
plugins: [
4+
postcssImport()
5+
]
6+
}
7+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const postcssNesting = require('postcss-nesting');
2+
const postcssAddFallback = require('../../lib/postcss-add-fallback/index.js');
3+
4+
module.exports = {
5+
plugins: [
6+
postcssNesting(),
7+
postcssAddFallback({importFrom: "./dist/themes-next/sap_fiori_3/parameters-bundle.css"}),
8+
]
9+
}

0 commit comments

Comments
 (0)