From d4417e93a31636cdf141e3256b3fc6ba96f8e527 Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Wed, 30 Jan 2019 17:26:53 -0800 Subject: [PATCH] [change] Use classic CSS in View, Text, etc., implementations The CSS base styles for certain primitives are implemented using classic CSS to reduce browser layout times and better support 'null' values in StyleSheet-defined styles. Combined with the previous patch this reduces the benchmark layout times by about 30%. Ref #1136 Fix #1044 Fix #1223 Fix #13 --- README.md | 2 +- .../__snapshots__/index-test.js.snap | 13 +++ .../src/exports/Image/index.js | 22 +++-- .../StyleSheet/StyleSheetValidation.js | 5 +- .../createReactDOMStyle-test.js.snap | 18 ++++ .../__tests__/createReactDOMStyle-test.js | 63 ++++--------- .../exports/StyleSheet/createReactDOMStyle.js | 31 +----- .../src/exports/StyleSheet/css.js | 51 ++++++++++ .../__snapshots__/index-test.js.snap | 6 +- .../src/exports/Text/index.js | 56 ++++++----- .../src/exports/TextInput/index.js | 21 ++--- .../src/exports/View/ViewPropTypes.js | 1 + .../src/exports/View/index.js | 43 +++++---- .../__snapshots__/index-test.js.snap | 12 ++- .../createDOMProps/__tests__/index-test.js | 24 ++--- .../src/modules/createDOMProps/index.js | 94 ++++++++++--------- 16 files changed, 255 insertions(+), 207 deletions(-) create mode 100644 packages/react-native-web/src/exports/StyleSheet/css.js diff --git a/README.md b/README.md index b11b8fc0..12358ac3 100644 --- a/README.md +++ b/README.md @@ -146,7 +146,7 @@ React Native v0.55 | SwipeableFlatList | ✓ | | | SwipeableListView | ✓ | | | Switch | ✓ | | -| Text | ✓ | Missing `onLongPress` ([#1011](https://github.com/necolas/react-native-web/issues/1011)) and `numberOfLines` ([#13](https://github.com/necolas/react-native-web/issues/13)) support. | +| Text | ✓ | Missing `onLongPress` ([#1011](https://github.com/necolas/react-native-web/issues/1011)) support. | | TextInput | ✓ | Missing rich text features ([#1023](https://github.com/necolas/react-native-web/issues/1023)), and auto-expanding behaviour ([#795](https://github.com/necolas/react-native-web/issues/795)). | | Touchable | ✓ | Includes additional support for mouse and keyboard interactions. | | TouchableHighlight | ✓ | | 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 8bcfdeac..d285ae2b 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 @@ -19,5 +19,18 @@ input::-webkit-inner-spin-button,input::-webkit-outer-spin-button,input::-webkit @media all { [stylesheet-group=\\"0.1\\"]{} :focus:not([data-focusvisible-polyfill]){outline: none;} +} +@media all { +[stylesheet-group=\\"1\\"]{} +.css-reset-4rbku5 { background-color: rgba(0,0,0,0.00); color: inherit; font: inherit; list-style: none; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px; text-align: inherit; text-decoration: none; } +.css-cursor-18t94o4 { cursor: pointer; } +.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; } +.css-hitSlop-mjp8i1 { bottom: 0px; left: 0px; position: absolute; right: 0px; top: 0px; z-index: -1; } +.css-accessibilityImage-9pa8cd { bottom: 0px; height: 100%; left: 0px; opacity: 0; position: absolute; right: 0px; top: 0px; width: 100%; z-index: -1; } +.css-text-76zvg2 { border-bottom-width: 0px; border-left-width: 0px; border-right-width: 0px; border-top-width: 0px; box-sizing: border-box; color: rgba(0,0,0,1.00); display: inline; font: 14px system-ui, -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", Roboto, Ubuntu, \\"Helvetica Neue\\", sans-serif; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; margin-top: 0px; padding-bottom: 0px; padding-left: 0px; padding-right: 0px; padding-top: 0px; white-space: pre-wrap; word-wrap: break-word; } +.css-textHasAncestor-16my406 { color: inherit; font: inherit; white-space: inherit; } +.css-textOneLine-bfa6kz { max-width: 100%; overflow-x: hidden; overflow-y: hidden; text-overflow: ellipsis; white-space: nowrap; } +.css-textMultiLine-cens5h { -webkit-box-orient: vertical; display: -webkit-box; max-width: 100%; overflow-x: hidden; overflow-y: hidden; text-overflow: ellipsis; } +.css-textinput-1cwyjr8 { -moz-appearance: textfield; -webkit-appearance: none; background-color: rgba(0,0,0,0.00); border-bottom-left-radius: 0px; border-bottom-right-radius: 0px; border-top-left-radius: 0px; border-top-right-radius: 0px; border: 0 solid black; box-sizing: border-box; font: 14px system-ui, -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", Roboto, Ubuntu, \\"Helvetica Neue\\", sans-serif; padding-bottom: 0px; padding-left: 0px; padding-right: 0px; padding-top: 0px; resize: none; } }" `; diff --git a/packages/react-native-web/src/exports/Image/index.js b/packages/react-native-web/src/exports/Image/index.js index cf72f098..2597bf95 100644 --- a/packages/react-native-web/src/exports/Image/index.js +++ b/packages/react-native-web/src/exports/Image/index.js @@ -10,6 +10,7 @@ import applyNativeMethods from '../../modules/applyNativeMethods'; import createElement from '../createElement'; +import css from '../StyleSheet/css'; import { getAssetByID } from '../../modules/AssetRegistry'; import resolveShadowValue from '../StyleSheet/resolveShadowValue'; import ImageLoader from '../../modules/ImageLoader'; @@ -254,10 +255,10 @@ class Image extends Component<*, State> { const hiddenImage = displayImageUri ? createElement('img', { alt: accessibilityLabel || '', + className: classes.accessibilityImage, draggable: draggable || false, ref: this._setImageRef, - src: displayImageUri, - style: styles.accessibilityImage + src: displayImageUri }) : null; @@ -387,6 +388,16 @@ class Image extends Component<*, State> { } } +const classes = css.create({ + accessibilityImage: { + ...StyleSheet.absoluteFillObject, + height: '100%', + opacity: 0, + width: '100%', + zIndex: -1 + } +}); + const styles = StyleSheet.create({ root: { flexBasis: 'auto', @@ -405,13 +416,6 @@ const styles = StyleSheet.create({ height: '100%', width: '100%', zIndex: -1 - }, - accessibilityImage: { - ...StyleSheet.absoluteFillObject, - height: '100%', - opacity: 0, - width: '100%', - zIndex: -1 } }); diff --git a/packages/react-native-web/src/exports/StyleSheet/StyleSheetValidation.js b/packages/react-native-web/src/exports/StyleSheet/StyleSheetValidation.js index b94e3aed..29ecb37a 100644 --- a/packages/react-native-web/src/exports/StyleSheet/StyleSheetValidation.js +++ b/packages/react-native-web/src/exports/StyleSheet/StyleSheetValidation.js @@ -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; diff --git a/packages/react-native-web/src/exports/StyleSheet/__tests__/__snapshots__/createReactDOMStyle-test.js.snap b/packages/react-native-web/src/exports/StyleSheet/__tests__/__snapshots__/createReactDOMStyle-test.js.snap index 0bf5e778..dbeac739 100644 --- a/packages/react-native-web/src/exports/StyleSheet/__tests__/__snapshots__/createReactDOMStyle-test.js.snap +++ b/packages/react-native-web/src/exports/StyleSheet/__tests__/__snapshots__/createReactDOMStyle-test.js.snap @@ -1,17 +1,35 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`StyleSheet/createReactDOMStyle fontFamily "Noto, BlinkMacSystemFont" 1`] = ` +Object { + "fontFamily": "Noto, BlinkMacSystemFont", +} +`; + exports[`StyleSheet/createReactDOMStyle fontFamily "Noto, System" 1`] = ` Object { "fontFamily": "Noto, system-ui, -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", Roboto, Ubuntu, \\"Helvetica Neue\\", sans-serif", } `; +exports[`StyleSheet/createReactDOMStyle fontFamily "Noto, System" 2`] = ` +Object { + "font": "14px Noto, system-ui, -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", Roboto, Ubuntu, \\"Helvetica Neue\\", sans-serif", +} +`; + exports[`StyleSheet/createReactDOMStyle fontFamily "System" 1`] = ` Object { "fontFamily": "system-ui, -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", Roboto, Ubuntu, \\"Helvetica Neue\\", sans-serif", } `; +exports[`StyleSheet/createReactDOMStyle fontFamily "System" 2`] = ` +Object { + "font": "14px system-ui, -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", Roboto, Ubuntu, \\"Helvetica Neue\\", sans-serif", +} +`; + exports[`StyleSheet/createReactDOMStyle fontFamily "monospace" 1`] = ` Object { "fontFamily": "monospace, monospace", diff --git a/packages/react-native-web/src/exports/StyleSheet/__tests__/createReactDOMStyle-test.js b/packages/react-native-web/src/exports/StyleSheet/__tests__/createReactDOMStyle-test.js index 1eb78a87..9ae47a2f 100644 --- a/packages/react-native-web/src/exports/StyleSheet/__tests__/createReactDOMStyle-test.js +++ b/packages/react-native-web/src/exports/StyleSheet/__tests__/createReactDOMStyle-test.js @@ -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%' @@ -147,10 +112,16 @@ describe('StyleSheet/createReactDOMStyle', () => { test('"System"', () => { expect(createReactDOMStyle({ fontFamily: 'System' })).toMatchSnapshot(); + expect(createReactDOMStyle({ font: '14px System' })).toMatchSnapshot(); }); test('"Noto, System"', () => { expect(createReactDOMStyle({ fontFamily: 'Noto, System' })).toMatchSnapshot(); + expect(createReactDOMStyle({ font: '14px Noto, System' })).toMatchSnapshot(); + }); + + test('"Noto, BlinkMacSystemFont"', () => { + expect(createReactDOMStyle({ fontFamily: 'Noto, BlinkMacSystemFont' })).toMatchSnapshot(); }); }); diff --git a/packages/react-native-web/src/exports/StyleSheet/createReactDOMStyle.js b/packages/react-native-web/src/exports/StyleSheet/createReactDOMStyle.js index e41890d5..d33aa940 100644 --- a/packages/react-native-web/src/exports/StyleSheet/createReactDOMStyle.js +++ b/packages/react-native-web/src/exports/StyleSheet/createReactDOMStyle.js @@ -42,14 +42,6 @@ const styleShortFormProperties = { writingDirection: ['direction'] }; -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'; @@ -96,13 +88,7 @@ const createReactDOMStyle = style => { Object.keys(style) .sort() .forEach(prop => { - let value = normalizeValueWithProperty(style[prop], 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 ? normalizeValueWithProperty(0) : value; - } + const value = normalizeValueWithProperty(style[prop], prop); // Ignore everything else with a null value if (value == null) { @@ -129,21 +115,6 @@ const createReactDOMStyle = style => { 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': { diff --git a/packages/react-native-web/src/exports/StyleSheet/css.js b/packages/react-native-web/src/exports/StyleSheet/css.js new file mode 100644 index 00000000..4606b64e --- /dev/null +++ b/packages/react-native-web/src/exports/StyleSheet/css.js @@ -0,0 +1,51 @@ +/** + * 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 { classic } from './compile'; +import styleResolver from './styleResolver'; +import { STYLE_GROUPS } from './constants'; + +/** + * A simple (and dangerous) CSS system. + * The order of CSS rule insertion is not guaranteed. + * Avoiding combining 2 or more classes that modify the same property. + */ +const css = { + /** + * const classes = css.create({ base: {}, extra: {} }) + */ + create(rules) { + const result = {}; + Object.keys(rules).forEach(name => { + const style = rules[name]; + const compiled = classic(style, name); + + Object.values(compiled).forEach(({ identifier, rules }) => { + rules.forEach(rule => { + styleResolver.sheet.insert(rule, STYLE_GROUPS.classic); + }); + result[name] = identifier; + }); + }); + return result; + }, + /** + * css.combine(classes.base, classes.extra) + */ + combine(...args) { + return args.reduce((className, value) => { + if (value) { + className += className.length > 0 ? ' ' + value : value; + } + return className; + }, ''); + } +}; + +export default css; diff --git a/packages/react-native-web/src/exports/Text/__tests__/__snapshots__/index-test.js.snap b/packages/react-native-web/src/exports/Text/__tests__/__snapshots__/index-test.js.snap index d0d30875..e53ecead 100644 --- a/packages/react-native-web/src/exports/Text/__tests__/__snapshots__/index-test.js.snap +++ b/packages/react-native-web/src/exports/Text/__tests__/__snapshots__/index-test.js.snap @@ -2,7 +2,7 @@ exports[`components/Text prop "onPress" 1`] = `
`; exports[`components/Text prop "selectable" 2`] = `
`; diff --git a/packages/react-native-web/src/exports/Text/index.js b/packages/react-native-web/src/exports/Text/index.js index 0610649c..841589c2 100644 --- a/packages/react-native-web/src/exports/Text/index.js +++ b/packages/react-native-web/src/exports/Text/index.js @@ -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,14 +66,19 @@ class Text extends Component<*> { otherProps.onKeyDown = this._createEnterHandler(onPress); } + otherProps.className = css.combine( + this.props.className, + classes.text, + this.context.isInAParentText === true && classes.textHasAncestor, + numberOfLines === 1 && classes.textOneLine, + numberOfLines > 1 && classes.textMultiLine + ); // 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, + numberOfLines > 1 && { WebkitLineClamp: numberOfLines }, selectable === false && styles.notSelectable, - numberOfLines === 1 && styles.singleLineStyle, onPress && styles.pressable ]; @@ -97,41 +103,45 @@ class Text extends Component<*> { } } -const styles = StyleSheet.create({ - initial: { +const classes = css.create({ + text: { borderWidth: 0, boxSizing: 'border-box', - color: 'inherit', + color: 'black', display: 'inline', - fontFamily: 'System', - fontSize: 14, - fontStyle: 'inherit', - fontVariant: ['inherit'], - fontWeight: 'inherit', - lineHeight: 'inherit', + font: '14px System', margin: 0, padding: 0, - textDecorationLine: 'none', whiteSpace: 'pre-wrap', wordWrap: 'break-word' }, - isInAParentText: { - // inherit parent font styles - fontFamily: 'inherit', - fontSize: 'inherit', + textHasAncestor: { + color: 'inherit', + font: 'inherit', whiteSpace: 'inherit' }, + textOneLine: { + maxWidth: '100%', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap' + }, + // See #13 + textMultiLine: { + display: '-webkit-box', + maxWidth: '100%', + overflow: 'hidden', + textOverflow: 'ellipsis', + WebkitBoxOrient: 'vertical' + } +}); + +const styles = StyleSheet.create({ notSelectable: { userSelect: 'none' }, pressable: { cursor: 'pointer' - }, - singleLineStyle: { - maxWidth: '100%', - overflow: 'hidden', - textOverflow: 'ellipsis', - whiteSpace: 'nowrap' } }); diff --git a/packages/react-native-web/src/exports/TextInput/index.js b/packages/react-native-web/src/exports/TextInput/index.js index c287ac0c..523a04bd 100644 --- a/packages/react-native-web/src/exports/TextInput/index.js +++ b/packages/react-native-web/src/exports/TextInput/index.js @@ -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'; @@ -146,8 +146,7 @@ class TextInput extends Component<*> { keyboardType: 'default', multiline: false, numberOfLines: 1, - secureTextEntry: false, - style: emptyObject + secureTextEntry: false }; static State = TextInputState; @@ -180,7 +179,6 @@ class TextInput extends Component<*> { multiline, numberOfLines, secureTextEntry, - style, /* eslint-disable */ blurOnSubmit, clearTextOnFocus, @@ -260,6 +258,7 @@ class TextInput extends Component<*> { // https://bugs.chromium.org/p/chromium/issues/detail?id=468153#c164 autoComplete: autoComplete === 'off' ? 'noop' : autoComplete, autoCorrect: autoCorrect ? 'on' : 'off', + className: classes.textinput, dir: 'auto', onBlur: normalizeEventHandler(this._handleBlur), onChange: normalizeEventHandler(this._handleChange), @@ -269,8 +268,7 @@ class TextInput extends Component<*> { onSelect: normalizeEventHandler(this._handleSelectionChange), readOnly: !editable, ref: this._setNode, - spellCheck: spellCheck != null ? spellCheck : autoCorrect, - style: [styles.initial, style] + spellCheck: spellCheck != null ? spellCheck : autoCorrect }); if (multiline) { @@ -418,18 +416,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, + font: '14px System', padding: 0, resize: 'none' } diff --git a/packages/react-native-web/src/exports/View/ViewPropTypes.js b/packages/react-native-web/src/exports/View/ViewPropTypes.js index fed0016b..57b603c5 100644 --- a/packages/react-native-web/src/exports/View/ViewPropTypes.js +++ b/packages/react-native-web/src/exports/View/ViewPropTypes.js @@ -37,6 +37,7 @@ export type ViewProps = { accessibilityTraits?: string | Array, accessible?: boolean, children?: any, + className?: string, hitSlop?: EdgeInsetsProp, importantForAccessibility?: 'auto' | 'yes' | 'no' | 'no-hide-descendants', nativeID?: string, diff --git a/packages/react-native-web/src/exports/View/index.js b/packages/react-native-web/src/exports/View/index.js index 2407c1fc..1a374b47 100644 --- a/packages/react-native-web/src/exports/View/index.js +++ b/packages/react-native-web/src/exports/View/index.js @@ -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 { const { isInAParentText } = this.context; + supportedProps.className = css.combine(this.props.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,38 @@ class View extends Component { } } -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' + zIndex: 0 }, // 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)); diff --git a/packages/react-native-web/src/modules/createDOMProps/__tests__/__snapshots__/index-test.js.snap b/packages/react-native-web/src/modules/createDOMProps/__tests__/__snapshots__/index-test.js.snap index 0ac71b9a..ad138400 100644 --- a/packages/react-native-web/src/modules/createDOMProps/__tests__/__snapshots__/index-test.js.snap +++ b/packages/react-native-web/src/modules/createDOMProps/__tests__/__snapshots__/index-test.js.snap @@ -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`] = `"css-reset-4rbku5"`; -exports[`modules/createDOMProps includes reset styles for "a" elements 1`] = `"rn-backgroundColor-1niwhzg rn-color-homxoj rn-textDecorationLine-13wfysu"`; +exports[`modules/createDOMProps includes base reset style for browser-styled elements 2`] = `"css-reset-4rbku5"`; -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-1kfwfc5 rn-fontWeight-gul640 rn-lineHeight-t9a87b rn-textAlign-1ttztb7"`; +exports[`modules/createDOMProps includes base reset style for browser-styled elements 3`] = `"css-reset-4rbku5"`; -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`] = `"css-reset-4rbku5"`; + +exports[`modules/createDOMProps includes cursor style for pressable roles 1`] = `"css-cursor-18t94o4"`; + +exports[`modules/createDOMProps includes cursor style for pressable roles 2`] = `"css-cursor-18t94o4"`; diff --git a/packages/react-native-web/src/modules/createDOMProps/__tests__/index-test.js b/packages/react-native-web/src/modules/createDOMProps/__tests__/index-test.js index 12d0c06a..0946674b 100644 --- a/packages/react-native-web/src/modules/createDOMProps/__tests__/index-test.js +++ b/packages/react-native-web/src/modules/createDOMProps/__tests__/index-test.js @@ -193,23 +193,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(); }); }); diff --git a/packages/react-native-web/src/modules/createDOMProps/index.js b/packages/react-native-web/src/modules/createDOMProps/index.js index 841dc5d2..35ac12a3 100644 --- a/packages/react-native-web/src/modules/createDOMProps/index.js +++ b/packages/react-native-web/src/modules/createDOMProps/index.js @@ -8,42 +8,25 @@ */ import AccessibilityUtil from '../AccessibilityUtil'; +import css from '../../exports/StyleSheet/css'; import StyleSheet from '../../exports/StyleSheet'; import styleResolver from '../../exports/StyleSheet/styleResolver'; const emptyObject = {}; -const resetStyles = StyleSheet.create({ - ariaButton: { +// Reset styles for heading, link, and list DOM elements +const classes = css.create({ + reset: { + backgroundColor: 'transparent', + color: 'inherit', + font: 'inherit', + listStyle: 'none', + margin: 0, + textAlign: 'inherit', + textDecoration: 'none' + }, + cursor: { 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' } }); @@ -129,6 +112,8 @@ const createDOMProps = (component, props, styleResolver) => { importantForAccessibility !== 'no-hide-descendants'; if ( role === 'link' || + component === 'a' || + component === 'button' || component === 'input' || component === 'select' || component === 'textarea' @@ -152,24 +137,49 @@ 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 = classes.cursor; + } + // style reset various elements (not all are used internally) + if ( + component === 'a' || + component === 'button' || + component === 'li' || + component === 'ul' || + role === 'heading' + ) { + c = classes.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) {