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 9064d7b4..36cb1526 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 @@ -15,35 +15,11 @@ html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlig body{margin:0;} button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;} input::-webkit-inner-spin-button,input::-webkit-outer-spin-button,input::-webkit-search-cancel-button,input::-webkit-search-decoration,input::-webkit-search-results-button,input::-webkit-search-results-decoration{display:none;} +.rn-reset{background-color:transparent;color:inherit;font:inherit;list-style:none;margin:0;text-align:inherit;text-decoration:none;} +.rn-pointer{cursor:pointer;} } .rn-pointerEvents-12vffkv > *{pointer-events:auto} .rn-pointerEvents-12vffkv{pointer-events:none !important} -.rn-alignItems-1oszu61{-ms-flex-align:stretch;-webkit-align-items:stretch;-webkit-box-align:stretch;align-items:stretch} -.rn-borderTopStyle-1efd50x{border-top-style:solid} -.rn-borderRightStyle-14skgim{border-right-style:solid} -.rn-borderBottomStyle-rull8r{border-bottom-style:solid} -.rn-borderLeftStyle-mm0ijv{border-left-style:solid} -.rn-borderTopWidth-13yce4e{border-top-width:0px} -.rn-borderRightWidth-fnigne{border-right-width:0px} -.rn-borderBottomWidth-ndvcnb{border-bottom-width:0px} -.rn-borderLeftWidth-gxnn5r{border-left-width:0px} -.rn-boxSizing-deolkf{box-sizing:border-box} -.rn-display-6koalj{display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex} -.rn-flexShrink-1qe8dj5{-ms-flex-negative:0;-webkit-flex-shrink:0;flex-shrink:0} -.rn-flexBasis-1mlwlqe{-ms-flex-preferred-size:auto;-webkit-flex-basis:auto;flex-basis:auto} -.rn-flexDirection-eqz5dr{-ms-flex-direction:column;-webkit-box-direction:normal;-webkit-box-orient:vertical;-webkit-flex-direction:column;flex-direction:column} -.rn-marginTop-1mnahxq{margin-top:0px} -.rn-marginRight-61z16t{margin-right:0px} -.rn-marginBottom-p1pxzi{margin-bottom:0px} -.rn-marginLeft-11wrixw{margin-left:0px} -.rn-minHeight-ifefl9{min-height:0px} -.rn-minWidth-bcqeeo{min-width:0px} -.rn-paddingTop-wk8lta{padding-top:0px} -.rn-paddingRight-9aemit{padding-right:0px} -.rn-paddingBottom-1mdbw0j{padding-bottom:0px} -.rn-paddingLeft-gy4na3{padding-left:0px} -.rn-position-bnwqim{position:relative} -.rn-zIndex-1lgpqti{z-index:0} .rn-flexGrow-16y2uox{-ms-flex-positive:1;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1} .rn-flexShrink-1wbh5a2{-ms-flex-negative:1;-webkit-flex-shrink:1;flex-shrink:1} .rn-flexBasis-1ro0kt6{-ms-flex-preferred-size:0%;-webkit-flex-basis:0%;flex-basis:0%}" @@ -64,5 +40,11 @@ html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlig body{margin:0;} button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;} input::-webkit-inner-spin-button,input::-webkit-outer-spin-button,input::-webkit-search-cancel-button,input::-webkit-search-decoration,input::-webkit-search-results-button,input::-webkit-search-results-decoration{display:none;} -}" +.rn-reset{background-color:transparent;color:inherit;font:inherit;list-style:none;margin:0;text-align:inherit;text-decoration:none;} +.rn-pointer{cursor:pointer;} +} +.rn-view-1d2if7t{-ms-flex-align:stretch;-ms-flex-direction:column;-ms-flex-negative:0;-ms-flex-preferred-size:auto;-webkit-align-items:stretch;-webkit-box-align:stretch;-webkit-box-direction:normal;-webkit-box-orient:vertical;-webkit-flex-basis:auto;-webkit-flex-direction:column;-webkit-flex-shrink:0;align-items:stretch;background-color:transparent;border:0 solid black;box-sizing:border-box;color:inherit;display:-webkit-box;display:-moz-box;display:-ms-flexbox;display:-webkit-flex;display:flex;flex-basis:auto;flex-direction:column;flex-shrink:0;font:inherit;list-style:none;margin:0px;min-height:0px;min-width:0px;padding:0px;position:relative;text-align:inherit;text-decoration:none;z-index:0} +.rn-hitSlop-1be6dej{bottom:0px;left:0px;position:absolute;right:0px;top:0px;z-index:-1} +.rn-text-1ogs4z5{background-color:transparent;border-width:0px;box-sizing:border-box;color:inherit;display:inline;font:14px system-ui, -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", Roboto, Ubuntu, \\"Helvetica Neue\\", sans-serif;margin:0px;padding:0px;text-align:inherit;text-decoration:none;white-space:pre-wrap;word-wrap:break-word} +.rn-textinput-wwmj4d{-moz-appearance:textfield;-webkit-appearance:none;background-color:transparent;border-radius:0px;border:0 solid black;box-sizing:border-box;font-family:14px System;padding:0px;resize:none}" `; diff --git a/packages/react-native-web/src/exports/StyleSheet/StyleSheetManager.js b/packages/react-native-web/src/exports/StyleSheet/StyleSheetManager.js index 757cafc5..b13bf3b3 100644 --- a/packages/react-native-web/src/exports/StyleSheet/StyleSheetManager.js +++ b/packages/react-native-web/src/exports/StyleSheet/StyleSheetManager.js @@ -15,9 +15,9 @@ import WebStyleSheet from './WebStyleSheet'; const emptyObject = {}; const STYLE_ELEMENT_ID = 'react-native-stylesheet'; -const createClassName = (prop, value) => { - const hashed = hash(prop + normalizeValue(value)); - return process.env.NODE_ENV !== 'production' ? `rn-${prop}-${hashed}` : `rn-${hashed}`; +const createClassName = (name, value) => { + const hashed = hash(name + normalizeValue(value)); + return process.env.NODE_ENV !== 'production' ? `rn-${name}-${hashed}` : `rn-${hashed}`; }; const normalizeValue = value => (typeof value === 'object' ? JSON.stringify(value) : value); @@ -69,6 +69,13 @@ export default class StyleSheetManager { return className; } + injectRule(name, body: string): void { + const className = createClassName(name, body); + const rule = `.${className}{${body}}`; + this._sheet.insertRuleOnce(rule); + return className; + } + _addToCache(className, prop, value) { const cache = this._cache; if (!cache.byProp[prop]) { 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__/StyleSheetManager-test.js.snap b/packages/react-native-web/src/exports/StyleSheet/__tests__/__snapshots__/StyleSheetManager-test.js.snap index 3142a035..79d5b49f 100644 --- a/packages/react-native-web/src/exports/StyleSheet/__tests__/__snapshots__/StyleSheetManager-test.js.snap +++ b/packages/react-native-web/src/exports/StyleSheet/__tests__/__snapshots__/StyleSheetManager-test.js.snap @@ -10,6 +10,8 @@ html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlig body{margin:0;} button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;} input::-webkit-inner-spin-button,input::-webkit-outer-spin-button,input::-webkit-search-cancel-button,input::-webkit-search-decoration,input::-webkit-search-results-button,input::-webkit-search-results-decoration{display:none;} +.rn-reset{background-color:transparent;color:inherit;font:inherit;list-style:none;margin:0;text-align:inherit;text-decoration:none;} +.rn-pointer{cursor:pointer;} } .rn---test-property-ax3bxi{--test-property:test-value}", } 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 104aaf85..d16b7c8a 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%' diff --git a/packages/react-native-web/src/exports/StyleSheet/constants.js b/packages/react-native-web/src/exports/StyleSheet/constants.js new file mode 100644 index 00000000..619bc315 --- /dev/null +++ b/packages/react-native-web/src/exports/StyleSheet/constants.js @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2016-present, Nicolas Gallagher. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict + */ + +export const monospaceFontStack = 'monospace, monospace'; + +export const systemFontStack = + 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, "Helvetica Neue", sans-serif'; diff --git a/packages/react-native-web/src/exports/StyleSheet/createReactDOMStyle.js b/packages/react-native-web/src/exports/StyleSheet/createReactDOMStyle.js index 1eb2bed3..0598ba46 100644 --- a/packages/react-native-web/src/exports/StyleSheet/createReactDOMStyle.js +++ b/packages/react-native-web/src/exports/StyleSheet/createReactDOMStyle.js @@ -7,6 +7,7 @@ * @noflow */ +import { monospaceFontStack, systemFontStack } from './constants'; import normalizeColor from '../../modules/normalizeColor'; import normalizeValue from './normalizeValue'; import resolveShadowValue from './resolveShadowValue'; @@ -54,18 +55,6 @@ const colorProps = { color: true }; -const borderWidthProps = { - borderWidth: true, - borderTopWidth: true, - borderRightWidth: true, - borderBottomWidth: true, - borderLeftWidth: true -}; - -const monospaceFontStack = 'monospace, monospace'; -const systemFontStack = - 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, "Helvetica Neue", sans-serif'; - const alphaSortProps = propsArray => propsArray.sort((a, b) => { if (a < b) { @@ -167,12 +156,6 @@ const createReducer = (style, styleProps) => { return (resolvedStyle, prop) => { let value = normalizeValue(prop, style[prop]); - // Make sure the default border width is explicitly set to '0' to avoid - // falling back to any unwanted user-agent styles. - if (borderWidthProps[prop]) { - value = value == null ? normalizeValue(null, 0) : value; - } - // Normalize color values if (colorProps[prop]) { value = normalizeColor(value); @@ -203,21 +186,6 @@ const createReducer = (style, styleProps) => { break; } - case 'display': { - resolvedStyle.display = value; - // A flex container in React Native has these defaults which should be - // set only if there is no otherwise supplied flex style. - if (style.display === 'flex' && style.flex == null) { - if (style.flexShrink == null) { - resolvedStyle.flexShrink = 0; - } - if (style.flexBasis == null) { - resolvedStyle.flexBasis = 'auto'; - } - } - break; - } - // The 'flex' property value in React Native must be a positive integer, // 0, or -1. case 'flex': { diff --git a/packages/react-native-web/src/exports/StyleSheet/createRuleBlock.js b/packages/react-native-web/src/exports/StyleSheet/createRuleBlock.js index 34e5b5c3..d0a89a2f 100644 --- a/packages/react-native-web/src/exports/StyleSheet/createRuleBlock.js +++ b/packages/react-native-web/src/exports/StyleSheet/createRuleBlock.js @@ -8,7 +8,6 @@ */ import hyphenateStyleName from 'hyphenate-style-name'; -import mapKeyValue from '../../modules/mapKeyValue'; import normalizeValue from './normalizeValue'; import prefixStyles from '../../modules/prefixStyles'; @@ -27,9 +26,15 @@ const createDeclarationString = (prop, val) => { * createRuleBlock({ width: 20, color: 'blue' }); * // => 'color:blue;width:20px' */ -const createRuleBlock = style => - mapKeyValue(prefixStyles(style), createDeclarationString) - .sort() - .join(';'); +const createRuleBlock = style => { + const prefixedStyle = prefixStyles(style); + return ( + Object.keys(prefixedStyle) + .map(prop => createDeclarationString(prop, prefixedStyle[prop])) + // put short-form and vendor prefixed properties first + .sort() + .join(';') + ); +}; export default createRuleBlock; 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..bbf1beb5 --- /dev/null +++ b/packages/react-native-web/src/exports/StyleSheet/css.js @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2016-present, Nicolas Gallagher. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @noflow + */ + +import createRuleBlock from './createRuleBlock'; +import styleResolver from './styleResolver'; +import { systemFontStack } from './constants'; + +const css = { + create(rules) { + const result = {}; + Object.keys(rules).forEach(key => { + const rule = rules[key]; + if (rule.font && rule.font.indexOf('System') > -1) { + rule.font = rule.font.replace('System', systemFontStack); + } + if (rule.fontFamily === 'System') { + rule.fontFamily = systemFontStack; + } + const cssRule = createRuleBlock(rule); + const className = styleResolver.styleSheetManager.injectRule(key, cssRule); + result[key] = className; + }); + return result; + } +}; + +export default css; diff --git a/packages/react-native-web/src/exports/StyleSheet/initialRules.js b/packages/react-native-web/src/exports/StyleSheet/initialRules.js index ebb289af..1ca82cc4 100644 --- a/packages/react-native-web/src/exports/StyleSheet/initialRules.js +++ b/packages/react-native-web/src/exports/StyleSheet/initialRules.js @@ -12,13 +12,34 @@ const safeRule = rule => `@media all{\n${rule}\n}`; const resets = [ // minimal top-level reset - 'html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0);}', + 'html{' + + '-ms-text-size-adjust:100%;' + + '-webkit-text-size-adjust:100%;' + + '-webkit-tap-highlight-color:rgba(0,0,0,0);' + + '}', 'body{margin:0;}', // minimal form pseudo-element reset 'button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;}', - 'input::-webkit-inner-spin-button,input::-webkit-outer-spin-button,' + - 'input::-webkit-search-cancel-button,input::-webkit-search-decoration,' + - 'input::-webkit-search-results-button,input::-webkit-search-results-decoration{display:none;}' + 'input::-webkit-inner-spin-button,' + + 'input::-webkit-outer-spin-button,' + + 'input::-webkit-search-cancel-button,' + + 'input::-webkit-search-decoration,' + + 'input::-webkit-search-results-button,' + + 'input::-webkit-search-results-decoration{' + + 'display:none;' + + '}', + // Reset styles for heading, link, and list DOM elements + '.rn-reset{' + + 'background-color:transparent;' + + 'color:inherit;' + + 'font:inherit;' + + 'list-style:none;' + + 'margin:0;' + + 'text-align:inherit;' + + 'text-decoration:none;' + + '}', + // For pressable elements + '.rn-pointer{cursor:pointer;}' ]; const reset = [safeRule(resets.join('\n'))]; 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 4ff86733..5a04dbfa 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..6da4549d 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,10 +66,10 @@ class Text extends Component<*> { otherProps.onKeyDown = this._createEnterHandler(onPress); } + otherProps.className = classes.text; // allow browsers to automatically infer the language writing direction otherProps.dir = dir !== undefined ? dir : 'auto'; otherProps.style = [ - styles.initial, this.context.isInAParentText === true && styles.isInAParentText, style, selectable === false && styles.notSelectable, @@ -97,24 +98,24 @@ class Text extends Component<*> { } } -const styles = StyleSheet.create({ - initial: { +const classes = css.create({ + text: { + backgroundColor: 'transparent', borderWidth: 0, boxSizing: 'border-box', color: 'inherit', display: 'inline', - fontFamily: 'System', - fontSize: 14, - fontStyle: 'inherit', - fontVariant: ['inherit'], - fontWeight: 'inherit', - lineHeight: 'inherit', + font: '14px System', margin: 0, padding: 0, - textDecorationLine: 'none', + textAlign: 'inherit', + textDecoration: 'none', whiteSpace: 'pre-wrap', wordWrap: 'break-word' - }, + } +}); + +const styles = StyleSheet.create({ isInAParentText: { // inherit parent font styles fontFamily: 'inherit', diff --git a/packages/react-native-web/src/exports/TextInput/index.js b/packages/react-native-web/src/exports/TextInput/index.js index 71a7bbdc..2e279aa4 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'; @@ -256,6 +256,7 @@ class TextInput extends Component<*> { Object.assign(otherProps, { autoCorrect: autoCorrect ? 'on' : 'off', + className: classes.textinput, dir: 'auto', onBlur: normalizeEventHandler(this._handleBlur), onChange: normalizeEventHandler(this._handleChange), @@ -266,7 +267,7 @@ class TextInput extends Component<*> { readOnly: !editable, ref: this._setNode, spellCheck: spellCheck != null ? spellCheck : autoCorrect, - style: [styles.initial, style] + style }); if (multiline) { @@ -428,18 +429,15 @@ class TextInput extends Component<*> { }; } -const styles = StyleSheet.create({ - initial: { +const classes = css.create({ + textinput: { MozAppearance: 'textfield', WebkitAppearance: 'none', backgroundColor: 'transparent', - borderColor: 'black', + border: '0 solid black', borderRadius: 0, - borderStyle: 'solid', - borderWidth: 0, boxSizing: 'border-box', - fontFamily: 'System', - fontSize: 14, + fontFamily: '14px System', padding: 0, resize: 'none' } diff --git a/packages/react-native-web/src/exports/View/index.js b/packages/react-native-web/src/exports/View/index.js index 2407c1fc..c6b3b1bb 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 = classes.view; supportedProps.style = StyleSheet.compose( - styles.initial, - StyleSheet.compose(isInAParentText && styles.inline, this.props.style) + isInAParentText && styles.inline, + this.props.style ); if (hitSlop) { const hitSlopStyle = calculateHitSlopStyle(hitSlop); - const hitSlopChild = createElement('span', { style: [styles.hitSlop, hitSlopStyle] }); + const hitSlopChild = createElement('span', { + className: classes.hitslop, + style: hitSlopStyle + }); supportedProps.children = React.Children.toArray([hitSlopChild, supportedProps.children]); } @@ -66,32 +71,45 @@ class View extends Component { } } -const styles = StyleSheet.create({ - // https://github.com/facebook/css-layout#default-values - initial: { +const classes = css.create({ + view: { alignItems: 'stretch', - borderWidth: 0, - borderStyle: 'solid', + border: '0 solid black', boxSizing: 'border-box', display: 'flex', + flexBasis: 'auto', flexDirection: 'column', + flexShrink: 0, margin: 0, + minHeight: 0, + minWidth: 0, padding: 0, position: 'relative', zIndex: 0, - // fix flexbox bugs - minHeight: 0, - minWidth: 0 - }, - inline: { - display: 'inline-flex' + // resets for if View is rendered as a link or list DOM element + backgroundColor: 'transparent', + color: 'inherit', + font: 'inherit', + listStyle: 'none', + textAlign: 'inherit', + textDecoration: 'none' }, // this zIndex-ordering positions the hitSlop above the View but behind // its children hitSlop: { - ...StyleSheet.absoluteFillObject, + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, zIndex: -1 } }); +const styles = StyleSheet.create({ + inline: { + display: 'inline-flex' + } +}); + export default applyLayout(applyNativeMethods(View)); 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 c392263b..742ae014 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`] = `"rn-reset"`; -exports[`modules/createDOMProps includes reset styles for "a" elements 1`] = `"rn-backgroundColor-1niwhzg rn-color-homxoj rn-textDecoration-bauka4"`; +exports[`modules/createDOMProps includes base reset style for browser-styled elements 2`] = `"rn-reset"`; -exports[`modules/createDOMProps includes reset styles for "button" elements 1`] = `"rn-appearance-30o5oe rn-backgroundColor-1niwhzg rn-color-homxoj rn-fontFamily-poiln3 rn-fontSize-7cikom rn-fontStyle-o11vmf rn-fontVariant-ebii48 rn-fontWeight-gul640 rn-lineHeight-t9a87b rn-textAlign-1ttztb7"`; +exports[`modules/createDOMProps includes base reset style for browser-styled elements 3`] = `"rn-reset"`; -exports[`modules/createDOMProps includes reset styles for "ul" elements 1`] = `"rn-listStyle-1ebb2ja"`; +exports[`modules/createDOMProps includes base reset style for browser-styled elements 4`] = `"rn-reset"`; + +exports[`modules/createDOMProps includes cursor style for pressable roles 1`] = `"rn-pointer"`; + +exports[`modules/createDOMProps includes cursor style for pressable roles 2`] = `"rn-pointer"`; 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 9385c1cd..94acc511 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 @@ -200,23 +200,15 @@ describe('modules/createDOMProps', () => { expect(props.rel).toMatchSnapshot(); }); - test('includes reset styles for "a" elements', () => { - const props = createDOMProps('a'); - expect(props.className).toMatchSnapshot(); + test('includes cursor style for pressable roles', () => { + expect(createDOMProps('span', { accessibilityRole: 'link' }).className).toMatchSnapshot(); + expect(createDOMProps('span', { accessibilityRole: 'button' }).className).toMatchSnapshot(); }); - test('includes reset styles for "button" elements', () => { - const props = createDOMProps('button'); - expect(props.className).toMatchSnapshot(); - }); - - test('includes cursor style for "button" role', () => { - const props = createDOMProps('span', { accessibilityRole: 'button' }); - expect(props.className).toMatchSnapshot(); - }); - - test('includes reset styles for "ul" elements', () => { - const props = createDOMProps('ul'); - expect(props.className).toMatchSnapshot(); + test('includes base reset style for browser-styled elements', () => { + expect(createDOMProps('a').className).toMatchSnapshot(); + expect(createDOMProps('button').className).toMatchSnapshot(); + expect(createDOMProps('li').className).toMatchSnapshot(); + expect(createDOMProps('ul').className).toMatchSnapshot(); }); }); diff --git a/packages/react-native-web/src/modules/createDOMProps/index.js b/packages/react-native-web/src/modules/createDOMProps/index.js index 1dce93cb..4309fa77 100644 --- a/packages/react-native-web/src/modules/createDOMProps/index.js +++ b/packages/react-native-web/src/modules/createDOMProps/index.js @@ -13,40 +13,6 @@ import styleResolver from '../../exports/StyleSheet/styleResolver'; const emptyObject = {}; -const resetStyles = StyleSheet.create({ - ariaButton: { - cursor: 'pointer' - }, - button: { - appearance: 'none', - backgroundColor: 'transparent', - color: 'inherit', - fontFamily: 'inherit', - fontSize: 'inherit', - fontStyle: 'inherit', - fontVariant: ['inherit'], - fontWeight: 'inherit', - lineHeight: 'inherit', - textAlign: 'inherit' - }, - heading: { - fontFamily: 'inherit', - fontSize: 'inherit', - fontStyle: 'inherit', - fontVariant: ['inherit'], - fontWeight: 'inherit', - lineHeight: 'inherit' - }, - link: { - backgroundColor: 'transparent', - color: 'inherit', - textDecorationLine: 'none' - }, - list: { - listStyle: 'none' - } -}); - const pointerEventsStyles = StyleSheet.create({ auto: { pointerEvents: 'auto' @@ -123,6 +89,8 @@ const createDOMProps = (component, props, styleResolver) => { importantForAccessibility !== 'no-hide-descendants'; if ( role === 'link' || + component === 'a' || + component === 'button' || component === 'input' || component === 'select' || component === 'textarea' @@ -146,30 +114,53 @@ const createDOMProps = (component, props, styleResolver) => { // STYLE // Resolve React Native styles to optimized browser equivalent - const reactNativeStyle = [ - component === 'a' && resetStyles.link, - component === 'button' && resetStyles.button, - role === 'heading' && resetStyles.heading, - component === 'ul' && resetStyles.list, - role === 'button' && !disabled && resetStyles.ariaButton, + const reactNativeStyle = StyleSheet.compose( pointerEvents && pointerEventsStyles[pointerEvents], - providedStyle, - placeholderTextColor && { placeholderTextColor } - ]; + StyleSheet.compose( + providedStyle, + placeholderTextColor && { placeholderTextColor } + ) + ); const { className, style } = styleResolver(reactNativeStyle); - if (className && className.constructor === String) { - domProps.className = props.className ? `${props.className} ${className}` : className; - } if (style) { domProps.style = style; } + // CLASSNAME + // Apply static style resets + let c; + // style interactive elements for mouse and mobile browsers + if ((role === 'button' || role === 'link') && !disabled) { + c = 'rn-pointer'; + } + // style reset various elements (not all are used internally) + if ( + component === 'a' || + component === 'button' || + component === 'li' || + component === 'ul' || + role === 'heading' + ) { + c = 'rn-reset' + (c != null ? ' ' + c : ''); + } + // style from createElement use + if (props.className != null) { + c = props.className + (c != null ? ' ' + c : ''); + } + // style from React Native StyleSheets + if (className != null && className !== '') { + c = (c != null ? c + ' ' : '') + className; + } + if (c != null) { + domProps.className = c; + } + // OTHER // Native element ID if (nativeID && nativeID.constructor === String) { domProps.id = nativeID; } - // Link security and automation test ids + // Link security if (component === 'a' && domProps.target === '_blank') { domProps.rel = `${domProps.rel || ''} noopener noreferrer`; } diff --git a/packages/react-native-web/src/modules/mapKeyValue/index.js b/packages/react-native-web/src/modules/mapKeyValue/index.js deleted file mode 100644 index f019a3c6..00000000 --- a/packages/react-native-web/src/modules/mapKeyValue/index.js +++ /dev/null @@ -1,14 +0,0 @@ -const hasOwnProperty = Object.prototype.hasOwnProperty; - -const mapKeyValue = (obj, fn) => { - const result = []; - for (const key in obj) { - if (hasOwnProperty.call(obj, key)) { - const r = fn(key, obj[key]); - r && result.push(r); - } - } - return result; -}; - -export default mapKeyValue;