Skip to content

Commit

Permalink
[change] StyleSheet: compile styles directly to CSS
Browse files Browse the repository at this point in the history
Introduces a centralized compiler for "atomic" and "classic" CSS output. The
"classic" compiler is for internal use only and offers no CSS safety
guarantees. The "atomic compiler is used to implement the public-facing
StyleSheet API.

The atomic compiler now maps the React style declarations, rather than CSS
style declarations, to CSS rules. This avoids having to convert React styles to
CSS styles before being able to lookup classNames. And it reduces the number of
CSS rules needed by each DOM element.

Before:

    { paddingHorizontal: 0; }
        ↓
    .paddingLeft-0 { padding-left: 0; }
    .paddingRight-0 { padding-right: 0; }

After:

    { paddingHorizontal: 0; }
        ↓
    .paddingHorizontal-0 { padding-left: 0; padding-right: 0 }

Overview of previous StyleSheet resolver:

1. Localise styles
2. Transform to CSS styles
3. Expand short-form properties
4a. Lookup Atomic CSS for each declaration
4b. Compile Atomic CSS for each static declaration
  i. Vendor prefix
  ii. Insert CSS rules
4c. Create inline style for each dynamic-only declaration
  i. Vendor prefix

Overview of new StyleSheet design:

1. Localise styles
2a. Lookup Atomic CSS for each declaration
2b. Compile Atomic CSS for each static declarations
  i. Transform to CSS styles
  ii. Expand short-form properties
  iii. Vendor prefix
  iiii. Insert CSS rules
2c. Create inline style for each dynamic-only declaration
  i. Transform to CSS styles
  ii. Expand short-form properties
  iii. Vendor prefix

Ref #1136
  • Loading branch information
