diff --git a/packages/react-native-web/src/exports/AppRegistry/__tests__/__snapshots__/index-test.js.snap b/packages/react-native-web/src/exports/AppRegistry/__tests__/__snapshots__/index-test.js.snap
index c1c2ab1f..b9015c7d 100644
--- a/packages/react-native-web/src/exports/AppRegistry/__tests__/__snapshots__/index-test.js.snap
+++ b/packages/react-native-web/src/exports/AppRegistry/__tests__/__snapshots__/index-test.js.snap
@@ -1,30 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AppRegistry getApplication "getStyleElement" produces styles that are a function of rendering "element" 1`] = `
-"@media all {
-[stylesheet-group=\\"0\\"]{}
+"[stylesheet-group=\\"0\\"]{}
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;}
-}
-@media all {
[stylesheet-group=\\"1\\"]{}
.css-view-1dbjc4n{-ms-flex-align:stretch;-ms-flex-direction:column;-ms-flex-negative:0;-ms-flex-preferred-size:auto;-webkit-align-items:stretch;-webkit-box-align:stretch;-webkit-box-direction:normal;-webkit-box-orient:vertical;-webkit-flex-basis:auto;-webkit-flex-direction:column;-webkit-flex-shrink:0;align-items:stretch;border:0 solid black;box-sizing:border-box;display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;flex-basis:auto;flex-direction:column;flex-shrink:0;margin-bottom:0px;margin-left:0px;margin-right:0px;margin-top:0px;min-height:0px;min-width:0px;padding-bottom:0px;padding-left:0px;padding-right:0px;padding-top:0px;position:relative;z-index:0;}
-}
-@media all {
[stylesheet-group=\\"2\\"]{}
.r-flex-13awgt0{-ms-flex-negative:1;-ms-flex-positive:1;-ms-flex-preferred-size:0%;-webkit-box-flex:1;-webkit-flex-basis:0%;-webkit-flex-grow:1;-webkit-flex-shrink:1;flex-basis:0%;flex-grow:1;flex-shrink:1;}
-}
-@media all {
[stylesheet-group=\\"2.2\\"]{}
.r-pointerEvents-12vffkv>*{pointer-events:auto;}
-.r-pointerEvents-12vffkv{pointer-events:none!important;}
-}"
+.r-pointerEvents-12vffkv{pointer-events:none!important;}"
`;
exports[`AppRegistry getApplication returns "element" and "getStyleElement" 1`] = `
@@ -36,15 +26,11 @@ exports[`AppRegistry getApplication returns "element" and "getStyleElement" 1`]
`;
exports[`AppRegistry getApplication returns "element" and "getStyleElement" 2`] = `
-""
+:focus:not([data-focusvisible-polyfill]){outline: none;}"
`;
diff --git a/packages/react-native-web/src/exports/StyleSheet/__tests__/__snapshots__/createOrderedCSSStyleSheet-test.js.snap b/packages/react-native-web/src/exports/StyleSheet/__tests__/__snapshots__/createOrderedCSSStyleSheet-test.js.snap
index 15ab3746..5b07463d 100644
--- a/packages/react-native-web/src/exports/StyleSheet/__tests__/__snapshots__/createOrderedCSSStyleSheet-test.js.snap
+++ b/packages/react-native-web/src/exports/StyleSheet/__tests__/__snapshots__/createOrderedCSSStyleSheet-test.js.snap
@@ -3,81 +3,57 @@
exports[`createOrderedCSSStyleSheet #insert deduplication for same group 1`] = `""`;
exports[`createOrderedCSSStyleSheet #insert deduplication for same group 2`] = `
-"@media all {
-[stylesheet-group=\\"0\\"]{}
-.one {}
-}"
+"[stylesheet-group=\\"0\\"]{}
+.one {}"
`;
exports[`createOrderedCSSStyleSheet #insert deduplication for same group 3`] = `
-"@media all {
-[stylesheet-group=\\"0\\"]{}
-.one {}
-}"
+"[stylesheet-group=\\"0\\"]{}
+.one {}"
`;
exports[`createOrderedCSSStyleSheet #insert insertion order for different groups 1`] = `
-"@media all {
-[stylesheet-group=\\"1\\"]{}
+"[stylesheet-group=\\"1\\"]{}
.one {}
-}
-@media all {
[stylesheet-group=\\"2.2\\"]{}
.two {}
-}
-@media all {
[stylesheet-group=\\"3\\"]{}
.three {}
-}
-@media all {
[stylesheet-group=\\"4\\"]{}
.four-1 {}
.four-2 {}
-}
-@media all {
[stylesheet-group=\\"9.9\\"]{}
.nine-1 {}
-.nine-2 {}
-}"
+.nine-2 {}"
`;
exports[`createOrderedCSSStyleSheet #insert insertion order for same group 1`] = `""`;
exports[`createOrderedCSSStyleSheet #insert insertion order for same group 2`] = `
-"@media all {
-[stylesheet-group=\\"0\\"]{}
-.one {}
-}"
+"[stylesheet-group=\\"0\\"]{}
+.one {}"
`;
exports[`createOrderedCSSStyleSheet #insert insertion order for same group 3`] = `
-"@media all {
-[stylesheet-group=\\"0\\"]{}
+"[stylesheet-group=\\"0\\"]{}
.one {}
-.two {}
-}"
+.two {}"
`;
exports[`createOrderedCSSStyleSheet #insert insertion order for same group 4`] = `
-"@media all {
-[stylesheet-group=\\"0\\"]{}
+"[stylesheet-group=\\"0\\"]{}
.one {}
.two {}
-.three {}
-}"
+.three {}"
`;
exports[`createOrderedCSSStyleSheet client-side hydration from SSR CSS 1`] = `
-"@media all {
-[stylesheet-group=\\"1\\"] {}
+"[stylesheet-group=\\"1\\"] {}
.one {width: 10px;}
-}
-@media all {
[stylesheet-group=\\"2\\"] {}
.two-1 {height: 20px;}
.two-2 {color: red;}
@keyframes anim {
0% {opacity: 1;}
-}
}"
`;
diff --git a/packages/react-native-web/src/exports/StyleSheet/createOrderedCSSStyleSheet.js b/packages/react-native-web/src/exports/StyleSheet/createOrderedCSSStyleSheet.js
index b60c8553..6b08b6a0 100644
--- a/packages/react-native-web/src/exports/StyleSheet/createOrderedCSSStyleSheet.js
+++ b/packages/react-native-web/src/exports/StyleSheet/createOrderedCSSStyleSheet.js
@@ -7,7 +7,7 @@
* @flow strict-local
*/
-type Groups = { [key: number]: Array };
+type Groups = { [key: number]: { start: ?number, rules: Array } };
type Selectors = { [key: string]: boolean };
const slice = Array.prototype.slice;
@@ -15,17 +15,17 @@ const slice = Array.prototype.slice;
/**
* Order-based insertion of CSS.
*
- * Each rule can be inserted (appended) into a numerically defined group.
+ * Each rule is associated with a numerically defined group.
* Groups are ordered within the style sheet according to their number, with the
* lowest first.
*
- * Groups are implemented using Media Query blocks. CSSMediaRule implements the
- * CSSGroupingRule, which includes 'insertRule', allowing groups to be treated as
- * a sub-sheet.
+ * Groups are implemented using marker rules. The selector of the first rule of
+ * each group is used only to encode the group number for hydration. An
+ * alternative implementation could rely on CSSMediaRule, allowing groups to be
+ * treated as a sub-sheet, but the Edge implementation of CSSMediaRule is
+ * broken.
* https://developer.mozilla.org/en-US/docs/Web/API/CSSMediaRule
- *
- * The selector of the first rule of each group is used only to encode the group
- * number for hydration.
+ * https://gist.github.com/necolas/aa0c37846ad6bd3b05b727b959e82674
*/
export default function createOrderedCSSStyleSheet(sheet: ?CSSStyleSheet) {
const groups: Groups = {};
@@ -35,59 +35,78 @@ export default function createOrderedCSSStyleSheet(sheet: ?CSSStyleSheet) {
* Hydrate approximate record from any existing rules in the sheet.
*/
if (sheet != null) {
- slice.call(sheet.cssRules).forEach(mediaRule => {
- if (mediaRule.media == null) {
- throw new Error(
- 'OrderedCSSStyleSheet: hydrating invalid stylesheet. Expected only @media rules.'
- );
- }
-
- // Create group number
- const group = decodeGroupRule(mediaRule);
- groups[group] = [];
-
+ let group;
+ slice.call(sheet.cssRules).forEach((cssRule, i) => {
+ const cssText = cssRule.cssText;
// Create record of existing selectors and rules
- slice.call(mediaRule.cssRules).forEach(rule => {
- const selectorText = getSelectorText(rule.cssText);
+ if (cssText.indexOf('stylesheet-group') > -1) {
+ group = decodeGroupRule(cssRule);
+ groups[group] = { start: i, rules: [cssText] };
+ } else {
+ const selectorText = getSelectorText(cssText);
if (selectorText != null) {
selectors[selectorText] = true;
- groups[group].push(rule.cssText);
+ groups[group].rules.push(cssText);
}
- });
+ }
});
}
+ function sheetInsert(sheet, group, text) {
+ const orderedGroups = getOrderedGroups(groups);
+ const groupIndex = orderedGroups.indexOf(group);
+ const nextGroupIndex = groupIndex + 1;
+ const nextGroup = orderedGroups[nextGroupIndex];
+ // Insert rule before the next group, or at the end of the stylesheet
+ const position =
+ nextGroup != null && groups[nextGroup].start != null
+ ? groups[nextGroup].start
+ : sheet.cssRules.length;
+ const isInserted = insertRuleAt(sheet, text, position);
+
+ if (isInserted) {
+ // Set the starting index of the new group
+ if (groups[group].start == null) {
+ groups[group].start = position;
+ }
+ // Increment the starting index of all subsequent groups
+ for (let i = nextGroupIndex; i < orderedGroups.length; i += 1) {
+ const groupNumber = orderedGroups[i];
+ const previousStart = groups[groupNumber].start;
+ groups[groupNumber].start = previousStart + 1;
+ }
+ }
+
+ return isInserted;
+ }
+
const OrderedCSSStyleSheet = {
/**
* The textContent of the style sheet.
- * Each group's rules are wrapped in a media query.
*/
getTextContent(): string {
return getOrderedGroups(groups)
.map(group => {
- const rules = groups[group];
- const str = rules.join('\n');
- return createMediaRule(str);
+ const rules = groups[group].rules;
+ return rules.join('\n');
})
.join('\n');
},
/**
- * Insert a rule into a media query in the style sheet
+ * Insert a rule into the style sheet
*/
- insert(cssText: string, group: number) {
+ insert(cssText: string, groupValue: number) {
+ const group = Number(groupValue);
+
// Create a new group.
if (groups[group] == null) {
const markerRule = encodeGroupRule(group);
-
// Create the internal record.
- groups[group] = [];
- groups[group].push(markerRule);
-
- // Create CSSOM CSSMediaRule.
+ groups[group] = { start: null, rules: [markerRule] };
+ // Update CSSOM.
if (sheet != null) {
- const groupIndex = getOrderedGroups(groups).indexOf(group);
- insertRuleAt(sheet, createMediaRule(markerRule), groupIndex);
+ sheetInsert(sheet, group, markerRule);
}
}
@@ -98,14 +117,14 @@ export default function createOrderedCSSStyleSheet(sheet: ?CSSStyleSheet) {
if (selectorText != null && selectors[selectorText] == null) {
// Update the internal records.
selectors[selectorText] = true;
- groups[group].push(cssText);
- // Update CSSOM CSSMediaRule.
+ groups[group].rules.push(cssText);
+ // Update CSSOM.
if (sheet != null) {
- const groupIndex = getOrderedGroups(groups).indexOf(group);
- const root = sheet.cssRules[groupIndex];
- if (root != null) {
- // $FlowFixMe: Flow is missing CSSOM types
- insertRuleAt(root, cssText, root.cssRules.length);
+ const isInserted = sheetInsert(sheet, group, cssText);
+ if (!isInserted) {
+ // Revert internal record change if a rule was rejected (e.g.,
+ // unrecognized pseudo-selector)
+ groups[group].rules.pop();
}
}
}
@@ -119,16 +138,12 @@ export default function createOrderedCSSStyleSheet(sheet: ?CSSStyleSheet) {
* Helper functions
*/
-function createMediaRule(content) {
- return `@media all {\n${content}\n}`;
-}
-
function encodeGroupRule(group) {
return `[stylesheet-group="${group}"]{}`;
}
-function decodeGroupRule(mediaRule) {
- return mediaRule.cssRules[0].selectorText.split('"')[1];
+function decodeGroupRule(cssRule) {
+ return Number(cssRule.selectorText.split('"')[1]);
}
function getOrderedGroups(obj: { [key: number]: any }) {
@@ -143,12 +158,14 @@ function getSelectorText(cssText) {
return selector !== '' ? selector.replace(pattern, '$1') : null;
}
-function insertRuleAt(root, cssText: string, position: number) {
+function insertRuleAt(root, cssText: string, position: number): boolean {
try {
// $FlowFixMe: Flow is missing CSSOM types needed to type 'root'.
root.insertRule(cssText, position);
+ return true;
} catch (e) {
// JSDOM doesn't support `CSSSMediaRule#insertRule`.
// Also ignore errors that occur from attempting to insert vendor-prefixed selectors.
+ return false;
}
}