diff --git a/packages/react-native-web/src/exports/StyleSheet/StyleSheet.js b/packages/react-native-web/src/exports/StyleSheet/StyleSheet.js index 8a6b42ed..36870538 100644 --- a/packages/react-native-web/src/exports/StyleSheet/StyleSheet.js +++ b/packages/react-native-web/src/exports/StyleSheet/StyleSheet.js @@ -4,10 +4,9 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @noflow + * @flow */ -import StyleSheetValidation from './StyleSheetValidation'; import ReactNativePropRegistry from '../../modules/ReactNativePropRegistry'; import flattenStyle from './flattenStyle'; @@ -23,18 +22,34 @@ const absoluteFill = ReactNativePropRegistry.register(absoluteFillObject); const StyleSheet = { absoluteFill, absoluteFillObject, - compose(style1, style2) { + compose(style1: any, style2: any) { + if (process.env.NODE_ENV !== 'production') { + /* eslint-disable prefer-rest-params */ + const len = arguments.length; + if (len > 2) { + const readableStyles = [...arguments].map(a => flattenStyle(a)); + throw new Error( + `StyleSheet.compose() only accepts 2 arguments, received ${len}: ${JSON.stringify( + readableStyles + )}` + ); + } + /* eslint-enable prefer-rest-params */ + } + if (style1 && style2) { return [style1, style2]; } else { return style1 || style2; } }, - create(styles) { + create(styles: Object) { const result = {}; Object.keys(styles).forEach(key => { if (process.env.NODE_ENV !== 'production') { - StyleSheetValidation.validateStyle(key, styles); + const validate = require('./validate'); + const interopValidate = validate.default ? validate.default : validate; + interopValidate(key, styles); } const id = styles[key] && ReactNativePropRegistry.register(styles[key]); result[key] = id; diff --git a/packages/react-native-web/src/exports/StyleSheet/StyleSheetValidation.js b/packages/react-native-web/src/exports/StyleSheet/StyleSheetValidation.js deleted file mode 100644 index 29ecb37a..00000000 --- a/packages/react-native-web/src/exports/StyleSheet/StyleSheetValidation.js +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Copyright (c) 2016-present, Nicolas Gallagher. - * Copyright (c) 2015-present, Facebook, Inc. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import ImageStylePropTypes from '../Image/ImageStylePropTypes'; -import TextInputStylePropTypes from '../TextInput/TextInputStylePropTypes'; -import TextStylePropTypes from '../Text/TextStylePropTypes'; -import ViewStylePropTypes from '../View/ViewStylePropTypes'; -import warning from 'fbjs/lib/warning'; -import { number, oneOf, string } from 'prop-types'; - -// Hardcoded because this is a legit case but we don't want to load it from -// a private API. We might likely want to unify style sheet creation with how it -// is done in the DOM so this might move into React. I know what I'm doing so -// plz don't fire me. -const ReactPropTypesSecret = 'SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED'; - -class StyleSheetValidation { - static validateStyleProp(prop: string, style: Object, caller: string) { - if (process.env.NODE_ENV !== 'production') { - const value = style[prop]; - - const isCustomProperty = prop.indexOf('--') === 0; - if (isCustomProperty) return; - - if (allStylePropTypes[prop] === undefined) { - const message1 = '"' + prop + '" is not a valid style property.'; - const message2 = - '\nValid style props: ' + - JSON.stringify(Object.keys(allStylePropTypes).sort(), null, ' '); - styleError(message1, style, caller, message2); - } else if (typeof value === 'string' && value.indexOf('!important') > -1) { - styleError( - `Invalid value of "${value}" set on prop "${prop}". Values cannot include "!important"`, - style, - caller - ); - } else { - const error = allStylePropTypes[prop]( - style, - prop, - caller, - 'prop', - null, - ReactPropTypesSecret - ); - if (error) { - styleError(error.message, style, caller); - } - } - } - } - - static validateStyle(name: string, styles: Object) { - if (process.env.NODE_ENV !== 'production') { - for (const prop in styles[name]) { - StyleSheetValidation.validateStyleProp(prop, styles[name], 'StyleSheet ' + name); - } - } - } - - static addValidStylePropTypes(stylePropTypes: Object) { - for (const key in stylePropTypes) { - allStylePropTypes[key] = stylePropTypes[key]; - } - } -} - -const styleError = function(message1, style, caller?, message2?) { - warning( - false, - message1 + - '\n' + - (caller || '<>') + - ': ' + - JSON.stringify(style, null, ' ') + - (message2 || '') - ); -}; - -const allStylePropTypes = {}; - -StyleSheetValidation.addValidStylePropTypes(ImageStylePropTypes); -StyleSheetValidation.addValidStylePropTypes(TextStylePropTypes); -StyleSheetValidation.addValidStylePropTypes(TextInputStylePropTypes); -StyleSheetValidation.addValidStylePropTypes(ViewStylePropTypes); - -StyleSheetValidation.addValidStylePropTypes({ - appearance: string, - borderCollapse: string, - borderSpacing: oneOf([number, string]), - clear: string, - cursor: string, - fill: string, - float: oneOf(['end', 'left', 'none', 'right', 'start']), - listStyle: string, - objectFit: oneOf(['fill', 'contain', 'cover', 'none', 'scale-down']), - objectPosition: string, - pointerEvents: string, - tableLayout: string -}); - -export default StyleSheetValidation; diff --git a/packages/react-native-web/src/exports/StyleSheet/__tests__/__snapshots__/compile-test.js.snap b/packages/react-native-web/src/exports/StyleSheet/__tests__/__snapshots__/compile-test.js.snap index 8c838305..d75088bc 100644 --- a/packages/react-native-web/src/exports/StyleSheet/__tests__/__snapshots__/compile-test.js.snap +++ b/packages/react-native-web/src/exports/StyleSheet/__tests__/__snapshots__/compile-test.js.snap @@ -114,14 +114,11 @@ to { left: 10px; } exports[`StyleSheet/compile inline converts style to inline styles 1`] = ` Object { - "WebkitFlexBasis": "auto", "WebkitFlexShrink": 1, "display": "flex", - "flexBasis": "auto", "flexShrink": 1, "marginLeft": "10px", "marginRight": "10px", "msFlexNegative": 1, - "msFlexPreferredSize": "auto", } `; diff --git a/packages/react-native-web/src/exports/StyleSheet/constants.js b/packages/react-native-web/src/exports/StyleSheet/constants.js index cca49e3d..f3783274 100644 --- a/packages/react-native-web/src/exports/StyleSheet/constants.js +++ b/packages/react-native-web/src/exports/StyleSheet/constants.js @@ -23,3 +23,27 @@ export const STYLE_GROUPS = { paddingVertical: 2.1 } }; + +export const STYLE_SHORT_FORM_EXPANSIONS = { + borderColor: ['borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor'], + borderRadius: [ + 'borderTopLeftRadius', + 'borderTopRightRadius', + 'borderBottomRightRadius', + 'borderBottomLeftRadius' + ], + borderStyle: ['borderTopStyle', 'borderRightStyle', 'borderBottomStyle', 'borderLeftStyle'], + borderWidth: ['borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth'], + margin: ['marginTop', 'marginRight', 'marginBottom', 'marginLeft'], + marginHorizontal: ['marginRight', 'marginLeft'], + marginVertical: ['marginTop', 'marginBottom'], + overflow: ['overflowX', 'overflowY'], + overscrollBehavior: ['overscrollBehaviorX', 'overscrollBehaviorY'], + padding: ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'], + paddingHorizontal: ['paddingRight', 'paddingLeft'], + paddingVertical: ['paddingTop', 'paddingBottom'] +}; + +export const MONOSPACE_FONT_STACK = 'monospace, monospace'; +export const SYSTEM_FONT_STACK = + '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 d33aa940..552f63a3 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 { MONOSPACE_FONT_STACK, SYSTEM_FONT_STACK, STYLE_SHORT_FORM_EXPANSIONS } from './constants'; import normalizeValueWithProperty from './normalizeValueWithProperty'; /** @@ -21,30 +22,6 @@ import normalizeValueWithProperty from './normalizeValueWithProperty'; */ const emptyObject = {}; -const styleShortFormProperties = { - borderColor: ['borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor'], - borderRadius: [ - 'borderTopLeftRadius', - 'borderTopRightRadius', - 'borderBottomRightRadius', - 'borderBottomLeftRadius' - ], - borderStyle: ['borderTopStyle', 'borderRightStyle', 'borderBottomStyle', 'borderLeftStyle'], - borderWidth: ['borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth'], - margin: ['marginTop', 'marginRight', 'marginBottom', 'marginLeft'], - marginHorizontal: ['marginRight', 'marginLeft'], - marginVertical: ['marginTop', 'marginBottom'], - overflow: ['overflowX', 'overflowY'], - overscrollBehavior: ['overscrollBehaviorX', 'overscrollBehaviorY'], - padding: ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'], - paddingHorizontal: ['paddingRight', 'paddingLeft'], - paddingVertical: ['paddingTop', 'paddingBottom'], - writingDirection: ['direction'] -}; - -const monospaceFontStack = 'monospace, monospace'; -const systemFontStack = - 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, "Helvetica Neue", sans-serif'; /** * Transform @@ -135,17 +112,17 @@ const createReactDOMStyle = style => { } case 'font': { - resolvedStyle[prop] = value.replace('System', systemFontStack); + resolvedStyle[prop] = value.replace('System', SYSTEM_FONT_STACK); break; } case 'fontFamily': { if (value.indexOf('System') > -1) { const stack = value.split(/,\s*/); - stack[stack.indexOf('System')] = systemFontStack; + stack[stack.indexOf('System')] = SYSTEM_FONT_STACK; resolvedStyle[prop] = stack.join(', '); } else if (value === 'monospace') { - resolvedStyle[prop] = monospaceFontStack; + resolvedStyle[prop] = MONOSPACE_FONT_STACK; } else { resolvedStyle[prop] = value; } @@ -177,8 +154,13 @@ const createReactDOMStyle = style => { break; } + case 'writingDirection': { + resolvedStyle.direction = value; + break; + } + default: { - const longFormProperties = styleShortFormProperties[prop]; + const longFormProperties = STYLE_SHORT_FORM_EXPANSIONS[prop]; if (longFormProperties) { longFormProperties.forEach((longForm, i) => { // The value of any longform property in the original styles takes diff --git a/packages/react-native-web/src/exports/StyleSheet/validate.js b/packages/react-native-web/src/exports/StyleSheet/validate.js new file mode 100644 index 00000000..b2fe07ca --- /dev/null +++ b/packages/react-native-web/src/exports/StyleSheet/validate.js @@ -0,0 +1,113 @@ +/** + * Copyright (c) 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-local + */ + +// import { STYLE_SHORT_FORM_EXPANSIONS } from './constants'; +import ImageStylePropTypes from '../Image/ImageStylePropTypes'; +import TextInputStylePropTypes from '../TextInput/TextInputStylePropTypes'; +import TextStylePropTypes from '../Text/TextStylePropTypes'; +import ViewStylePropTypes from '../View/ViewStylePropTypes'; +import warning from 'fbjs/lib/warning'; + +const validProperties = [ + ...Object.keys(ImageStylePropTypes), + ...Object.keys(TextInputStylePropTypes), + ...Object.keys(TextStylePropTypes), + ...Object.keys(ViewStylePropTypes), + 'appearance', + 'borderCollapse', + 'borderSpacing', + 'clear', + 'cursor', + 'fill', + 'float', + 'listStyle', + 'objectFit', + 'objectPosition', + 'pointerEvents', + 'placeholderTextColor', + 'tableLayout' +] + .sort() + .reduce((acc, curr) => { + acc[curr] = true; + return acc; + }, {}); + +const invalidShortforms = { + background: true, + borderBottom: true, + borderLeft: true, + borderRight: true, + borderTop: true, + font: true, + grid: true, + outline: true, + textDecoration: true +}; + +/* +const singleValueShortForms = Object.keys(STYLE_SHORT_FORM_EXPANSIONS).reduce((a, c) => { + a[c] = true; + return a; +}, {}); +*/ + +function error(message) { + warning(false, message); +} + +export default function validate(key: string, styles: Object) { + const obj = styles[key]; + for (const k in obj) { + const prop = k.trim(); + const value = obj[prop]; + const isValidProp = validProperties[prop]; + const isInvalidShorthand = invalidShortforms[prop]; + let isInvalid = false; + + if (value === null) { + continue; + } + + if (!isValidProp) { + let suggestion = ''; + if (prop === 'animation' || prop === 'animationName') { + suggestion = 'Did you mean "animationKeyframes"?'; + // } else if (prop === 'boxShadow') { + // suggestion = 'Did you mean "shadow{Color,Offset,Opacity,Radius}"?'; + } else if (prop === 'direction') { + suggestion = 'Did you mean "writingDirection"?'; + } else if (prop === 'verticalAlign') { + suggestion = 'Did you mean "textAlignVertical"?'; + } else if (isInvalidShorthand) { + suggestion = 'Please use long-form properties.'; + } + isInvalid = true; + error(`Invalid style property of "${prop}". ${suggestion}`); + } + + // else if (singleValueShortForms[prop]) { + // TODO: fix check + // if (typeof value === 'string' && value.indexOf(' ') > -1) { + // error( + // `Invalid style declaration "${prop}:${value}". This property must only specify a single value.` + // ); + // isInvalid = true; + // } + // } + else if (typeof value === 'string' && value.indexOf('!important') > -1) { + error(`Invalid style declaration "${prop}:${value}". Values cannot include "!important"`); + isInvalid = true; + } + + if (isInvalid) { + delete obj[k]; + } + } +}