[change] Introduce static CSS base rules for core primitives

This patch addresses 2 related issues:

1) Browser layout times in Chrome increase when elements use a lot of CSS class
names. This begins to add up for larger trees.

2) React Native supports passing 'null' values for styles. This can remove base
styles defined using 'StyleSheet' in the implementation of components like View
and Text.

Both of these issues can be avoided, and some runtime logic and computation
removed, by moving the base styles to static CSS rules. Comparisons of the
"benchmark" UI tests (which only render View) indicate that total times in
Chrome are reduced by ~20% with almost all of those savings coming from a
~33% reduction in layout-related timings.

Ref #1136
This commit is contained in:
Nicolas Gallagher
2018-11-05 13:30:51 -08:00
parent 000b92e707
commit cc46c1d1a8
18 changed files with 224 additions and 241 deletions

View File

@@ -15,35 +15,11 @@ html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlig
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;}
.rn-reset{background-color:transparent;color:inherit;font:inherit;list-style:none;margin:0;text-align:inherit;text-decoration:none;}
.rn-pointer{cursor:pointer;}
}
.rn-pointerEvents-12vffkv > *{pointer-events:auto}
.rn-pointerEvents-12vffkv{pointer-events:none !important}
.rn-alignItems-1oszu61{-ms-flex-align:stretch;-webkit-align-items:stretch;-webkit-box-align:stretch;align-items:stretch}
.rn-borderTopStyle-1efd50x{border-top-style:solid}
.rn-borderRightStyle-14skgim{border-right-style:solid}
.rn-borderBottomStyle-rull8r{border-bottom-style:solid}
.rn-borderLeftStyle-mm0ijv{border-left-style:solid}
.rn-borderTopWidth-13yce4e{border-top-width:0px}
.rn-borderRightWidth-fnigne{border-right-width:0px}
.rn-borderBottomWidth-ndvcnb{border-bottom-width:0px}
.rn-borderLeftWidth-gxnn5r{border-left-width:0px}
.rn-boxSizing-deolkf{box-sizing:border-box}
.rn-display-6koalj{display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex}
.rn-flexShrink-1qe8dj5{-ms-flex-negative:0;-webkit-flex-shrink:0;flex-shrink:0}
.rn-flexBasis-1mlwlqe{-ms-flex-preferred-size:auto;-webkit-flex-basis:auto;flex-basis:auto}
.rn-flexDirection-eqz5dr{-ms-flex-direction:column;-webkit-box-direction:normal;-webkit-box-orient:vertical;-webkit-flex-direction:column;flex-direction:column}
.rn-marginTop-1mnahxq{margin-top:0px}
.rn-marginRight-61z16t{margin-right:0px}
.rn-marginBottom-p1pxzi{margin-bottom:0px}
.rn-marginLeft-11wrixw{margin-left:0px}
.rn-minHeight-ifefl9{min-height:0px}
.rn-minWidth-bcqeeo{min-width:0px}
.rn-paddingTop-wk8lta{padding-top:0px}
.rn-paddingRight-9aemit{padding-right:0px}
.rn-paddingBottom-1mdbw0j{padding-bottom:0px}
.rn-paddingLeft-gy4na3{padding-left:0px}
.rn-position-bnwqim{position:relative}
.rn-zIndex-1lgpqti{z-index:0}
.rn-flexGrow-16y2uox{-ms-flex-positive:1;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}
.rn-flexShrink-1wbh5a2{-ms-flex-negative:1;-webkit-flex-shrink:1;flex-shrink:1}
.rn-flexBasis-1ro0kt6{-ms-flex-preferred-size:0%;-webkit-flex-basis:0%;flex-basis:0%}"
@@ -64,5 +40,11 @@ html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlig
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;}
}</style>"
.rn-reset{background-color:transparent;color:inherit;font:inherit;list-style:none;margin:0;text-align:inherit;text-decoration:none;}
.rn-pointer{cursor:pointer;}
}
.rn-view-1d2if7t{-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;background-color:transparent;border:0 solid black;box-sizing:border-box;color:inherit;display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;flex-basis:auto;flex-direction:column;flex-shrink:0;font:inherit;list-style:none;margin:0px;min-height:0px;min-width:0px;padding:0px;position:relative;text-align:inherit;text-decoration:none;z-index:0}
.rn-hitSlop-1be6dej{bottom:0px;left:0px;position:absolute;right:0px;top:0px;z-index:-1}
.rn-text-1ogs4z5{background-color:transparent;border-width:0px;box-sizing:border-box;color:inherit;display:inline;font:14px system-ui, -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", Roboto, Ubuntu, \\"Helvetica Neue\\", sans-serif;margin:0px;padding:0px;text-align:inherit;text-decoration:none;white-space:pre-wrap;word-wrap:break-word}
.rn-textinput-wwmj4d{-moz-appearance:textfield;-webkit-appearance:none;background-color:transparent;border-radius:0px;border:0 solid black;box-sizing:border-box;font-family:14px System;padding:0px;resize:none}</style>"
`;

View File

@@ -15,9 +15,9 @@ import WebStyleSheet from './WebStyleSheet';
const emptyObject = {};
const STYLE_ELEMENT_ID = 'react-native-stylesheet';
const createClassName = (prop, value) => {
const hashed = hash(prop + normalizeValue(value));
return process.env.NODE_ENV !== 'production' ? `rn-${prop}-${hashed}` : `rn-${hashed}`;
const createClassName = (name, value) => {
const hashed = hash(name + normalizeValue(value));
return process.env.NODE_ENV !== 'production' ? `rn-${name}-${hashed}` : `rn-${hashed}`;
};
const normalizeValue = value => (typeof value === 'object' ? JSON.stringify(value) : value);
@@ -69,6 +69,13 @@ export default class StyleSheetManager {
return className;
}
injectRule(name, body: string): void {
const className = createClassName(name, body);
const rule = `.${className}{${body}}`;
this._sheet.insertRuleOnce(rule);
return className;
}
_addToCache(className, prop, value) {
const cache = this._cache;
if (!cache.byProp[prop]) {

View File

@@ -103,10 +103,7 @@ StyleSheetValidation.addValidStylePropTypes({
objectFit: oneOf(['fill', 'contain', 'cover', 'none', 'scale-down']),
objectPosition: string,
pointerEvents: string,
tableLayout: string,
/* @private */
MozAppearance: string,
WebkitAppearance: string
tableLayout: string
});
export default StyleSheetValidation;

View File

@@ -10,6 +10,8 @@ html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlig
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;}
.rn-reset{background-color:transparent;color:inherit;font:inherit;list-style:none;margin:0;text-align:inherit;text-decoration:none;}
.rn-pointer{cursor:pointer;}
}
.rn---test-property-ax3bxi{--test-property:test-value}",
}

View File

@@ -39,44 +39,17 @@ describe('StyleSheet/createReactDOMStyle', () => {
expect(createReactDOMStyle(style)).toMatchSnapshot();
});
describe('borderWidth styles', () => {
test('defaults to 0 when "null"', () => {
expect(createReactDOMStyle({ borderWidth: null })).toEqual({
borderTopWidth: '0px',
borderRightWidth: '0px',
borderBottomWidth: '0px',
borderLeftWidth: '0px'
});
expect(createReactDOMStyle({ borderWidth: 2, borderRightWidth: null })).toEqual({
borderTopWidth: '2px',
borderRightWidth: '0px',
borderBottomWidth: '2px',
borderLeftWidth: '2px'
});
});
});
describe('flexbox styles', () => {
test('flex defaults', () => {
expect(createReactDOMStyle({ display: 'flex' })).toEqual({
display: 'flex',
flexShrink: 0,
flexBasis: 'auto'
});
});
test('flex: -1', () => {
expect(createReactDOMStyle({ display: 'flex', flex: -1 })).toEqual({
display: 'flex',
expect(createReactDOMStyle({ flex: -1 })).toEqual({
flexBasis: 'auto',
flexGrow: 0,
flexShrink: 1,
flexBasis: 'auto'
flexShrink: 1
});
});
test('flex: 0', () => {
expect(createReactDOMStyle({ display: 'flex', flex: 0 })).toEqual({
display: 'flex',
expect(createReactDOMStyle({ flex: 0 })).toEqual({
flexGrow: 0,
flexShrink: 0,
flexBasis: '0%'
@@ -84,8 +57,7 @@ describe('StyleSheet/createReactDOMStyle', () => {
});
test('flex: 1', () => {
expect(createReactDOMStyle({ display: 'flex', flex: 1 })).toEqual({
display: 'flex',
expect(createReactDOMStyle({ flex: 1 })).toEqual({
flexGrow: 1,
flexShrink: 1,
flexBasis: '0%'
@@ -93,8 +65,7 @@ describe('StyleSheet/createReactDOMStyle', () => {
});
test('flex: 10', () => {
expect(createReactDOMStyle({ display: 'flex', flex: 10 })).toEqual({
display: 'flex',
expect(createReactDOMStyle({ flex: 10 })).toEqual({
flexGrow: 10,
flexShrink: 1,
flexBasis: '0%'
@@ -103,15 +74,12 @@ describe('StyleSheet/createReactDOMStyle', () => {
test('flexBasis overrides', () => {
// is flex-basis applied?
expect(createReactDOMStyle({ display: 'flex', flexBasis: '25%' })).toEqual({
display: 'flex',
flexShrink: 0,
expect(createReactDOMStyle({ flexBasis: '25%' })).toEqual({
flexBasis: '25%'
});
// can flex-basis override the 'flex' expansion?
expect(createReactDOMStyle({ display: 'flex', flex: 1, flexBasis: '25%' })).toEqual({
display: 'flex',
expect(createReactDOMStyle({ flex: 1, flexBasis: '25%' })).toEqual({
flexGrow: 1,
flexShrink: 1,
flexBasis: '25%'
@@ -120,15 +88,12 @@ describe('StyleSheet/createReactDOMStyle', () => {
test('flexShrink overrides', () => {
// is flex-shrink applied?
expect(createReactDOMStyle({ display: 'flex', flexShrink: 1 })).toEqual({
display: 'flex',
flexShrink: 1,
flexBasis: 'auto'
expect(createReactDOMStyle({ flexShrink: 1 })).toEqual({
flexShrink: 1
});
// can flex-shrink override the 'flex' expansion?
expect(createReactDOMStyle({ display: 'flex', flex: 1, flexShrink: 2 })).toEqual({
display: 'flex',
expect(createReactDOMStyle({ flex: 1, flexShrink: 2 })).toEqual({
flexGrow: 1,
flexShrink: 2,
flexBasis: '0%'

View File

@@ -0,0 +1,13 @@
/**
* 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 strict
*/
export const monospaceFontStack = 'monospace, monospace';
export const systemFontStack =
'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, "Helvetica Neue", sans-serif';

View File

@@ -7,6 +7,7 @@
* @noflow
*/
import { monospaceFontStack, systemFontStack } from './constants';
import normalizeColor from '../../modules/normalizeColor';
import normalizeValue from './normalizeValue';
import resolveShadowValue from './resolveShadowValue';
@@ -54,18 +55,6 @@ const colorProps = {
color: true
};
const borderWidthProps = {
borderWidth: true,
borderTopWidth: true,
borderRightWidth: true,
borderBottomWidth: true,
borderLeftWidth: true
};
const monospaceFontStack = 'monospace, monospace';
const systemFontStack =
'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, "Helvetica Neue", sans-serif';
const alphaSortProps = propsArray =>
propsArray.sort((a, b) => {
if (a < b) {
@@ -167,12 +156,6 @@ const createReducer = (style, styleProps) => {
return (resolvedStyle, prop) => {
let value = normalizeValue(prop, style[prop]);
// Make sure the default border width is explicitly set to '0' to avoid
// falling back to any unwanted user-agent styles.
if (borderWidthProps[prop]) {
value = value == null ? normalizeValue(null, 0) : value;
}
// Normalize color values
if (colorProps[prop]) {
value = normalizeColor(value);
@@ -203,21 +186,6 @@ const createReducer = (style, styleProps) => {
break;
}
case 'display': {
resolvedStyle.display = value;
// A flex container in React Native has these defaults which should be
// set only if there is no otherwise supplied flex style.
if (style.display === 'flex' && style.flex == null) {
if (style.flexShrink == null) {
resolvedStyle.flexShrink = 0;
}
if (style.flexBasis == null) {
resolvedStyle.flexBasis = 'auto';
}
}
break;
}
// The 'flex' property value in React Native must be a positive integer,
// 0, or -1.
case 'flex': {

View File

@@ -8,7 +8,6 @@
*/
import hyphenateStyleName from 'hyphenate-style-name';
import mapKeyValue from '../../modules/mapKeyValue';
import normalizeValue from './normalizeValue';
import prefixStyles from '../../modules/prefixStyles';
@@ -27,9 +26,15 @@ const createDeclarationString = (prop, val) => {
* createRuleBlock({ width: 20, color: 'blue' });
* // => 'color:blue;width:20px'
*/
const createRuleBlock = style =>
mapKeyValue(prefixStyles(style), createDeclarationString)
.sort()
.join(';');
const createRuleBlock = style => {
const prefixedStyle = prefixStyles(style);
return (
Object.keys(prefixedStyle)
.map(prop => createDeclarationString(prop, prefixedStyle[prop]))
// put short-form and vendor prefixed properties first
.sort()
.join(';')
);
};
export default createRuleBlock;

View File

@@ -0,0 +1,33 @@
/**
* 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.
*
* @noflow
*/
import createRuleBlock from './createRuleBlock';
import styleResolver from './styleResolver';
import { systemFontStack } from './constants';
const css = {
create(rules) {
const result = {};
Object.keys(rules).forEach(key => {
const rule = rules[key];
if (rule.font && rule.font.indexOf('System') > -1) {
rule.font = rule.font.replace('System', systemFontStack);
}
if (rule.fontFamily === 'System') {
rule.fontFamily = systemFontStack;
}
const cssRule = createRuleBlock(rule);
const className = styleResolver.styleSheetManager.injectRule(key, cssRule);
result[key] = className;
});
return result;
}
};
export default css;

View File

@@ -12,13 +12,34 @@ 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);}',
'html{' +
'-ms-text-size-adjust:100%;' +
'-webkit-text-size-adjust:100%;' +
'-webkit-tap-highlight-color:rgba(0,0,0,0);' +
'}',
'body{margin:0;}',
// minimal form pseudo-element reset
'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;}'
'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;' +
'}',
// Reset styles for heading, link, and list DOM elements
'.rn-reset{' +
'background-color:transparent;' +
'color:inherit;' +
'font:inherit;' +
'list-style:none;' +
'margin:0;' +
'text-align:inherit;' +
'text-decoration:none;' +
'}',
// For pressable elements
'.rn-pointer{cursor:pointer;}'
];
const reset = [safeRule(resets.join('\n'))];

View File

@@ -2,7 +2,7 @@
exports[`components/Text prop "onPress" 1`] = `
<div
className="rn-borderTopWidth-13yce4e rn-borderRightWidth-fnigne rn-borderBottomWidth-ndvcnb rn-borderLeftWidth-gxnn5r rn-boxSizing-deolkf rn-color-homxoj rn-cursor-1loqt21 rn-display-1471scf rn-fontFamily-14xgk7a rn-fontSize-1b43r93 rn-fontStyle-o11vmf rn-fontVariant-ebii48 rn-fontWeight-gul640 rn-lineHeight-t9a87b rn-marginTop-1mnahxq rn-marginRight-61z16t rn-marginBottom-p1pxzi rn-marginLeft-11wrixw rn-paddingTop-wk8lta rn-paddingRight-9aemit rn-paddingBottom-1mdbw0j rn-paddingLeft-gy4na3 rn-textDecoration-bauka4 rn-whiteSpace-q42fyq rn-wordWrap-qvutc0"
className="rn-text-1ogs4z5 rn-cursor-1loqt21"
data-focusable={true}
dir="auto"
onClick={[Function]}
@@ -13,14 +13,14 @@ exports[`components/Text prop "onPress" 1`] = `
exports[`components/Text prop "selectable" 1`] = `
<div
className="rn-borderTopWidth-13yce4e rn-borderRightWidth-fnigne rn-borderBottomWidth-ndvcnb rn-borderLeftWidth-gxnn5r rn-boxSizing-deolkf rn-color-homxoj rn-display-1471scf rn-fontFamily-14xgk7a rn-fontSize-1b43r93 rn-fontStyle-o11vmf rn-fontVariant-ebii48 rn-fontWeight-gul640 rn-lineHeight-t9a87b rn-marginTop-1mnahxq rn-marginRight-61z16t rn-marginBottom-p1pxzi rn-marginLeft-11wrixw rn-paddingTop-wk8lta rn-paddingRight-9aemit rn-paddingBottom-1mdbw0j rn-paddingLeft-gy4na3 rn-textDecoration-bauka4 rn-whiteSpace-q42fyq rn-wordWrap-qvutc0"
className="rn-text-1ogs4z5"
dir="auto"
/>
`;
exports[`components/Text prop "selectable" 2`] = `
<div
className="rn-borderTopWidth-13yce4e rn-borderRightWidth-fnigne rn-borderBottomWidth-ndvcnb rn-borderLeftWidth-gxnn5r rn-boxSizing-deolkf rn-color-homxoj rn-display-1471scf rn-fontFamily-14xgk7a rn-fontSize-1b43r93 rn-fontStyle-o11vmf rn-fontVariant-ebii48 rn-fontWeight-gul640 rn-lineHeight-t9a87b rn-marginTop-1mnahxq rn-marginRight-61z16t rn-marginBottom-p1pxzi rn-marginLeft-11wrixw rn-paddingTop-wk8lta rn-paddingRight-9aemit rn-paddingBottom-1mdbw0j rn-paddingLeft-gy4na3 rn-textDecoration-bauka4 rn-userSelect-lrvibr rn-whiteSpace-q42fyq rn-wordWrap-qvutc0"
className="rn-text-1ogs4z5 rn-userSelect-lrvibr"
dir="auto"
/>
`;

View File

@@ -13,6 +13,7 @@ import applyNativeMethods from '../../modules/applyNativeMethods';
import { bool } from 'prop-types';
import { Component } from 'react';
import createElement from '../createElement';
import css from '../StyleSheet/css';
import StyleSheet from '../StyleSheet';
import TextPropTypes from './TextPropTypes';
@@ -65,10 +66,10 @@ class Text extends Component<*> {
otherProps.onKeyDown = this._createEnterHandler(onPress);
}
otherProps.className = classes.text;
// allow browsers to automatically infer the language writing direction
otherProps.dir = dir !== undefined ? dir : 'auto';
otherProps.style = [
styles.initial,
this.context.isInAParentText === true && styles.isInAParentText,
style,
selectable === false && styles.notSelectable,
@@ -97,24 +98,24 @@ class Text extends Component<*> {
}
}
const styles = StyleSheet.create({
initial: {
const classes = css.create({
text: {
backgroundColor: 'transparent',
borderWidth: 0,
boxSizing: 'border-box',
color: 'inherit',
display: 'inline',
fontFamily: 'System',
fontSize: 14,
fontStyle: 'inherit',
fontVariant: ['inherit'],
fontWeight: 'inherit',
lineHeight: 'inherit',
font: '14px System',
margin: 0,
padding: 0,
textDecorationLine: 'none',
textAlign: 'inherit',
textDecoration: 'none',
whiteSpace: 'pre-wrap',
wordWrap: 'break-word'
},
}
});
const styles = StyleSheet.create({
isInAParentText: {
// inherit parent font styles
fontFamily: 'inherit',

View File

@@ -14,8 +14,8 @@ import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment';
import { Component } from 'react';
import ColorPropType from '../ColorPropType';
import createElement from '../createElement';
import css from '../StyleSheet/css';
import findNodeHandle from '../findNodeHandle';
import StyleSheet from '../StyleSheet';
import StyleSheetPropType from '../../modules/StyleSheetPropType';
import TextInputStylePropTypes from './TextInputStylePropTypes';
import TextInputState from '../../modules/TextInputState';
@@ -256,6 +256,7 @@ class TextInput extends Component<*> {
Object.assign(otherProps, {
autoCorrect: autoCorrect ? 'on' : 'off',
className: classes.textinput,
dir: 'auto',
onBlur: normalizeEventHandler(this._handleBlur),
onChange: normalizeEventHandler(this._handleChange),
@@ -266,7 +267,7 @@ class TextInput extends Component<*> {
readOnly: !editable,
ref: this._setNode,
spellCheck: spellCheck != null ? spellCheck : autoCorrect,
style: [styles.initial, style]
style
});
if (multiline) {
@@ -428,18 +429,15 @@ class TextInput extends Component<*> {
};
}
const styles = StyleSheet.create({
initial: {
const classes = css.create({
textinput: {
MozAppearance: 'textfield',
WebkitAppearance: 'none',
backgroundColor: 'transparent',
borderColor: 'black',
border: '0 solid black',
borderRadius: 0,
borderStyle: 'solid',
borderWidth: 0,
boxSizing: 'border-box',
fontFamily: 'System',
fontSize: 14,
fontFamily: '14px System',
padding: 0,
resize: 'none'
}

View File

@@ -10,6 +10,7 @@ import applyLayout from '../../modules/applyLayout';
import applyNativeMethods from '../../modules/applyNativeMethods';
import { bool } from 'prop-types';
import createElement from '../createElement';
import css from '../StyleSheet/css';
import filterSupportedProps from './filterSupportedProps';
import invariant from 'fbjs/lib/invariant';
import StyleSheet from '../StyleSheet';
@@ -51,14 +52,18 @@ class View extends Component<ViewProps> {
const { isInAParentText } = this.context;
supportedProps.className = classes.view;
supportedProps.style = StyleSheet.compose(
styles.initial,
StyleSheet.compose(isInAParentText && styles.inline, this.props.style)
isInAParentText && styles.inline,
this.props.style
);
if (hitSlop) {
const hitSlopStyle = calculateHitSlopStyle(hitSlop);
const hitSlopChild = createElement('span', { style: [styles.hitSlop, hitSlopStyle] });
const hitSlopChild = createElement('span', {
className: classes.hitslop,
style: hitSlopStyle
});
supportedProps.children = React.Children.toArray([hitSlopChild, supportedProps.children]);
}
@@ -66,32 +71,45 @@ class View extends Component<ViewProps> {
}
}
const styles = StyleSheet.create({
// https://github.com/facebook/css-layout#default-values
initial: {
const classes = css.create({
view: {
alignItems: 'stretch',
borderWidth: 0,
borderStyle: 'solid',
border: '0 solid black',
boxSizing: 'border-box',
display: 'flex',
flexBasis: 'auto',
flexDirection: 'column',
flexShrink: 0,
margin: 0,
minHeight: 0,
minWidth: 0,
padding: 0,
position: 'relative',
zIndex: 0,
// fix flexbox bugs
minHeight: 0,
minWidth: 0
},
inline: {
display: 'inline-flex'
// resets for if View is rendered as a link or list DOM element
backgroundColor: 'transparent',
color: 'inherit',
font: 'inherit',
listStyle: 'none',
textAlign: 'inherit',
textDecoration: 'none'
},
// this zIndex-ordering positions the hitSlop above the View but behind
// its children
hitSlop: {
...StyleSheet.absoluteFillObject,
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: -1
}
});
const styles = StyleSheet.create({
inline: {
display: 'inline-flex'
}
});
export default applyLayout(applyNativeMethods(View));

View File

@@ -2,10 +2,14 @@
exports[`modules/createDOMProps includes "rel" values for "a" elements (to securely open external links) 1`] = `" noopener noreferrer"`;
exports[`modules/createDOMProps includes cursor style for "button" role 1`] = `"rn-cursor-1loqt21"`;
exports[`modules/createDOMProps includes base reset style for browser-styled elements 1`] = `"rn-reset"`;
exports[`modules/createDOMProps includes reset styles for "a" elements 1`] = `"rn-backgroundColor-1niwhzg rn-color-homxoj rn-textDecoration-bauka4"`;
exports[`modules/createDOMProps includes base reset style for browser-styled elements 2`] = `"rn-reset"`;
exports[`modules/createDOMProps includes reset styles for "button" elements 1`] = `"rn-appearance-30o5oe rn-backgroundColor-1niwhzg rn-color-homxoj rn-fontFamily-poiln3 rn-fontSize-7cikom rn-fontStyle-o11vmf rn-fontVariant-ebii48 rn-fontWeight-gul640 rn-lineHeight-t9a87b rn-textAlign-1ttztb7"`;
exports[`modules/createDOMProps includes base reset style for browser-styled elements 3`] = `"rn-reset"`;
exports[`modules/createDOMProps includes reset styles for "ul" elements 1`] = `"rn-listStyle-1ebb2ja"`;
exports[`modules/createDOMProps includes base reset style for browser-styled elements 4`] = `"rn-reset"`;
exports[`modules/createDOMProps includes cursor style for pressable roles 1`] = `"rn-pointer"`;
exports[`modules/createDOMProps includes cursor style for pressable roles 2`] = `"rn-pointer"`;

View File

@@ -200,23 +200,15 @@ describe('modules/createDOMProps', () => {
expect(props.rel).toMatchSnapshot();
});
test('includes reset styles for "a" elements', () => {
const props = createDOMProps('a');
expect(props.className).toMatchSnapshot();
test('includes cursor style for pressable roles', () => {
expect(createDOMProps('span', { accessibilityRole: 'link' }).className).toMatchSnapshot();
expect(createDOMProps('span', { accessibilityRole: 'button' }).className).toMatchSnapshot();
});
test('includes reset styles for "button" elements', () => {
const props = createDOMProps('button');
expect(props.className).toMatchSnapshot();
});
test('includes cursor style for "button" role', () => {
const props = createDOMProps('span', { accessibilityRole: 'button' });
expect(props.className).toMatchSnapshot();
});
test('includes reset styles for "ul" elements', () => {
const props = createDOMProps('ul');
expect(props.className).toMatchSnapshot();
test('includes base reset style for browser-styled elements', () => {
expect(createDOMProps('a').className).toMatchSnapshot();
expect(createDOMProps('button').className).toMatchSnapshot();
expect(createDOMProps('li').className).toMatchSnapshot();
expect(createDOMProps('ul').className).toMatchSnapshot();
});
});

View File

@@ -13,40 +13,6 @@ import styleResolver from '../../exports/StyleSheet/styleResolver';
const emptyObject = {};
const resetStyles = StyleSheet.create({
ariaButton: {
cursor: 'pointer'
},
button: {
appearance: 'none',
backgroundColor: 'transparent',
color: 'inherit',
fontFamily: 'inherit',
fontSize: 'inherit',
fontStyle: 'inherit',
fontVariant: ['inherit'],
fontWeight: 'inherit',
lineHeight: 'inherit',
textAlign: 'inherit'
},
heading: {
fontFamily: 'inherit',
fontSize: 'inherit',
fontStyle: 'inherit',
fontVariant: ['inherit'],
fontWeight: 'inherit',
lineHeight: 'inherit'
},
link: {
backgroundColor: 'transparent',
color: 'inherit',
textDecorationLine: 'none'
},
list: {
listStyle: 'none'
}
});
const pointerEventsStyles = StyleSheet.create({
auto: {
pointerEvents: 'auto'
@@ -123,6 +89,8 @@ const createDOMProps = (component, props, styleResolver) => {
importantForAccessibility !== 'no-hide-descendants';
if (
role === 'link' ||
component === 'a' ||
component === 'button' ||
component === 'input' ||
component === 'select' ||
component === 'textarea'
@@ -146,30 +114,53 @@ const createDOMProps = (component, props, styleResolver) => {
// STYLE
// Resolve React Native styles to optimized browser equivalent
const reactNativeStyle = [
component === 'a' && resetStyles.link,
component === 'button' && resetStyles.button,
role === 'heading' && resetStyles.heading,
component === 'ul' && resetStyles.list,
role === 'button' && !disabled && resetStyles.ariaButton,
const reactNativeStyle = StyleSheet.compose(
pointerEvents && pointerEventsStyles[pointerEvents],
providedStyle,
placeholderTextColor && { placeholderTextColor }
];
StyleSheet.compose(
providedStyle,
placeholderTextColor && { placeholderTextColor }
)
);
const { className, style } = styleResolver(reactNativeStyle);
if (className && className.constructor === String) {
domProps.className = props.className ? `${props.className} ${className}` : className;
}
if (style) {
domProps.style = style;
}
// CLASSNAME
// Apply static style resets
let c;
// style interactive elements for mouse and mobile browsers
if ((role === 'button' || role === 'link') && !disabled) {
c = 'rn-pointer';
}
// style reset various elements (not all are used internally)
if (
component === 'a' ||
component === 'button' ||
component === 'li' ||
component === 'ul' ||
role === 'heading'
) {
c = 'rn-reset' + (c != null ? ' ' + c : '');
}
// style from createElement use
if (props.className != null) {
c = props.className + (c != null ? ' ' + c : '');
}
// style from React Native StyleSheets
if (className != null && className !== '') {
c = (c != null ? c + ' ' : '') + className;
}
if (c != null) {
domProps.className = c;
}
// OTHER
// Native element ID
if (nativeID && nativeID.constructor === String) {
domProps.id = nativeID;
}
// Link security and automation test ids
// Link security
if (component === 'a' && domProps.target === '_blank') {
domProps.rel = `${domProps.rel || ''} noopener noreferrer`;
}

View File

@@ -1,14 +0,0 @@
const hasOwnProperty = Object.prototype.hasOwnProperty;
const mapKeyValue = (obj, fn) => {
const result = [];
for (const key in obj) {
if (hasOwnProperty.call(obj, key)) {
const r = fn(key, obj[key]);
r && result.push(r);
}
}
return result;
};
export default mapKeyValue;