Skip to content

Commit 9f860b8

Browse files
committed
[change] StyleSheet: compile styles directly to CSS
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
1 parent 29be779 commit 9f860b8

40 files changed

+1032
-858
lines changed

.watchmanconfig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

packages/react-native-web/src/exports/ActivityIndicator/__tests__/__snapshots__/index-test.js.snap

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ exports[`components/ActivityIndicator prop "animating" is "false" 1`] = `
1717
Object {
1818
"animationDuration": "0.75s",
1919
"animationIterationCount": "infinite",
20-
"animationName": Array [
20+
"animationKeyframes": Array [
2121
Object {
2222
"0%": Object {
2323
"transform": Array [
@@ -97,7 +97,7 @@ exports[`components/ActivityIndicator prop "animating" is "true" 1`] = `
9797
Object {
9898
"animationDuration": "0.75s",
9999
"animationIterationCount": "infinite",
100-
"animationName": Array [
100+
"animationKeyframes": Array [
101101
Object {
102102
"0%": Object {
103103
"transform": Array [
@@ -211,7 +211,7 @@ exports[`components/ActivityIndicator prop "hidesWhenStopped" is "false" 1`] = `
211211
Object {
212212
"animationDuration": "0.75s",
213213
"animationIterationCount": "infinite",
214-
"animationName": Array [
214+
"animationKeyframes": Array [
215215
Object {
216216
"0%": Object {
217217
"transform": Array [
@@ -290,7 +290,7 @@ exports[`components/ActivityIndicator prop "hidesWhenStopped" is "true" 1`] = `
290290
Object {
291291
"animationDuration": "0.75s",
292292
"animationIterationCount": "infinite",
293-
"animationName": Array [
293+
"animationKeyframes": Array [
294294
Object {
295295
"0%": Object {
296296
"transform": Array [
@@ -370,7 +370,7 @@ exports[`components/ActivityIndicator prop "size" is "large" 1`] = `
370370
Object {
371371
"animationDuration": "0.75s",
372372
"animationIterationCount": "infinite",
373-
"animationName": Array [
373+
"animationKeyframes": Array [
374374
Object {
375375
"0%": Object {
376376
"transform": Array [
@@ -448,7 +448,7 @@ exports[`components/ActivityIndicator prop "size" is a number 1`] = `
448448
Object {
449449
"animationDuration": "0.75s",
450450
"animationIterationCount": "infinite",
451-
"animationName": Array [
451+
"animationKeyframes": Array [
452452
Object {
453453
"0%": Object {
454454
"transform": Array [

packages/react-native-web/src/exports/ActivityIndicator/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ const styles = StyleSheet.create({
8686
},
8787
animation: {
8888
animationDuration: '0.75s',
89-
animationName: [
89+
animationKeyframes: [
9090
{
9191
'0%': { transform: [{ rotate: '0deg' }] },
9292
'100%': { transform: [{ rotate: '360deg' }] }

packages/react-native-web/src/exports/AppRegistry/__tests__/__snapshots__/index-test.js.snap

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@ exports[`AppRegistry getApplication returns "element" and "getStyleElement" 1`]
1111
exports[`AppRegistry getApplication returns "element" and "getStyleElement" 2`] = `
1212
"<style id=\\"react-native-stylesheet\\">@media all {
1313
[stylesheet-group=\\"0\\"]{}
14-
:focus:not([data-rn-focusvisible-x92cna]){outline: none;}
1514
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0);}
1615
body{margin:0;}
1716
button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;}
1817
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;}
18+
}
19+
@media all {
20+
[stylesheet-group=\\"0.1\\"]{}
21+
:focus:not([data-focusvisible-polyfill]){outline: none;}
1922
}</style>"
2023
`;

packages/react-native-web/src/exports/Picker/__tests__/__snapshots__/index-test.js.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ exports[`components/Picker prop "children" items 1`] = `
1010

1111
exports[`components/Picker prop "children" renders items 1`] = `
1212
<select
13-
className="rn-fontFamily-14xgk7a rn-fontSize-7cikom rn-marginTop-1mnahxq rn-marginRight-61z16t rn-marginBottom-p1pxzi rn-marginLeft-11wrixw"
13+
className="rn-fontFamily-1qd0xha rn-fontSize-7cikom rn-margin-crgep1"
1414
data-focusable={true}
1515
onChange={[Function]}
1616
>

packages/react-native-web/src/exports/ProgressBar/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ const styles = StyleSheet.create({
9494
},
9595
animation: {
9696
animationDuration: '1s',
97-
animationName: [
97+
animationKeyframes: [
9898
{
9999
'0%': { transform: [{ translateX: '-100%' }] },
100100
'100%': { transform: [{ translateX: '400%' }] }

packages/react-native-web/src/exports/StyleSheet/ReactNativeStyleResolver.js

Lines changed: 90 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright (c) 2016-present, Nicolas Gallagher.
2+
* Copyright (c) Nicolas Gallagher.
33
*
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
@@ -13,47 +13,83 @@
1313
*/
1414

1515
import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment';
16-
import createReactDOMStyle from './createReactDOMStyle';
16+
import createCSSStyleSheet from './createCSSStyleSheet';
17+
import createCompileableStyle from './createCompileableStyle';
18+
import createOrderedCSSStyleSheet from './createOrderedCSSStyleSheet';
1719
import flattenArray from '../../modules/flattenArray';
1820
import flattenStyle from './flattenStyle';
1921
import I18nManager from '../I18nManager';
2022
import i18nStyle from './i18nStyle';
21-
import { prefixInlineStyles } from '../../modules/prefixStyles';
22-
import StyleSheetManager from './StyleSheetManager';
23+
import { atomic, inline, stringifyValueWithProperty } from './compile';
24+
import initialRules from './initialRules';
25+
import modality from './modality';
26+
import { STYLE_ELEMENT_ID, STYLE_GROUPS } from './constants';
2327

2428
const emptyObject = {};
2529

2630
export default class ReactNativeStyleResolver {
2731
_init() {
2832
this.cache = { ltr: {}, rtl: {}, rtlNoSwap: {} };
2933
this.injectedCache = { ltr: {}, rtl: {}, rtlNoSwap: {} };
30-
this.styleSheetManager = new StyleSheetManager();
34+
this.sheet = createOrderedCSSStyleSheet(createCSSStyleSheet(STYLE_ELEMENT_ID));
35+
this._lookupCache = {
36+
byClassName: {},
37+
byProp: {}
38+
};
3139
}
3240

3341
constructor() {
3442
this._init();
43+
modality(rule => this.sheet.insert(rule, STYLE_GROUPS.modality));
44+
initialRules.forEach(rule => {
45+
this.sheet.insert(rule, STYLE_GROUPS.reset);
46+
});
47+
}
48+
49+
_addToCache(className, prop, value) {
50+
const cache = this._lookupCache;
51+
if (!cache.byProp[prop]) {
52+
cache.byProp[prop] = {};
53+
}
54+
cache.byProp[prop][value] = className;
55+
cache.byClassName[className] = { prop, value };
56+
}
57+
58+
getClassName(prop, value) {
59+
const val = stringifyValueWithProperty(value, prop);
60+
const cache = this._lookupCache.byProp;
61+
return cache[prop] && cache[prop].hasOwnProperty(val) && cache[prop][val];
62+
}
63+
64+
getDeclaration(className) {
65+
const cache = this._lookupCache.byClassName;
66+
return cache[className] || emptyObject;
3567
}
3668

3769
getStyleSheet() {
3870
// reset state on the server so critical css is always the result
39-
const sheet = this.styleSheetManager.getStyleSheet();
71+
const textContent = this.sheet.getTextContent();
4072
if (!canUseDOM) {
4173
this._init();
4274
}
43-
return sheet;
75+
return {
76+
id: STYLE_ELEMENT_ID,
77+
textContent
78+
};
4479
}
4580

4681
_injectRegisteredStyle(id) {
4782
const { doLeftAndRightSwapInRTL, isRTL } = I18nManager;
4883
const dir = isRTL ? (doLeftAndRightSwapInRTL ? 'rtl' : 'rtlNoSwap') : 'ltr';
4984
if (!this.injectedCache[dir][id]) {
50-
const style = flattenStyle(id);
51-
const domStyle = createReactDOMStyle(i18nStyle(style));
52-
Object.keys(domStyle).forEach(styleProp => {
53-
const value = domStyle[styleProp];
54-
if (value != null) {
55-
this.styleSheetManager.injectDeclaration(styleProp, value);
56-
}
85+
const style = createCompileableStyle(i18nStyle(flattenStyle(id)));
86+
const results = atomic(style);
87+
Object.values(results).forEach(({ identifier, property, rules, value }) => {
88+
this._addToCache(identifier, property, value);
89+
rules.forEach(rule => {
90+
const group = STYLE_GROUPS.custom[property] || STYLE_GROUPS.atomic;
91+
this.sheet.insert(rule, group);
92+
});
5793
});
5894
this.injectedCache[dir][id] = true;
5995
}
@@ -91,7 +127,7 @@ export default class ReactNativeStyleResolver {
91127
isArrayOfNumbers = false;
92128
} else {
93129
if (isArrayOfNumbers) {
94-
cacheKey += (id + '-');
130+
cacheKey += id + '-';
95131
}
96132
this._injectRegisteredStyle(id);
97133
}
@@ -113,7 +149,7 @@ export default class ReactNativeStyleResolver {
113149
// Preserves unrecognized class names.
114150
const { classList: rnClassList, style: rnStyle } = rdomClassList.reduce(
115151
(styleProps, className) => {
116-
const { prop, value } = this.styleSheetManager.getDeclaration(className);
152+
const { prop, value } = this.getDeclaration(className);
117153
if (prop) {
118154
styleProps.style[prop] = value;
119155
} else {
@@ -138,7 +174,7 @@ export default class ReactNativeStyleResolver {
138174
// Next class names take priority over current inline styles
139175
const style = { ...rdomStyle };
140176
rdomClassListNext.forEach(className => {
141-
const { prop } = this.styleSheetManager.getDeclaration(className);
177+
const { prop } = this.getDeclaration(className);
142178
if (style[prop]) {
143179
style[prop] = '';
144180
}
@@ -154,45 +190,50 @@ export default class ReactNativeStyleResolver {
154190
*/
155191
_resolveStyle(style) {
156192
const flatStyle = flattenStyle(style);
157-
const domStyle = createReactDOMStyle(i18nStyle(flatStyle));
158-
159-
const props = Object.keys(domStyle).reduce(
160-
(props, styleProp) => {
161-
const value = domStyle[styleProp];
162-
if (value != null) {
163-
const className = this.styleSheetManager.getClassName(styleProp, value);
164-
if (className) {
165-
props.classList.push(className);
166-
} else {
167-
// Certain properties and values are not transformed by 'createReactDOMStyle' as they
168-
// require more complex transforms into multiple CSS rules. Here we assume that StyleManager
169-
// can bind these styles to a className, and prevent them becoming invalid inline-styles.
170-
if (
171-
styleProp === 'pointerEvents' ||
172-
styleProp === 'placeholderTextColor' ||
173-
styleProp === 'animationName'
174-
) {
175-
const className = this.styleSheetManager.injectDeclaration(styleProp, value);
176-
if (className) {
177-
props.classList.push(className);
178-
}
193+
const localizedStyle = createCompileableStyle(i18nStyle(flatStyle));
194+
195+
const props = Object.keys(localizedStyle)
196+
.sort()
197+
.reduce(
198+
(props, styleProp) => {
199+
const value = localizedStyle[styleProp];
200+
if (value != null) {
201+
const className = this.getClassName(styleProp, value);
202+
if (className) {
203+
props.classList.push(className);
179204
} else {
180-
if (!props.style) {
181-
props.style = {};
205+
// Certain properties and values are not transformed by 'createReactDOMStyle' as they
206+
// require more complex transforms into multiple CSS rules. Here we assume that StyleManager
207+
// can bind these styles to a className, and prevent them becoming invalid inline-styles.
208+
if (
209+
styleProp === 'pointerEvents' ||
210+
styleProp === 'placeholderTextColor' ||
211+
styleProp === 'animationKeyframes'
212+
) {
213+
const a = atomic({ [styleProp]: value });
214+
Object.values(a).forEach(({ identifier, rules }) => {
215+
props.classList.push(identifier);
216+
rules.forEach(rule => {
217+
this.sheet.insert(rule, STYLE_GROUPS.atomic);
218+
});
219+
});
220+
} else {
221+
if (!props.style) {
222+
props.style = {};
223+
}
224+
// 4x slower render
225+
props.style[styleProp] = value;
182226
}
183-
// 4x slower render
184-
props.style[styleProp] = value;
185227
}
186228
}
187-
}
188-
return props;
189-
},
190-
{ classList: [] }
191-
);
229+
return props;
230+
},
231+
{ classList: [] }
232+
);
192233

193234
props.className = classListToString(props.classList);
194235
if (props.style) {
195-
props.style = prefixInlineStyles(props.style);
236+
props.style = inline(props.style);
196237
}
197238
return props;
198239
}

0 commit comments

Comments
 (0)