necolas committed Mar 12, 2019
1 parent 29be779 commit 9f860b8
Show file tree
Hide file tree
Showing 40 changed files with 1,032 additions and 858 deletions.
1 change: 1 addition & 0 deletions .watchmanconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ exports[`components/ActivityIndicator prop "animating" is "false" 1`] = `
Object {
"animationDuration": "0.75s",
"animationIterationCount": "infinite",
"animationName": Array [
"animationKeyframes": Array [
Object {
"0%": Object {
"transform": Array [
Expand Down Expand Up @@ -97,7 +97,7 @@ exports[`components/ActivityIndicator prop "animating" is "true" 1`] = `
Object {
"animationDuration": "0.75s",
"animationIterationCount": "infinite",
"animationName": Array [
"animationKeyframes": Array [
Object {
"0%": Object {
"transform": Array [
Expand Down Expand Up @@ -211,7 +211,7 @@ exports[`components/ActivityIndicator prop "hidesWhenStopped" is "false" 1`] = `
Object {
"animationDuration": "0.75s",
"animationIterationCount": "infinite",
"animationName": Array [
"animationKeyframes": Array [
Object {
"0%": Object {
"transform": Array [
Expand Down Expand Up @@ -290,7 +290,7 @@ exports[`components/ActivityIndicator prop "hidesWhenStopped" is "true" 1`] = `
Object {
"animationDuration": "0.75s",
"animationIterationCount": "infinite",
"animationName": Array [
"animationKeyframes": Array [
Object {
"0%": Object {
"transform": Array [
Expand Down Expand Up @@ -370,7 +370,7 @@ exports[`components/ActivityIndicator prop "size" is "large" 1`] = `
Object {
"animationDuration": "0.75s",
"animationIterationCount": "infinite",
"animationName": Array [
"animationKeyframes": Array [
Object {
"0%": Object {
"transform": Array [
Expand Down Expand Up @@ -448,7 +448,7 @@ exports[`components/ActivityIndicator prop "size" is a number 1`] = `
Object {
"animationDuration": "0.75s",
"animationIterationCount": "infinite",
"animationName": Array [
"animationKeyframes": Array [
Object {
"0%": Object {
"transform": Array [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ const styles = StyleSheet.create({
},
animation: {
animationDuration: '0.75s',
animationName: [
animationKeyframes: [
{
'0%': { transform: [{ rotate: '0deg' }] },
'100%': { transform: [{ rotate: '360deg' }] }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ exports[`AppRegistry getApplication returns "element" and "getStyleElement" 1`]
exports[`AppRegistry getApplication returns "element" and "getStyleElement" 2`] = `
"<style id=\\"react-native-stylesheet\\">@media all {
[stylesheet-group=\\"0\\"]{}
:focus:not([data-rn-focusvisible-x92cna]){outline: none;}
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0);}
body{margin:0;}
button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;}
input::-webkit-inner-spin-button,input::-webkit-outer-spin-button,input::-webkit-search-cancel-button,input::-webkit-search-decoration,input::-webkit-search-results-button,input::-webkit-search-results-decoration{display:none;}
}
@media all {
[stylesheet-group=\\"0.1\\"]{}
:focus:not([data-focusvisible-polyfill]){outline: none;}
}</style>"
`;
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ exports[`components/Picker prop "children" items 1`] = `

exports[`components/Picker prop "children" renders items 1`] = `
<select
className="rn-fontFamily-14xgk7a rn-fontSize-7cikom rn-marginTop-1mnahxq rn-marginRight-61z16t rn-marginBottom-p1pxzi rn-marginLeft-11wrixw"
className="rn-fontFamily-1qd0xha rn-fontSize-7cikom rn-margin-crgep1"
data-focusable={true}
onChange={[Function]}
>
Expand Down
2 changes: 1 addition & 1 deletion packages/react-native-web/src/exports/ProgressBar/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ const styles = StyleSheet.create({
},
animation: {
animationDuration: '1s',
animationName: [
animationKeyframes: [
{
'0%': { transform: [{ translateX: '-100%' }] },
'100%': { transform: [{ translateX: '400%' }] }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright (c) 2016-present, Nicolas Gallagher.
* Copyright (c) Nicolas Gallagher.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
Expand All @@ -13,47 +13,83 @@
*/

import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment';
import createReactDOMStyle from './createReactDOMStyle';
import createCSSStyleSheet from './createCSSStyleSheet';
import createCompileableStyle from './createCompileableStyle';
import createOrderedCSSStyleSheet from './createOrderedCSSStyleSheet';
import flattenArray from '../../modules/flattenArray';
import flattenStyle from './flattenStyle';
import I18nManager from '../I18nManager';
import i18nStyle from './i18nStyle';
import { prefixInlineStyles } from '../../modules/prefixStyles';
import StyleSheetManager from './StyleSheetManager';
import { atomic, inline, stringifyValueWithProperty } from './compile';
import initialRules from './initialRules';
import modality from './modality';
import { STYLE_ELEMENT_ID, STYLE_GROUPS } from './constants';

const emptyObject = {};

export default class ReactNativeStyleResolver {
_init() {
this.cache = { ltr: {}, rtl: {}, rtlNoSwap: {} };
this.injectedCache = { ltr: {}, rtl: {}, rtlNoSwap: {} };
this.styleSheetManager = new StyleSheetManager();
this.sheet = createOrderedCSSStyleSheet(createCSSStyleSheet(STYLE_ELEMENT_ID));
this._lookupCache = {
byClassName: {},
byProp: {}
};
}

constructor() {
this._init();
modality(rule => this.sheet.insert(rule, STYLE_GROUPS.modality));
initialRules.forEach(rule => {
this.sheet.insert(rule, STYLE_GROUPS.reset);
});
}

_addToCache(className, prop, value) {
const cache = this._lookupCache;
if (!cache.byProp[prop]) {
cache.byProp[prop] = {};
}
cache.byProp[prop][value] = className;
cache.byClassName[className] = { prop, value };
}

getClassName(prop, value) {
const val = stringifyValueWithProperty(value, prop);
const cache = this._lookupCache.byProp;
return cache[prop] && cache[prop].hasOwnProperty(val) && cache[prop][val];
}

getDeclaration(className) {
const cache = this._lookupCache.byClassName;
return cache[className] || emptyObject;
}

getStyleSheet() {
// reset state on the server so critical css is always the result
const sheet = this.styleSheetManager.getStyleSheet();
const textContent = this.sheet.getTextContent();
if (!canUseDOM) {
this._init();
}
return sheet;
return {
id: STYLE_ELEMENT_ID,
textContent
};
}

_injectRegisteredStyle(id) {
const { doLeftAndRightSwapInRTL, isRTL } = I18nManager;
const dir = isRTL ? (doLeftAndRightSwapInRTL ? 'rtl' : 'rtlNoSwap') : 'ltr';
if (!this.injectedCache[dir][id]) {
const style = flattenStyle(id);
const domStyle = createReactDOMStyle(i18nStyle(style));
Object.keys(domStyle).forEach(styleProp => {
const value = domStyle[styleProp];
if (value != null) {
this.styleSheetManager.injectDeclaration(styleProp, value);
}
const style = createCompileableStyle(i18nStyle(flattenStyle(id)));
const results = atomic(style);
Object.values(results).forEach(({ identifier, property, rules, value }) => {
this._addToCache(identifier, property, value);
rules.forEach(rule => {
const group = STYLE_GROUPS.custom[property] || STYLE_GROUPS.atomic;
this.sheet.insert(rule, group);
});
});
this.injectedCache[dir][id] = true;
}
Expand Down Expand Up @@ -91,7 +127,7 @@ export default class ReactNativeStyleResolver {
isArrayOfNumbers = false;
} else {
if (isArrayOfNumbers) {
cacheKey += (id + '-');
cacheKey += id + '-';
}
this._injectRegisteredStyle(id);
}
Expand All @@ -113,7 +149,7 @@ export default class ReactNativeStyleResolver {
// Preserves unrecognized class names.
const { classList: rnClassList, style: rnStyle } = rdomClassList.reduce(
(styleProps, className) => {
const { prop, value } = this.styleSheetManager.getDeclaration(className);
const { prop, value } = this.getDeclaration(className);
if (prop) {
styleProps.style[prop] = value;
} else {
Expand All @@ -138,7 +174,7 @@ export default class ReactNativeStyleResolver {
// Next class names take priority over current inline styles
const style = { ...rdomStyle };
rdomClassListNext.forEach(className => {
const { prop } = this.styleSheetManager.getDeclaration(className);
const { prop } = this.getDeclaration(className);
if (style[prop]) {
style[prop] = '';
}
Expand All @@ -154,45 +190,50 @@ export default class ReactNativeStyleResolver {
*/
_resolveStyle(style) {
const flatStyle = flattenStyle(style);
const domStyle = createReactDOMStyle(i18nStyle(flatStyle));

const props = Object.keys(domStyle).reduce(
(props, styleProp) => {
const value = domStyle[styleProp];
if (value != null) {
const className = this.styleSheetManager.getClassName(styleProp, value);
if (className) {
props.classList.push(className);
} else {
// Certain properties and values are not transformed by 'createReactDOMStyle' as they
// require more complex transforms into multiple CSS rules. Here we assume that StyleManager
// can bind these styles to a className, and prevent them becoming invalid inline-styles.
if (
styleProp === 'pointerEvents' ||
styleProp === 'placeholderTextColor' ||
styleProp === 'animationName'
) {
const className = this.styleSheetManager.injectDeclaration(styleProp, value);
if (className) {
props.classList.push(className);
}
const localizedStyle = createCompileableStyle(i18nStyle(flatStyle));

const props = Object.keys(localizedStyle)
.sort()
.reduce(
(props, styleProp) => {
const value = localizedStyle[styleProp];
if (value != null) {
const className = this.getClassName(styleProp, value);
if (className) {
props.classList.push(className);
} else {
if (!props.style) {
props.style = {};
// Certain properties and values are not transformed by 'createReactDOMStyle' as they
// require more complex transforms into multiple CSS rules. Here we assume that StyleManager
// can bind these styles to a className, and prevent them becoming invalid inline-styles.
if (
styleProp === 'pointerEvents' ||
styleProp === 'placeholderTextColor' ||
styleProp === 'animationKeyframes'
) {
const a = atomic({ [styleProp]: value });
Object.values(a).forEach(({ identifier, rules }) => {
props.classList.push(identifier);
rules.forEach(rule => {
this.sheet.insert(rule, STYLE_GROUPS.atomic);
});
});
} else {
if (!props.style) {
props.style = {};
}
// 4x slower render
props.style[styleProp] = value;
}
// 4x slower render
props.style[styleProp] = value;
}
}
}
return props;
},
{ classList: [] }
);
return props;
},
{ classList: [] }
);

props.className = classListToString(props.classList);
if (props.style) {
props.style = prefixInlineStyles(props.style);
props.style = inline(props.style);
}
return props;
}
Expand Down
Loading

0 comments on commit 9f860b8

Please sign in to comment.