Skip to content

Commit 29be779

Browse files
committed
[change] CSS insertion by OrderedCSSStyleSheet
`OrderedCSSStyleSheet` can be used to control the order in which CSS rules are inserted. This feature is necessary to support the combined use of Classic CSS and Atomic CSS. It also makes it possible to control the order of Atomic CSS rules, which is necessary to correctly resolve style conflicts (e.g., between 'margin' and 'marginHorizontal') without expanding short-form properties to long-form properties. Ref necolas#1136
1 parent 2a418be commit 29be779

File tree

9 files changed

+357
-86
lines changed

9 files changed

+357
-86
lines changed

Diff for: .eslintrc

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"window": false,
3535
// Flow global types,
3636
"$Enum": false,
37+
"CSSStyleSheet": false,
3738
"HTMLInputElement": false,
3839
"ReactClass": false,
3940
"ReactComponent": false,

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ exports[`AppRegistry getApplication returns "element" and "getStyleElement" 1`]
99
`;
1010

1111
exports[`AppRegistry getApplication returns "element" and "getStyleElement" 2`] = `
12-
"<style id=\\"react-native-stylesheet\\">@media all{
12+
"<style id=\\"react-native-stylesheet\\">@media all {
13+
[stylesheet-group=\\"0\\"]{}
14+
:focus:not([data-rn-focusvisible-x92cna]){outline: none;}
1315
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0);}
1416
body{margin:0;}
1517
button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;}

Diff for: packages/react-native-web/src/exports/StyleSheet/StyleSheetManager.js

+25-6
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
* @noflow
88
*/
99

10+
import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment';
1011
import createAtomicRules from './createAtomicRules';
1112
import hash from '../../vendor/hash';
1213
import initialRules from './initialRules';
13-
import WebStyleSheet from './WebStyleSheet';
14+
import createOrderedCSSStyleSheet from './createOrderedCSSStyleSheet';
15+
import modality from './modality';
1416

1517
const emptyObject = {};
1618
const STYLE_ELEMENT_ID = 'react-native-stylesheet';
@@ -20,6 +22,22 @@ const createClassName = (prop, value) => {
2022
return process.env.NODE_ENV !== 'production' ? `rn-${prop}-${hashed}` : `rn-${hashed}`;
2123
};
2224

25+
const createCSSStyleSheet = () => {
26+
const id = STYLE_ELEMENT_ID;
27+
28+
let element;
29+
element = document.getElementById(id);
30+
if (!element) {
31+
element = document.createElement('style');
32+
element.setAttribute('id', id);
33+
const head = document.head;
34+
if (head) {
35+
head.insertBefore(element, head.firstChild);
36+
}
37+
}
38+
return element.sheet;
39+
};
40+
2341
const normalizeValue = value => (typeof value === 'object' ? JSON.stringify(value) : value);
2442

