[change] Refactor style resolver

Minor refactor of the style resolver to convert it to a factory function.
This commit is contained in:
Nicolas Gallagher
2019-03-03 15:57:49 -08:00
parent f048d848a1
commit 0e302a50d2
4 changed files with 315 additions and 24 deletions
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`StyleSheet/ReactNativeStyleResolver resolve resolves inline-style pointerEvents to classname 1`] = `
exports[`StyleSheet/createStyleResolver resolve resolves inline-style pointerEvents to classname 1`] = `
Object {
"classList": Array [
"rn-pointerEvents-12vffkv",
@@ -9,7 +9,7 @@ Object {
}
`;
exports[`StyleSheet/ReactNativeStyleResolver resolve with register before RTL, resolves to correct className 1`] = `
exports[`StyleSheet/createStyleResolver resolve with register before RTL, resolves to correct className 1`] = `
Object {
"classList": Array [
"rn-marginRight-zso239",
@@ -20,7 +20,7 @@ Object {
}
`;
exports[`StyleSheet/ReactNativeStyleResolver resolve with register before RTL, resolves to correct className 2`] = `
exports[`StyleSheet/createStyleResolver resolve with register before RTL, resolves to correct className 2`] = `
Object {
"classList": Array [
"rn-left-2s0hu9",
@@ -31,7 +31,7 @@ Object {
}
`;
exports[`StyleSheet/ReactNativeStyleResolver resolve with register, resolves to className 1`] = `
exports[`StyleSheet/createStyleResolver resolve with register, resolves to className 1`] = `
Object {
"classList": Array [
"rn-borderColor-4a18lf",
@@ -45,7 +45,7 @@ Object {
}
`;
exports[`StyleSheet/ReactNativeStyleResolver resolve with register, resolves to className 2`] = `
exports[`StyleSheet/createStyleResolver resolve with register, resolves to className 2`] = `
Object {
"classList": Array [
"rn-borderColor-4a18lf",
@@ -59,7 +59,7 @@ Object {
}
`;
exports[`StyleSheet/ReactNativeStyleResolver resolve with register, resolves to className 3`] = `
exports[`StyleSheet/createStyleResolver resolve with register, resolves to className 3`] = `
Object {
"classList": Array [
"rn-borderColor-4a18lf",
@@ -73,7 +73,7 @@ Object {
}
`;
exports[`StyleSheet/ReactNativeStyleResolver resolve with register, resolves to mixed 1`] = `
exports[`StyleSheet/createStyleResolver resolve with register, resolves to mixed 1`] = `
Object {
"classList": Array [
"rn-left-1tsx3h3",
@@ -95,7 +95,7 @@ Object {
}
`;
exports[`StyleSheet/ReactNativeStyleResolver resolve with register, resolves to mixed 2`] = `
exports[`StyleSheet/createStyleResolver resolve with register, resolves to mixed 2`] = `
Object {
"classList": Array [
"rn-left-1tsx3h3",
@@ -117,7 +117,7 @@ Object {
}
`;
exports[`StyleSheet/ReactNativeStyleResolver resolve with register, resolves to mixed 3`] = `
exports[`StyleSheet/createStyleResolver resolve with register, resolves to mixed 3`] = `
Object {
"classList": Array [
"rn-left-1tsx3h3",
@@ -139,7 +139,7 @@ Object {
}
`;
exports[`StyleSheet/ReactNativeStyleResolver resolve without register, resolves to inline styles 1`] = `
exports[`StyleSheet/createStyleResolver resolve without register, resolves to inline styles 1`] = `
Object {
"classList": Array [],
"className": "",
@@ -160,7 +160,7 @@ Object {
}
`;
exports[`StyleSheet/ReactNativeStyleResolver resolve without register, resolves to inline styles 2`] = `
exports[`StyleSheet/createStyleResolver resolve without register, resolves to inline styles 2`] = `
Object {
"classList": Array [],
"className": "",
@@ -181,7 +181,7 @@ Object {
}
`;
exports[`StyleSheet/ReactNativeStyleResolver resolve without register, resolves to inline styles 3`] = `
exports[`StyleSheet/createStyleResolver resolve without register, resolves to inline styles 3`] = `
Object {
"classList": Array [],
"className": "",
@@ -202,7 +202,7 @@ Object {
}
`;
exports[`StyleSheet/ReactNativeStyleResolver resolveWithNode next class names have priority over current inline styles 1`] = `
exports[`StyleSheet/createStyleResolver resolveWithNode next class names have priority over current inline styles 1`] = `
Object {
"className": "rn-opacity-6dt33c",
"style": Object {
@@ -211,7 +211,7 @@ Object {
}
`;
exports[`StyleSheet/ReactNativeStyleResolver resolveWithNode next inline styles have priority over current inline styles 1`] = `
exports[`StyleSheet/createStyleResolver resolveWithNode next inline styles have priority over current inline styles 1`] = `
Object {
"className": "",
"style": Object {
@@ -222,14 +222,14 @@ Object {
}
`;
exports[`StyleSheet/ReactNativeStyleResolver resolveWithNode preserves unrelated class names 1`] = `
exports[`StyleSheet/createStyleResolver resolveWithNode preserves unrelated class names 1`] = `
Object {
"className": "unknown-class-1 unknown-class-2",
"style": Object {},
}
`;
exports[`StyleSheet/ReactNativeStyleResolver resolveWithNode preserves unrelated inline styles 1`] = `
exports[`StyleSheet/createStyleResolver resolveWithNode preserves unrelated inline styles 1`] = `
Object {
"className": "",
"style": Object {
@@ -239,7 +239,7 @@ Object {
}
`;
exports[`StyleSheet/ReactNativeStyleResolver resolveWithNode when isRTL=true & doLeftAndRightSwapInRTL=false, resolves to non-flipped inline styles 1`] = `
exports[`StyleSheet/createStyleResolver resolveWithNode when isRTL=true & doLeftAndRightSwapInRTL=false, resolves to non-flipped inline styles 1`] = `
Object {
"className": "",
"style": Object {
@@ -250,7 +250,7 @@ Object {
}
`;
exports[`StyleSheet/ReactNativeStyleResolver resolveWithNode when isRTL=true, resolves to flipped classNames 1`] = `
exports[`StyleSheet/createStyleResolver resolveWithNode when isRTL=true, resolves to flipped classNames 1`] = `
Object {
"className": "rn-left-1u10d71 rn-marginRight-zso239",
"style": Object {
@@ -260,7 +260,7 @@ Object {
}
`;
exports[`StyleSheet/ReactNativeStyleResolver resolveWithNode when isRTL=true, resolves to flipped inline styles 1`] = `
exports[`StyleSheet/createStyleResolver resolveWithNode when isRTL=true, resolves to flipped inline styles 1`] = `
Object {
"className": "",
"style": Object {
@@ -2,13 +2,13 @@
import I18nManager from '../../I18nManager';
import ReactNativePropRegistry from '../../../modules/ReactNativePropRegistry';
import ReactNativeStyleResolver from '../ReactNativeStyleResolver';
import createStyleResolver from '../createStyleResolver';
let styleResolver;
describe('StyleSheet/ReactNativeStyleResolver', () => {
describe('StyleSheet/createStyleResolver', () => {
beforeEach(() => {
styleResolver = new ReactNativeStyleResolver();
styleResolver = createStyleResolver({ insert() {} });
});
describe('resolve', () => {
@@ -0,0 +1,291 @@
/**
* 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.
*
* @noflow
*/
/**
* WARNING: changes to this file in particular can cause significant changes to
* the results of render performance benchmarks.
*/
import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment';
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 { 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 function createStyleResolver() {
let resolved, inserted, sheet, lookup;
const init = () => {
resolved = { ltr: {}, rtl: {}, rtlNoSwap: {} };
inserted = { ltr: {}, rtl: {}, rtlNoSwap: {} };
sheet = createOrderedCSSStyleSheet(createCSSStyleSheet(STYLE_ELEMENT_ID));
lookup = {
byClassName: {},
byProp: {}
};
modality(rule => sheet.insert(rule, STYLE_GROUPS.modality));
initialRules.forEach(rule => {
sheet.insert(rule, STYLE_GROUPS.reset);
});
};
init();
function addToLookup(className, prop, value) {
if (!lookup.byProp[prop]) {
lookup.byProp[prop] = {};
}
lookup.byProp[prop][value] = className;
lookup.byClassName[className] = { prop, value };
}
function getClassName(prop, value) {
const val = stringifyValueWithProperty(value, prop);
const cache = lookup.byProp;
return cache[prop] && cache[prop].hasOwnProperty(val) && cache[prop][val];
}
function _injectRegisteredStyle(id) {
const { doLeftAndRightSwapInRTL, isRTL } = I18nManager;
const dir = isRTL ? (doLeftAndRightSwapInRTL ? 'rtl' : 'rtlNoSwap') : 'ltr';
if (!inserted[dir][id]) {
const style = createCompileableStyle(i18nStyle(flattenStyle(id)));
const results = atomic(style);
Object.values(results).forEach(({ identifier, property, rules, value }) => {
addToLookup(identifier, property, value);
rules.forEach(rule => {
const group = STYLE_GROUPS.custom[property] || STYLE_GROUPS.atomic;
sheet.insert(rule, group);
});
});
inserted[dir][id] = true;
}
}
/**
* Resolves a React Native style object to DOM attributes
*/
function resolve(style) {
if (!style) {
return emptyObject;
}
// fast and cachable
if (typeof style === 'number') {
_injectRegisteredStyle(style);
const key = createCacheKey(style);
return _resolveStyle(style, key);
}
// resolve a plain RN style object
if (!Array.isArray(style)) {
return _resolveStyle(style);
}
// flatten the style array
// cache resolved props when all styles are registered
// otherwise fallback to resolving
const flatArray = flattenArray(style);
let isArrayOfNumbers = true;
let cacheKey = '';
for (let i = 0; i < flatArray.length; i++) {
const id = flatArray[i];
if (typeof id !== 'number') {
isArrayOfNumbers = false;
} else {
if (isArrayOfNumbers) {
cacheKey += id + '-';
}
_injectRegisteredStyle(id);
}
}
const key = isArrayOfNumbers ? createCacheKey(cacheKey) : null;
return _resolveStyle(flatArray, key);
}
/**
* Resolves a React Native style object to DOM attributes, accounting for
* the existing styles applied to the DOM node.
*
* To determine the next style, some of the existing DOM state must be
* converted back into React Native styles.
*/
function resolveWithNode(rnStyleNext, node) {
function getDeclaration(className) {
return lookup.byClassName[className] || emptyObject;
}
const { classList: rdomClassList, style: rdomStyle } = getDOMStyleInfo(node);
// Convert the DOM classList back into a React Native form
// Preserves unrecognized class names.
const { classList: rnClassList, style: rnStyle } = rdomClassList.reduce(
(styleProps, className) => {
const { prop, value } = getDeclaration(className);
if (prop) {
styleProps.style[prop] = value;
} else {
styleProps.classList.push(className);
}
return styleProps;
},
{ classList: [], style: {} }
);
// Create next DOM style props from current and next RN styles
const { classList: rdomClassListNext, style: rdomStyleNext } = resolve([
i18nStyle(rnStyle),
rnStyleNext
]);
// Final className
// Add the current class names not managed by React Native
const className = classListToString(rdomClassListNext.concat(rnClassList));
// Final style
// Next class names take priority over current inline styles
const style = { ...rdomStyle };
rdomClassListNext.forEach(className => {
const { prop } = getDeclaration(className);
if (style[prop]) {
style[prop] = '';
}
});
// Next inline styles take priority over current inline styles
Object.assign(style, rdomStyleNext);
return { className, style };
}
/**
* Resolves a React Native style object
*/
function _resolveStyle(style, key) {
const { doLeftAndRightSwapInRTL, isRTL } = I18nManager;
const dir = isRTL ? (doLeftAndRightSwapInRTL ? 'rtl' : 'rtlNoSwap') : 'ltr';
// faster: memoized
if (key != null && resolved[dir][key] != null) {
return resolved[dir][key];
}
const flatStyle = flattenStyle(style);
const localizedStyle = createCompileableStyle(i18nStyle(flatStyle));
// slower: convert style object to props and cache
const props = Object.keys(localizedStyle)
.sort()
.reduce(
(props, styleProp) => {
const value = localizedStyle[styleProp];
if (value != null) {
const className = 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 === 'animationKeyframes'
) {
const a = atomic({ [styleProp]: value });
Object.values(a).forEach(({ identifier, rules }) => {
props.classList.push(identifier);
rules.forEach(rule => {
sheet.insert(rule, STYLE_GROUPS.atomic);
});
});
} else {
if (!props.style) {
props.style = {};
}
// 4x slower render
props.style[styleProp] = value;
}
}
}
return props;
},
{ classList: [] }
);
props.className = classListToString(props.classList);
if (props.style) {
props.style = inline(props.style);
}
if (key != null) {
resolved[dir][key] = props;
}
return props;
}
return {
getStyleSheet() {
const textContent = sheet.getTextContent();
// Reset state on the server so critical css is always the result
if (!canUseDOM) {
init();
}
return {
id: STYLE_ELEMENT_ID,
textContent
};
},
resolve,
sheet,
resolveWithNode
};
}
/**
* Misc helpers
*/
const createCacheKey = id => {
const prefix = 'rn';
return `${prefix}-${id}`;
};
const classListToString = list => list.join(' ').trim();
/**
* Copies classList and style data from a DOM node
*/
const hyphenPattern = /-([a-z])/g;
const toCamelCase = str => str.replace(hyphenPattern, m => m[1].toUpperCase());
const getDOMStyleInfo = node => {
const nodeStyle = node.style;
const classList = Array.prototype.slice.call(node.classList);
const style = {};
// DOM style is a CSSStyleDeclaration
// https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleDeclaration
for (let i = 0; i < nodeStyle.length; i += 1) {
const property = nodeStyle.item(i);
if (property) {
// DOM style uses hyphenated prop names and may include vendor prefixes
// Transform back into React DOM style.
style[toCamelCase(property)] = nodeStyle.getPropertyValue(property);
}
}
return { classList, style };
};
@@ -7,6 +7,6 @@
* @flow
*/
import ReactNativeStyleResolver from './ReactNativeStyleResolver';
const styleResolver = new ReactNativeStyleResolver();
import createStyleResolver from './createStyleResolver';
const styleResolver = createStyleResolver();
export default styleResolver;