Skip to content

Commit

Permalink
feat: framework-level support for CSS Custom Properties (#196)
Browse files Browse the repository at this point in the history
  • Loading branch information
vladitasev committed Mar 19, 2019
1 parent 0934d70 commit 291829a
Show file tree
Hide file tree
Showing 28 changed files with 3,321 additions and 87 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,4 @@ yarn-debug.log*
.yarn-integrity

# Ignore default target directory for the npm package 'ui5-schemas'
.tmp
.tmp
4 changes: 4 additions & 0 deletions packages/base/src/sap/ui/webcomponents/base/Bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import whenDOMReady from "./util/whenDOMReady";
import EventEnrichment from "./events/EventEnrichment";
import { insertIconFontFace } from "./IconFonts";
import DOMEventHandler from "./DOMEventHandler";
import { initConfiguration } from "./Configuration";
import { applyTheme } from "./Theming";
import whenPolyfillLoaded from "./compatibility/whenPolyfillLoaded";

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

bootPromise = new Promise(async resolve => {
await whenDOMReady();
initConfiguration();
applyTheme();
insertIconFontFace();
DOMEventHandler.start();
await whenPolyfillLoaded();
Expand Down
9 changes: 6 additions & 3 deletions packages/base/src/sap/ui/webcomponents/base/Configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,14 @@ const applyConfigurations = () => {
});
};

parseConfigurationScript();
parseURLParameters();
applyConfigurations();
const initConfiguration = () => {
parseConfigurationScript();
parseURLParameters();
applyConfigurations();
};

export {
initConfiguration,
getTheme,
getRTL,
getLanguage,
Expand Down
33 changes: 31 additions & 2 deletions packages/base/src/sap/ui/webcomponents/base/Theming.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,44 @@
import { getTheme, _setTheme } from "./Configuration";
import { getStyles } from "./theming/ThemeBundle";
import { getCustomCSS } from "./theming/CustomStyle";
import { getThemeProperties } from "./theming/ThemeProperties";
import { injectThemeProperties, updateWebComponentStyles } from "./theming/StyleInjection";

const themeChangeCallbacks = [];

const getDefaultTheme = () => {
return "sap_fiori_3";
};

const attachThemeChange = function attachThemeChange(callback) {
if (themeChangeCallbacks.indexOf(callback) === -1) {
themeChangeCallbacks.push(callback);
}
};

const setTheme = function setTheme(theme) {
const applyTheme = async () => {
let cssText = "";
const theme = getTheme();

const defaultTheme = getDefaultTheme();
if (theme !== defaultTheme) {
cssText = await getThemeProperties("@ui5/webcomponents", theme);
}
injectThemeProperties(cssText);
updateWebComponentStyles();
};

const setTheme = async theme => {
if (theme === getTheme()) {
return;
}

// Update configuration
_setTheme(theme);

// Update CSS Custom Properties
await applyTheme();

themeChangeCallbacks.forEach(callback => callback(theme));
};

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

export { attachThemeChange, setTheme, getEffectiveStyle };
export {
getDefaultTheme,
attachThemeChange,
applyTheme,
setTheme,
getEffectiveStyle,
};
14 changes: 0 additions & 14 deletions packages/base/src/sap/ui/webcomponents/base/WebComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import Integer from "./types/Integer";
import ControlRenderer from "./ControlRenderer";
import RenderScheduler from "./RenderScheduler";
import TemplateContext from "./TemplateContext";
import { attachThemeChange } from "./Theming";
import State from "./State";

const metadata = {
Expand Down Expand Up @@ -50,19 +49,6 @@ class WebComponent extends HTMLElement {
this._domRefReadyPromise._deferredResolve = deferredResolve;

this._monitoredChildProps = new Map();

// Only for native Shadow DOM, and only when present
if (!window.ShadyDOM && !this.constructor.getMetadata().getNoShadowDOM()) {
attachThemeChange(this._onThemeChange.bind(this));
}
}

_onThemeChange() {
const klass = this.constructor;
const tag = klass.getMetadata().getTag();
const styleURLs = klass.getMetadata().getStyleUrl();

ShadowDOM.updateStyle(tag, this.shadowRoot, styleURLs);
}

_whenShadowRootReady() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,8 @@ import "../thirdparty/fetch";
// async - await
import "regenerator-runtime/runtime";

// CSS Custom Properties
import "../compatibility/CSSVarsSimulation";

// Plus all polyfills needed for Edge are also needed for IE11
import "./Edge";
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
let vars = new Map();

/**
* Scans the given string, extracts all CSS vars from it and stores them internally
* @param styleString - string containing CSS variables
*/
const findCSSVars = styleString => {
vars = new Map();
const couples = styleString.match(/--[^:)]+:\s*[^;}]+/g) || [];
couples.forEach(couple => {
const [varName, varValue] = couple.split(/:\s*/);
vars.set(varName, varValue);
});
};

/**
* Replaces all occurrences of CSS vars with their values (and fallback values)
* @param styleString - string containing CSS selectors
* @returns {*}
*/
const applyCSSVars = styleString => {
// Replace all variables, with or without default value (default value removed too)
vars.forEach((varValue, varName) => {
const re = new RegExp(`var\\(\\s*${varName}.*?\\)`, "g");
styleString = styleString.replace(re, varValue);
});

// Replace all unresolved variables with their default values
styleString = styleString.replace(/var\(.*?,\s*(.*?)\)/g, "$1");

return styleString;
};

const CSSVarsSimulation = {
findCSSVars,
applyCSSVars,
};

window.CSSVarsSimulation = CSSVarsSimulation;

export default CSSVarsSimulation;
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getTheme, getRTL, getCompactSize } from "../Configuration";

import StyleInjection from "../theming/StyleInjection";
import { injectWebComponentStyle } from "../theming/StyleInjection";
import { registerStyle } from "../theming/ThemeBundle";

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

// Create the shadow DOM root span
rootSpan = document.createElement("span");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,47 +1,69 @@
import { getTheme } from "../Configuration";
import { attachThemeChange, getEffectiveStyle } from "../Theming";

class StyleInjection {
constructor() {
this.tagNamesInHead = [];
this.tagsToStyleUrls = new Map();
attachThemeChange(this.updateStylesInHead.bind(this));
}
import createStyleInHead from "../util/createStyleInHead";

createStyleTag(tagName, styleUrls, cssText) {
if (this.tagNamesInHead.indexOf(tagName) !== -1) {
return;
}
const injectedForTags = [];

const style = document.createElement("style");
style.type = "text/css";
style.setAttribute("data-sap-source", tagName);
style.innerHTML = cssText;
document.head.appendChild(style);
/**
* Creates/updates a style element holding all CSS Custom Properties
* @param cssText
*/
const injectThemeProperties = cssText => {
// Needed for all browsers
let styleElement = document.head.querySelector(`style[ui5-webcomponents-theme-properties]`);
if (styleElement) {
styleElement.textContent = cssText || ""; // in case of undefined
} else {
styleElement = createStyleInHead(cssText, { "ui5-webcomponents-theme-properties": "" });
}

this.tagNamesInHead.push(tagName);
this.tagsToStyleUrls.set(tagName, styleUrls);
// IE only
if (window.CSSVarsSimulation) {
window.CSSVarsSimulation.findCSSVars(cssText);
}
};

async updateStylesInHead() {
if (!window.ShadyDOM) {
return;
}
/**
* Creates a style element holding the CSS for a web component (and resolves CSS Custom Properties for IE)
* @param tagName
* @param cssText
*/
const injectWebComponentStyle = (tagName, cssText) => {
if (!window.ShadyDOM) {
return;
}

const theme = getTheme();
this.tagNamesInHead.forEach(async tagName => {
const styleUrls = this.tagsToStyleUrls.get(tagName);
const css = await getEffectiveStyle(theme, styleUrls, tagName);
// Edge and IE
if (injectedForTags.indexOf(tagName) !== -1) {
return;
}
createStyleInHead(cssText, { "data-sap-source": tagName });
injectedForTags.push(tagName);

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

if (styleElement) {
styleElement.innerHTML = css || ""; // in case of undefined
} else {
this.createStyleTag(tagName, styleUrls, css || "");
}
});
/**
* Updates the style elements holding the CSS for all web components by resolving the CSS Custom properties
*/
const updateWebComponentStyles = () => {
if (!window.CSSVarsSimulation) {
return;
}
}

export default new StyleInjection();
// IE only
injectedForTags.forEach(tagName => {
const originalStyleElement = document.head.querySelector(`style[data-sap-source="${tagName}"]`);
const replacedVarsStyleElement = document.head.querySelector(`style[data-sap-source-replaced-vars="${tagName}"]`);
const resolvedVarsCSS = window.CSSVarsSimulation.applyCSSVars(originalStyleElement.textContent);
replacedVarsStyleElement.textContent = resolvedVarsCSS;
});
};

export {
injectThemeProperties,
injectWebComponentStyle,
updateWebComponentStyles,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { fetchTextOnce } from "../util/FetchHelper";

const themeURLs = new Map();
const propertiesStyles = new Map();

const registerThemeProperties = (packageName, themeName, data) => {
if (data.includes(":root")) {
// inlined content
propertiesStyles.set(`${packageName}_${themeName}`, data);
} else {
// url for fetching
themeURLs.set(`${packageName}_${themeName}`, data);
}
};

const getThemeProperties = async (packageName, themeName) => {
const style = propertiesStyles.get(`${packageName}_${themeName}`);
if (style) {
return style;
}

const data = await fetchThemeProperties(packageName, themeName);
propertiesStyles.set(`${packageName}_${themeName}`, data);
return data;
};

const fetchThemeProperties = async (packageName, themeName) => {
const url = themeURLs.get(`${packageName}_${themeName}`);
return fetchTextOnce(url);
};

export { registerThemeProperties, getThemeProperties };
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Creates a <style> tag in the <head> tag
* @param cssText - the CSS
* @param attributes - optional attributes to add to the tag
* @returns {HTMLElement}
*/
const createStyleInHead = (cssText, attributes = {}) => {
const style = document.createElement("style");
style.type = "text/css";

Object.entries(attributes).forEach(pair => style.setAttribute(...pair));

style.textContent = cssText;
document.head.appendChild(style);
return style;
};

export default createStyleInHead;
1 change: 1 addition & 0 deletions packages/main/.eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ bundle.esm.js
bundle.es5.js
rollup.config*.js
wdio.conf.js
postcss.config.js
3 changes: 2 additions & 1 deletion packages/main/bundle.esm.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import "@ui5/webcomponents-base/src/sap/ui/webcomponents/base/browsersupport/Edg

import "@ui5/webcomponents-base/src/sap/ui/webcomponents/base/shims/jquery-shim";
import "@ui5/webcomponents-base/src/sap/ui/webcomponents/base/events/PolymerGestures";
import "./src/ThemePropertiesProvider";

import Gregorian from "@ui5/webcomponents-core/dist/sap/ui/core/date/Gregorian";
import Buddhist from "@ui5/webcomponents-core/dist/sap/ui/core/date/Buddhist";
Expand Down Expand Up @@ -62,4 +63,4 @@ import * as Theming from "@ui5/webcomponents-base/src/sap/ui/webcomponents/base/
window["sap-ui-webcomponents-main-bundle"] = {
configuration,
Theming,
};
};
7 changes: 7 additions & 0 deletions packages/main/config/postcss.bundles/postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const postcssImport = require('postcss-import');
module.exports = {
plugins: [
postcssImport()
]
}

9 changes: 9 additions & 0 deletions packages/main/config/postcss.components/postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const postcssNesting = require('postcss-nesting');
const postcssAddFallback = require('../../lib/postcss-add-fallback/index.js');

module.exports = {
plugins: [
postcssNesting(),
postcssAddFallback({importFrom: "./dist/themes-next/sap_fiori_3/parameters-bundle.css"}),
]
}
Loading

0 comments on commit 291829a

Please sign in to comment.