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) {