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 | 90 ++++++++++--------- 16 files changed, 253 insertions(+), 205 deletions(-) create mode 100644 packages/react-native-web/src/exports/StyleSheet/css.js diff --git a/README.md b/README.md index b11b8fc0c..12358ac3c 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 8bcfdeacb..d285ae2b9 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 cf72f098c..2597bf953 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 b94e3aeda..29ecb37a6 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 0bf5e7782..dbeac739d 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 1eb78a87a..9ae47a2f8 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 e41890d58..d33aa9402 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 000000000..4606b64e0 --- /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 d0d30875a..e53eceadf 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 0610649c2..841589c22 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 c287ac0c7..523a04bd5 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 fed0016bb..57b603c50 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 2407c1fc3..1a374b477 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 0ac71b9a2..ad1384004 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 12d0c06a2..0946674b0 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 841dc5d28..35ac12a30 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: { - cursor: 'pointer' - }, - button: { - appearance: 'none', +// Reset styles for heading, link, and list DOM elements +const classes = css.create({ + reset: { 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' + font: 'inherit', + listStyle: 'none', + margin: 0, + textAlign: 'inherit', + textDecoration: 'none' }, - link: { - backgroundColor: 'transparent', - color: 'inherit', - textDecorationLine: 'none' - }, - list: { - listStyle: 'none' + cursor: { + cursor: 'pointer' } }); @@ -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) {