2543
export default class StyleSheetManager {
@@ -29,9 +47,10 @@ export default class StyleSheetManager {
2947
};
3048

3149
constructor() {
32-
this._sheet = new WebStyleSheet(STYLE_ELEMENT_ID);
50+
this._sheet = createOrderedCSSStyleSheet(canUseDOM ? createCSSStyleSheet() : null);
51+
modality(rule => this._sheet.insert(rule, 0));
3352
initialRules.forEach(rule => {
34-
this._sheet.insertRuleOnce(rule);
53+
this._sheet.insert(rule, 0);
3554
});
3655
}
3756

@@ -47,11 +66,11 @@ export default class StyleSheetManager {
4766
}
4867

4968
getStyleSheet() {
50-
const { cssText } = this._sheet;
69+
const textContent = this._sheet.getTextContent();
5170

5271
return {
5372
id: STYLE_ELEMENT_ID,
54-
textContent: cssText
73+
textContent
5574
};
5675
}
5776

@@ -63,7 +82,7 @@ export default class StyleSheetManager {
6382
this._addToCache(className, prop, val);
6483
const rules = createAtomicRules(`.${className}`, prop, value);
6584
rules.forEach(rule => {
66-
this._sheet.insertRuleOnce(rule);
85+
this._sheet.insert(rule, 1);
6786
});
6887
}
6988
return className;

Diff for: packages/react-native-web/src/exports/StyleSheet/WebStyleSheet.js

-70
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`createOrderedCSSStyleSheet #insert deduplication for same group 1`] = `""`;
4+
5+
exports[`createOrderedCSSStyleSheet #insert deduplication for same group 2`] = `
6+
"@media all {
7+
[stylesheet-group=\\"0\\"]{}
8+
.one {}
9+
}"
10+
`;
11+
12+
exports[`createOrderedCSSStyleSheet #insert deduplication for same group 3`] = `
13+
"@media all {
14+
[stylesheet-group=\\"0\\"]{}
15+
.one {}
16+
}"
17+
`;
18+
19+
exports[`createOrderedCSSStyleSheet #insert insertion order for different groups 1`] = `
20+
"@media all {
21+
[stylesheet-group=\\"1\\"]{}
22+
.one {}
23+
}
24+
@media all {
25+
[stylesheet-group=\\"2.2\\"]{}
26+
.two {}
27+
}
28+
@media all {
29+
[stylesheet-group=\\"3\\"]{}
30+
.three {}
31+
}
32+
@media all {
33+
[stylesheet-group=\\"4\\"]{}
34+
.four-1 {}
35+
.four-2 {}
36+
}
37+
@media all {
38+
[stylesheet-group=\\"9.9\\"]{}
39+
.nine-1 {}
40+
.nine-2 {}
41+
}"
42+
`;
43+
44+
exports[`createOrderedCSSStyleSheet #insert insertion order for same group 1`] = `""`;
45+
46+
exports[`createOrderedCSSStyleSheet #insert insertion order for same group 2`] = `
47+
"@media all {
48+
[stylesheet-group=\\"0\\"]{}
49+
.one {}
50+
}"
51+
`;
52+
53+
exports[`createOrderedCSSStyleSheet #insert insertion order for same group 3`] = `
54+
"@media all {
55+
[stylesheet-group=\\"0\\"]{}
56+
.one {}
57+
.two {}
58+
}"
59+
`;
60+
61+
exports[`createOrderedCSSStyleSheet #insert insertion order for same group 4`] = `
62+
"@media all {
63+
[stylesheet-group=\\"0\\"]{}
64+
.one {}
65+
.two {}
66+
.three {}
67+
}"
68+
`;
69+
70+
exports[`createOrderedCSSStyleSheet client-side hydration from SSR CSS 1`] = `
71+
"@media all {
72+
[stylesheet-group=\\"1\\"] {}
73+
.one {width: 10px;}
74+
}
75+
@media all {
76+
[stylesheet-group=\\"2\\"] {}
77+
.two-1 {height: 20px;}
78+
.two-2 {color: red;}
79+
@keyframes anim {
80+
0% {opacity: 1;}
81+
}
82+
}"
83+
`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/* eslint-env jasmine, jest */
2+
3+
'use strict';
4+
5+
import createOrderedCSSStyleSheet from '../createOrderedCSSStyleSheet';
6+
7+
const insertStyleElement = () => {
8+
const element = document.createElement('style');
9+
const head = document.head;
10+
head.insertBefore(element, head.firstChild);
11+
return element;
12+
};
13+
14+
const removeStyleElement = element => {
15+
document.head.removeChild(element);
16+
};
17+
18+
describe('createOrderedCSSStyleSheet', () => {
19+
describe('#insert', () => {
20+
test('insertion order for same group', () => {
21+
const sheet = createOrderedCSSStyleSheet();
22+
23+
expect(sheet.getTextContent()).toMatchSnapshot();
24+
25+
sheet.insert('.one {}', 0);
26+
expect(sheet.getTextContent()).toMatchSnapshot();
27+
28+
sheet.insert('.two {}', 0);
29+
expect(sheet.getTextContent()).toMatchSnapshot();
30+
31+
sheet.insert('.three {}', 0);
32+
expect(sheet.getTextContent()).toMatchSnapshot();
33+
});
34+
35+
test('deduplication for same group', () => {
36+
const sheet = createOrderedCSSStyleSheet();
37+
38+
expect(sheet.getTextContent()).toMatchSnapshot();
39+
40+
sheet.insert('.one {}', 0);
41+
expect(sheet.getTextContent()).toMatchSnapshot();
42+
43+
sheet.insert('.one {}', 0);
44+
expect(sheet.getTextContent()).toMatchSnapshot();
45+
});
46+
47+
test('insertion order for different groups', () => {
48+
const sheet = createOrderedCSSStyleSheet();
49+
50+
sheet.insert('.nine-1 {}', 9.9);
51+
sheet.insert('.nine-2 {}', 9.9);
52+
sheet.insert('.three {}', 3);
53+
sheet.insert('.one {}', 1);
54+
sheet.insert('.two {}', 2.2);
55+
sheet.insert('.four-1 {}', 4);
56+
sheet.insert('.four-2 {}', 4);
57+
58+
expect(sheet.getTextContent()).toMatchSnapshot();
59+
});
60+
});
61+
62+
describe('client-side hydration', () => {
63+
let element;
64+
65+
beforeEach(() => {
66+
if (element != null) {
67+
removeStyleElement(element);
68+
}
69+
element = insertStyleElement();
70+
});
71+
72+
test('from SSR CSS', () => {
73+
// Setup SSR CSS
74+
const serverSheet = createOrderedCSSStyleSheet();
75+
serverSheet.insert('.one { width: 10px; }', 1);
76+
serverSheet.insert('.two-1 { height: 20px; }', 2);
77+
serverSheet.insert('.two-2 { color: red; }', 2);
78+
serverSheet.insert('@keyframes anim { 0% { opacity: 1; } }', 2);
79+
const textContent = serverSheet.getTextContent();
80+
81+
// Add SSR CSS to client style sheet
82+
element.appendChild(document.createTextNode(textContent));
83+
const clientSheet = createOrderedCSSStyleSheet(element.sheet);
84+
expect(clientSheet.getTextContent()).toMatchSnapshot();
85+
});
86+
});
87+
});

0 commit comments

Comments
 (0)