mirror of
https://github.com/zoriya/react-native-web.git
synced 2026-05-23 06:48:35 +00:00
[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 #1136
This commit is contained in:
@@ -34,6 +34,7 @@
|
||||
"window": false,
|
||||
// Flow global types,
|
||||
"$Enum": false,
|
||||
"CSSStyleSheet": false,
|
||||
"HTMLInputElement": false,
|
||||
"ReactClass": false,
|
||||
"ReactComponent": false,
|
||||
|
||||
+3
-1
@@ -9,7 +9,9 @@ exports[`AppRegistry getApplication returns "element" and "getStyleElement" 1`]
|
||||
`;
|
||||
|
||||
exports[`AppRegistry getApplication returns "element" and "getStyleElement" 2`] = `
|
||||
"<style id=\\"react-native-stylesheet\\">@media all{
|
||||
"<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;}
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
* @noflow
|
||||
*/
|
||||
|
||||
import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment';
|
||||
import createAtomicRules from './createAtomicRules';
|
||||
import hash from '../../vendor/hash';
|
||||
import initialRules from './initialRules';
|
||||
import WebStyleSheet from './WebStyleSheet';
|
||||
import createOrderedCSSStyleSheet from './createOrderedCSSStyleSheet';
|
||||
import modality from './modality';
|
||||
|
||||
const emptyObject = {};
|
||||
const STYLE_ELEMENT_ID = 'react-native-stylesheet';
|
||||
@@ -20,6 +22,22 @@ const createClassName = (prop, value) => {
|
||||
return process.env.NODE_ENV !== 'production' ? `rn-${prop}-${hashed}` : `rn-${hashed}`;
|
||||
};
|
||||
|
||||
const createCSSStyleSheet = () => {
|
||||
const id = STYLE_ELEMENT_ID;
|
||||
|
||||
let element;
|
||||
element = document.getElementById(id);
|
||||
if (!element) {
|
||||
element = document.createElement('style');
|
||||
element.setAttribute('id', id);
|
||||
const head = document.head;
|
||||
if (head) {
|
||||
head.insertBefore(element, head.firstChild);
|
||||
}
|
||||
}
|
||||
return element.sheet;
|
||||
};
|
||||
|
||||
const normalizeValue = value => (typeof value === 'object' ? JSON.stringify(value) : value);
|
||||
|
||||
export default class StyleSheetManager {
|
||||
@@ -29,9 +47,10 @@ export default class StyleSheetManager {
|
||||
};
|
||||
|
||||
constructor() {
|
||||
this._sheet = new WebStyleSheet(STYLE_ELEMENT_ID);
|
||||
this._sheet = createOrderedCSSStyleSheet(canUseDOM ? createCSSStyleSheet() : null);
|
||||
modality(rule => this._sheet.insert(rule, 0));
|
||||
initialRules.forEach(rule => {
|
||||
this._sheet.insertRuleOnce(rule);
|
||||
this._sheet.insert(rule, 0);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -47,11 +66,11 @@ export default class StyleSheetManager {
|
||||
}
|
||||
|
||||
getStyleSheet() {
|
||||
const { cssText } = this._sheet;
|
||||
const textContent = this._sheet.getTextContent();
|
||||
|
||||
return {
|
||||
id: STYLE_ELEMENT_ID,
|
||||
textContent: cssText
|
||||
textContent
|
||||
};
|
||||
}
|
||||
|
||||
@@ -63,7 +82,7 @@ export default class StyleSheetManager {
|
||||
this._addToCache(className, prop, val);
|
||||
const rules = createAtomicRules(`.${className}`, prop, value);
|
||||
rules.forEach(rule => {
|
||||
this._sheet.insertRuleOnce(rule);
|
||||
this._sheet.insert(rule, 1);
|
||||
});
|
||||
}
|
||||
return className;
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2016-present, Nicolas Gallagher.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment';
|
||||
import modality from './modality';
|
||||
|
||||
export default class WebStyleSheet {
|
||||
_cssRules = [];
|
||||
_sheet = null;
|
||||
_textContent = '';
|
||||
|
||||
constructor(id: string) {
|
||||
let domStyleElement;
|
||||
|
||||
// on the client we check for an existing style sheet before injecting
|
||||
if (canUseDOM) {
|
||||
domStyleElement = document.getElementById(id);
|
||||
if (!domStyleElement) {
|
||||
const html = `<style id="${id}"></style>`;
|
||||
if (document.head) {
|
||||
document.head.insertAdjacentHTML('afterbegin', html);
|
||||
domStyleElement = document.getElementById(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (domStyleElement) {
|
||||
modality(domStyleElement);
|
||||
// $FlowFixMe
|
||||
this._sheet = domStyleElement.sheet;
|
||||
this._textContent = domStyleElement.textContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
containsRule(rule: string): boolean {
|
||||
return this._cssRules.indexOf(rule) > -1;
|
||||
}
|
||||
|
||||
get cssText(): string {
|
||||
return this._cssRules.join('\n');
|
||||
}
|
||||
|
||||
insertRuleOnce(rule: string, position: ?number) {
|
||||
// Reduce chance of duplicate rules
|
||||
if (!this.containsRule(rule)) {
|
||||
this._cssRules.push(rule);
|
||||
|
||||
// Check whether a rule was part of any prerendered styles (textContent
|
||||
// doesn't include styles injected via 'insertRule')
|
||||
if (this._textContent.indexOf(rule) === -1 && this._sheet) {
|
||||
const pos = position || this._sheet.cssRules.length;
|
||||
try {
|
||||
this._sheet.insertRule(rule, pos);
|
||||
} catch (e) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.warn(
|
||||
`Failed to inject CSS: "${rule}". The browser may have rejecting unrecognized vendor prefixes. (This should have no user-facing impact.)`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`createOrderedCSSStyleSheet #insert deduplication for same group 1`] = `""`;
|
||||
|
||||
exports[`createOrderedCSSStyleSheet #insert deduplication for same group 2`] = `
|
||||
"@media all {
|
||||
[stylesheet-group=\\"0\\"]{}
|
||||
.one {}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`createOrderedCSSStyleSheet #insert deduplication for same group 3`] = `
|
||||
"@media all {
|
||||
[stylesheet-group=\\"0\\"]{}
|
||||
.one {}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`createOrderedCSSStyleSheet #insert insertion order for different groups 1`] = `
|
||||
"@media all {
|
||||
[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 {}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`createOrderedCSSStyleSheet #insert insertion order for same group 1`] = `""`;
|
||||
|
||||
exports[`createOrderedCSSStyleSheet #insert insertion order for same group 2`] = `
|
||||
"@media all {
|
||||
[stylesheet-group=\\"0\\"]{}
|
||||
.one {}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`createOrderedCSSStyleSheet #insert insertion order for same group 3`] = `
|
||||
"@media all {
|
||||
[stylesheet-group=\\"0\\"]{}
|
||||
.one {}
|
||||
.two {}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`createOrderedCSSStyleSheet #insert insertion order for same group 4`] = `
|
||||
"@media all {
|
||||
[stylesheet-group=\\"0\\"]{}
|
||||
.one {}
|
||||
.two {}
|
||||
.three {}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`createOrderedCSSStyleSheet client-side hydration from SSR CSS 1`] = `
|
||||
"@media all {
|
||||
[stylesheet-group=\\"1\\"] {}
|
||||
.one {width: 10px;}
|
||||
}
|
||||
@media all {
|
||||
[stylesheet-group=\\"2\\"] {}
|
||||
.two-1 {height: 20px;}
|
||||
.two-2 {color: red;}
|
||||
@keyframes anim {
|
||||
0% {opacity: 1;}
|
||||
}
|
||||
}"
|
||||
`;
|
||||
Vendored
+87
@@ -0,0 +1,87 @@
|
||||
/* eslint-env jasmine, jest */
|
||||
|
||||
'use strict';
|
||||
|
||||
import createOrderedCSSStyleSheet from '../createOrderedCSSStyleSheet';
|
||||
|
||||
const insertStyleElement = () => {
|
||||
const element = document.createElement('style');
|
||||
const head = document.head;
|
||||
head.insertBefore(element, head.firstChild);
|
||||
return element;
|
||||
};
|
||||
|
||||
const removeStyleElement = element => {
|
||||
document.head.removeChild(element);
|
||||
};
|
||||
|
||||
describe('createOrderedCSSStyleSheet', () => {
|
||||
describe('#insert', () => {
|
||||
test('insertion order for same group', () => {
|
||||
const sheet = createOrderedCSSStyleSheet();
|
||||
|
||||
expect(sheet.getTextContent()).toMatchSnapshot();
|
||||
|
||||
sheet.insert('.one {}', 0);
|
||||
expect(sheet.getTextContent()).toMatchSnapshot();
|
||||
|
||||
sheet.insert('.two {}', 0);
|
||||
expect(sheet.getTextContent()).toMatchSnapshot();
|
||||
|
||||
sheet.insert('.three {}', 0);
|
||||
expect(sheet.getTextContent()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('deduplication for same group', () => {
|
||||
const sheet = createOrderedCSSStyleSheet();
|
||||
|
||||
expect(sheet.getTextContent()).toMatchSnapshot();
|
||||
|
||||
sheet.insert('.one {}', 0);
|
||||
expect(sheet.getTextContent()).toMatchSnapshot();
|
||||
|
||||
sheet.insert('.one {}', 0);
|
||||
expect(sheet.getTextContent()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('insertion order for different groups', () => {
|
||||
const sheet = createOrderedCSSStyleSheet();
|
||||
|
||||
sheet.insert('.nine-1 {}', 9.9);
|
||||
sheet.insert('.nine-2 {}', 9.9);
|
||||
sheet.insert('.three {}', 3);
|
||||
sheet.insert('.one {}', 1);
|
||||
sheet.insert('.two {}', 2.2);
|
||||
sheet.insert('.four-1 {}', 4);
|
||||
sheet.insert('.four-2 {}', 4);
|
||||
|
||||
expect(sheet.getTextContent()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('client-side hydration', () => {
|
||||
let element;
|
||||
|
||||
beforeEach(() => {
|
||||
if (element != null) {
|
||||
removeStyleElement(element);
|
||||
}
|
||||
element = insertStyleElement();
|
||||
});
|
||||
|
||||
test('from SSR CSS', () => {
|
||||
// Setup SSR CSS
|
||||
const serverSheet = createOrderedCSSStyleSheet();
|
||||
serverSheet.insert('.one { width: 10px; }', 1);
|
||||
serverSheet.insert('.two-1 { height: 20px; }', 2);
|
||||
serverSheet.insert('.two-2 { color: red; }', 2);
|
||||
serverSheet.insert('@keyframes anim { 0% { opacity: 1; } }', 2);
|
||||
const textContent = serverSheet.getTextContent();
|
||||
|
||||
// Add SSR CSS to client style sheet
|
||||
element.appendChild(document.createTextNode(textContent));
|
||||
const clientSheet = createOrderedCSSStyleSheet(element.sheet);
|
||||
expect(clientSheet.getTextContent()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @flow strict-local
|
||||
*/
|
||||
|
||||
type Groups = { [key: number]: Array<string> };
|
||||
type Selectors = { [key: string]: boolean };
|
||||
|
||||
const slice = Array.prototype.slice;
|
||||
|
||||
/**
|
||||
* Order-based insertion of CSS.
|
||||
*
|
||||
* Each rule can be inserted (appended) into 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.
|
||||
* 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.
|
||||
*/
|
||||
export default function createOrderedCSSStyleSheet(sheet: ?CSSStyleSheet) {
|
||||
const groups: Groups = {};
|
||||
const selectors: Selectors = {};
|
||||
|
||||
/**
|
||||
* 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] = [];
|
||||
|
||||
// Create record of existing selectors and rules
|
||||
slice.call(mediaRule.cssRules).forEach(rule => {
|
||||
const selectorText = getSelectorText(rule.cssText);
|
||||
if (selectorText != null) {
|
||||
selectors[selectorText] = true;
|
||||
groups[group].push(rule.cssText);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
})
|
||||
.join('\n');
|
||||
},
|
||||
|
||||
/**
|
||||
* Insert a rule into a media query in the style sheet
|
||||
*/
|
||||
insert(cssText: string, group: number) {
|
||||
// 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.
|
||||
if (sheet != null) {
|
||||
const groupIndex = getOrderedGroups(groups).indexOf(group);
|
||||
insertRuleAt(sheet, createMediaRule(markerRule), groupIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// selectorText is more reliable than cssText for insertion checks. The
|
||||
// browser excludes vendor-prefixed properties and rewrites certain values
|
||||
// making cssText more likely to be different from what was inserted.
|
||||
const selectorText = getSelectorText(cssText);
|
||||
if (selectorText != null && selectors[selectorText] == null) {
|
||||
// Update the internal records.
|
||||
selectors[selectorText] = true;
|
||||
groups[group].push(cssText);
|
||||
// Update CSSOM CSSMediaRule.
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return OrderedCSSStyleSheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 getOrderedGroups(obj: { [key: number]: any }) {
|
||||
return Object.keys(obj)
|
||||
.sort()
|
||||
.map(k => Number(k));
|
||||
}
|
||||
|
||||
const pattern = /\s*([,])\s*/g;
|
||||
function getSelectorText(cssText) {
|
||||
const selector = cssText.split('{')[0].trim();
|
||||
return selector !== '' ? selector.replace(pattern, '$1') : null;
|
||||
}
|
||||
|
||||
function insertRuleAt(root, cssText: string, position: number) {
|
||||
try {
|
||||
// $FlowFixMe: Flow is missing CSSOM types needed to type 'root'.
|
||||
root.insertRule(cssText, position);
|
||||
} catch (e) {
|
||||
// JSDOM doesn't support `CSSSMediaRule#insertRule`.
|
||||
// Also ignore errors that occur from attempting to insert vendor-prefixed selectors.
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,6 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
// Prevent browsers throwing parse errors, e.g., on vendor-prefixed pseudo-elements
|
||||
const safeRule = rule => `@media all{\n${rule}\n}`;
|
||||
|
||||
const resets = [
|
||||
// minimal top-level reset
|
||||
'html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0);}',
|
||||
@@ -21,6 +18,4 @@ const resets = [
|
||||
'input::-webkit-search-results-button,input::-webkit-search-results-decoration{display:none;}'
|
||||
];
|
||||
|
||||
const reset = [safeRule(resets.join('\n'))];
|
||||
|
||||
export default reset;
|
||||
export default resets;
|
||||
|
||||
@@ -28,7 +28,9 @@ const focusVisibleAttributeName =
|
||||
|
||||
const rule = `:focus:not([${focusVisibleAttributeName}]){outline: none;}`;
|
||||
|
||||
const modality = styleElement => {
|
||||
const modality = insertRule => {
|
||||
insertRule(rule);
|
||||
|
||||
if (!canUseDOM) {
|
||||
return;
|
||||
}
|
||||
@@ -264,8 +266,6 @@ const modality = styleElement => {
|
||||
removeInitialPointerMoveListeners();
|
||||
}
|
||||
|
||||
styleElement.sheet.insertRule(rule, 0);
|
||||
|
||||
document.addEventListener('keydown', onKeyDown, true);
|
||||
document.addEventListener('mousedown', onPointerDown, true);
|
||||
document.addEventListener('pointerdown', onPointerDown, true);
|
||||
|
||||
Reference in New Issue
Block a user