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