From 4e3d8dbb025da892b0c58730e2b9926a8fed68b2 Mon Sep 17 00:00:00 2001 From: Nicolas Gallagher Date: Thu, 27 Jul 2017 16:46:08 -0700 Subject: [PATCH] [change] support CSS custom properties Update 'setValueForStyles' and style validation to support defining and using custom properties. Fix #516 --- .../storybook/1-components/View/ViewScreen.js | 7 + docs/storybook/ui-explorer/ExternalLink.js | 12 + docs/storybook/ui-explorer/StyleList.js | 10 +- docs/storybook/ui-explorer/UIExplorer.js | 7 +- docs/storybook/ui-explorer/index.js | 3 +- src/apis/AppState/index.js | 3 +- src/apis/StyleSheet/StyleSheetValidation.js | 26 ++- src/propTypes/ColorPropType.js | 3 +- src/propTypes/StyleSheetPropType.js | 10 +- src/vendor/dangerousStyleValue/index.js | 53 +++++ src/vendor/setValueForStyles/index.js | 208 +----------------- src/vendor/warnValidStyle/index.js | 170 ++++++++++++++ 12 files changed, 292 insertions(+), 220 deletions(-) create mode 100644 docs/storybook/ui-explorer/ExternalLink.js create mode 100644 src/vendor/dangerousStyleValue/index.js create mode 100644 src/vendor/warnValidStyle/index.js diff --git a/docs/storybook/1-components/View/ViewScreen.js b/docs/storybook/1-components/View/ViewScreen.js index 890e63f3..32be8b6b 100644 --- a/docs/storybook/1-components/View/ViewScreen.js +++ b/docs/storybook/1-components/View/ViewScreen.js @@ -14,6 +14,7 @@ import UIExplorer, { Code, Description, DocItem, + ExternalLink, Section, StyleList } from '../../ui-explorer'; @@ -291,6 +292,12 @@ const ViewScreen = () => ; const stylePropTypes = [ + { + label: 'web', + name: ( + Custom properties + ) + }, { name: 'alignContent', typeInfo: 'string' diff --git a/docs/storybook/ui-explorer/ExternalLink.js b/docs/storybook/ui-explorer/ExternalLink.js new file mode 100644 index 00000000..587b64d1 --- /dev/null +++ b/docs/storybook/ui-explorer/ExternalLink.js @@ -0,0 +1,12 @@ +/* eslint-disable react/prop-types */ + +/** + * @flow + */ + +import AppText from './AppText'; +import React from 'react'; + +const ExternalLink = props => ; + +export default ExternalLink; diff --git a/docs/storybook/ui-explorer/StyleList.js b/docs/storybook/ui-explorer/StyleList.js index 574a0e98..085857b2 100644 --- a/docs/storybook/ui-explorer/StyleList.js +++ b/docs/storybook/ui-explorer/StyleList.js @@ -16,10 +16,12 @@ const StyleList = ({ stylePropTypes }) => {name} - {': '} - - {typeInfo} - + {typeInfo ? ': ' : null} + {typeInfo + ? + {typeInfo} + + : null} )} ; diff --git a/docs/storybook/ui-explorer/UIExplorer.js b/docs/storybook/ui-explorer/UIExplorer.js index 00097c3b..ff0c1018 100644 --- a/docs/storybook/ui-explorer/UIExplorer.js +++ b/docs/storybook/ui-explorer/UIExplorer.js @@ -5,6 +5,7 @@ */ import AppText from './AppText'; +import ExternalLink from './ExternalLink'; import insertBetween from './insertBetween'; import React from 'react'; import { StyleSheet, View } from 'react-native'; @@ -22,14 +23,12 @@ export const Description = ({ children }) => const Divider = () => ; const SourceLink = ({ uri }) => - View source code on GitHub - ; + ; const UIExplorer = ({ children, description, sections, title, url }) => diff --git a/docs/storybook/ui-explorer/index.js b/docs/storybook/ui-explorer/index.js index 5a5e5d30..3bef8a88 100644 --- a/docs/storybook/ui-explorer/index.js +++ b/docs/storybook/ui-explorer/index.js @@ -5,10 +5,11 @@ import AppText from './AppText'; import Code from './Code'; import DocItem from './DocItem'; +import ExternalLink from './ExternalLink'; import Section from './Section'; import StyleList from './StyleList'; import TextList from './TextList'; import UIExplorer, { Description } from './UIExplorer'; export default UIExplorer; -export { AppText, Code, Description, DocItem, Section, StyleList, TextList }; +export { AppText, Code, Description, DocItem, ExternalLink, Section, StyleList, TextList }; diff --git a/src/apis/AppState/index.js b/src/apis/AppState/index.js index 52880c04..f3eef3d5 100644 --- a/src/apis/AppState/index.js +++ b/src/apis/AppState/index.js @@ -15,7 +15,8 @@ import findIndex from 'array-find-index'; import invariant from 'fbjs/lib/invariant'; // Android 4.4 browser -const isPrefixed = canUseDOM && !document.hasOwnProperty('hidden') && document.hasOwnProperty('webkitHidden'); +const isPrefixed = + canUseDOM && !document.hasOwnProperty('hidden') && document.hasOwnProperty('webkitHidden'); const EVENT_TYPES = ['change']; const VISIBILITY_CHANGE_EVENT = isPrefixed ? 'webkitvisibilitychange' : 'visibilitychange'; diff --git a/src/apis/StyleSheet/StyleSheetValidation.js b/src/apis/StyleSheet/StyleSheetValidation.js index 1e29d7b8..07de6958 100644 --- a/src/apis/StyleSheet/StyleSheetValidation.js +++ b/src/apis/StyleSheet/StyleSheetValidation.js @@ -26,23 +26,27 @@ const ReactPropTypesSecret = 'SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED'; export default class StyleSheetValidation { static validateStyleProp(prop, style, caller) { if (process.env.NODE_ENV !== 'production') { + 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); - } - const error = allStylePropTypes[prop]( - style, - prop, - caller, - 'prop', - null, - ReactPropTypesSecret - ); - if (error) { - styleError(error.message, style, caller); + } else { + const error = allStylePropTypes[prop]( + style, + prop, + caller, + 'prop', + null, + ReactPropTypesSecret + ); + if (error) { + styleError(error.message, style, caller); + } } } } diff --git a/src/propTypes/ColorPropType.js b/src/propTypes/ColorPropType.js index c0335689..07df65b6 100644 --- a/src/propTypes/ColorPropType.js +++ b/src/propTypes/ColorPropType.js @@ -35,7 +35,8 @@ const colorPropType = function(isRequired, props, propName, componentName, locat return; } - if (color === 'currentcolor' || color === 'inherit') { + // Web supports additional color keywords and custom property values + if (color === 'currentcolor' || color === 'inherit' || color.indexOf('var(') === 0) { return; } diff --git a/src/propTypes/StyleSheetPropType.js b/src/propTypes/StyleSheetPropType.js index b29d49f8..0e1b661e 100644 --- a/src/propTypes/StyleSheetPropType.js +++ b/src/propTypes/StyleSheetPropType.js @@ -19,7 +19,15 @@ function StyleSheetPropType(shape: { [key: string]: ReactPropsCheckType }): Reac if (props[propName]) { // Just make a dummy prop object with only the flattened style newProps = {}; - newProps[propName] = StyleSheet.flatten(props[propName]); + const flatStyle = StyleSheet.flatten(props[propName]); + // Remove custom properties from check + const nextStyle = Object.keys(flatStyle).reduce((acc, curr) => { + if (curr.indexOf('--') !== 0) { + acc[curr] = flatStyle[curr]; + } + return acc; + }, {}); + newProps[propName] = nextStyle; } return shapePropType(newProps, propName, componentName, location, ...rest); }; diff --git a/src/vendor/dangerousStyleValue/index.js b/src/vendor/dangerousStyleValue/index.js new file mode 100644 index 00000000..710c156c --- /dev/null +++ b/src/vendor/dangerousStyleValue/index.js @@ -0,0 +1,53 @@ +/* eslint-disable */ + +/** + * Copyright 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule dangerousStyleValue + */ + +import isUnitlessNumber from '../../modules/unitlessNumbers'; + +/** + * Convert a value into the proper css writable value. The style name `name` + * should be logical (no hyphens), as specified + * in `CSSProperty.isUnitlessNumber`. + * + * @param {string} name CSS property name such as `topMargin`. + * @param {*} value CSS property value such as `10px`. + * @return {string} Normalized style value with dimensions applied. + */ +function dangerousStyleValue(name, value, isCustomProperty) { + // Note that we've removed escapeTextForBrowser() calls here since the + // whole string will be escaped when the attribute is injected into + // the markup. If you provide unsafe user data here they can inject + // arbitrary CSS which may be problematic (I couldn't repro this): + // https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet + // http://www.thespanner.co.uk/2007/11/26/ultimate-xss-css-injection/ + // This is not an XSS hole but instead a potential CSS injection issue + // which has lead to a greater discussion about how we're going to + // trust URLs moving forward. See #2115901 + + var isEmpty = value == null || typeof value === 'boolean' || value === ''; + if (isEmpty) { + return ''; + } + + if ( + !isCustomProperty && + typeof value === 'number' && + value !== 0 && + !(isUnitlessNumber.hasOwnProperty(name) && isUnitlessNumber[name]) + ) { + return value + 'px'; // Presumes implicit 'px' suffix for unitless numbers + } + + return ('' + value).trim(); +} + +export default dangerousStyleValue; diff --git a/src/vendor/setValueForStyles/index.js b/src/vendor/setValueForStyles/index.js index dbc9131e..a88177f3 100644 --- a/src/vendor/setValueForStyles/index.js +++ b/src/vendor/setValueForStyles/index.js @@ -10,199 +10,8 @@ * */ -import unitlessNumbers from '../../modules/unitlessNumbers'; - -if (process.env.NODE_ENV !== 'production') { - var camelizeStyleName = require('fbjs/lib/camelizeStyleName'); - var warning = require('fbjs/lib/warning'); - - // 'msTransform' is correct, but the other prefixes should be capitalized - var badVendoredStyleNamePattern = /^(?:webkit|moz|o)[A-Z]/; - - // style values shouldn't contain a semicolon - var badStyleValueWithSemicolonPattern = /;\s*$/; - - var warnedStyleNames = {}; - var warnedStyleValues = {}; - var warnedForNaNValue = false; - - var warnHyphenatedStyleName = function(name, owner) { - if (warnedStyleNames.hasOwnProperty(name) && warnedStyleNames[name]) { - return; - } - - warnedStyleNames[name] = true; - process.env.NODE_ENV !== 'production' - ? warning( - false, - 'Unsupported style property %s. Did you mean %s?%s', - name, - camelizeStyleName(name), - checkRenderMessage(owner) - ) - : void 0; - }; - - var warnBadVendoredStyleName = function(name, owner) { - if (warnedStyleNames.hasOwnProperty(name) && warnedStyleNames[name]) { - return; - } - - warnedStyleNames[name] = true; - process.env.NODE_ENV !== 'production' - ? warning( - false, - 'Unsupported vendor-prefixed style property %s. Did you mean %s?%s', - name, - name.charAt(0).toUpperCase() + name.slice(1), - checkRenderMessage(owner) - ) - : void 0; - }; - - var warnStyleValueWithSemicolon = function(name, value, owner) { - if (warnedStyleValues.hasOwnProperty(value) && warnedStyleValues[value]) { - return; - } - - warnedStyleValues[value] = true; - process.env.NODE_ENV !== 'production' - ? warning( - false, - "Style property values shouldn't contain a semicolon.%s " + 'Try "%s: %s" instead.', - checkRenderMessage(owner), - name, - value.replace(badStyleValueWithSemicolonPattern, '') - ) - : void 0; - }; - - var warnStyleValueIsNaN = function(name, value, owner) { - if (warnedForNaNValue) { - return; - } - - warnedForNaNValue = true; - process.env.NODE_ENV !== 'production' - ? warning( - false, - '`NaN` is an invalid value for the `%s` css style property.%s', - name, - checkRenderMessage(owner) - ) - : void 0; - }; - - var checkRenderMessage = function(owner) { - if (owner) { - var name = owner.getName(); - if (name) { - return ' Check the render method of `' + name + '`.'; - } - } - return ''; - }; - - /** - * @param {string} name - * @param {*} value - * @param {ReactDOMComponent} component - */ - var warnValidStyle = function(name, value, component) { - var owner; - if (component) { - owner = component._currentElement._owner; - } - if (name.indexOf('-') > -1) { - warnHyphenatedStyleName(name, owner); - } else if (badVendoredStyleNamePattern.test(name)) { - warnBadVendoredStyleName(name, owner); - } else if (badStyleValueWithSemicolonPattern.test(value)) { - warnStyleValueWithSemicolon(name, value, owner); - } - - if (typeof value === 'number' && isNaN(value)) { - warnStyleValueIsNaN(name, value, owner); - } - }; -} - -var styleWarnings = {}; - -/** - * Convert a value into the proper css writable value. The style name `name` - * should be logical (no hyphens) - * - * @param {string} name CSS property name such as `topMargin`. - * @param {*} value CSS property value such as `10px`. - * @param {ReactDOMComponent} component - * @return {string} Normalized style value with dimensions applied. - */ -function dangerousStyleValue(name, value, component) { - // Note that we've removed escapeTextForBrowser() calls here since the - // whole string will be escaped when the attribute is injected into - // the markup. If you provide unsafe user data here they can inject - // arbitrary CSS which may be problematic (I couldn't repro this): - // https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet - // http://www.thespanner.co.uk/2007/11/26/ultimate-xss-css-injection/ - // This is not an XSS hole but instead a potential CSS injection issue - // which has lead to a greater discussion about how we're going to - // trust URLs moving forward. See #2115901 - - var isEmpty = value == null || typeof value === 'boolean' || value === ''; - if (isEmpty) { - return ''; - } - - var isNonNumeric = isNaN(value); - if ( - isNonNumeric || - value === 0 || - (unitlessNumbers.hasOwnProperty(name) && unitlessNumbers[name]) - ) { - return '' + value; // cast to string - } - - if (typeof value === 'string') { - if (process.env.NODE_ENV !== 'production') { - var warning = require('fbjs/lib/warning'); - - // Allow '0' to pass through without warning. 0 is already special and - // doesn't require units, so we don't need to warn about it. - if (component && value !== '0') { - var owner = component._currentElement._owner; - var ownerName = owner ? owner.getName() : null; - if (ownerName && !styleWarnings[ownerName]) { - styleWarnings[ownerName] = {}; - } - var warned = false; - if (ownerName) { - var warnings = styleWarnings[ownerName]; - warned = warnings[name]; - if (!warned) { - warnings[name] = true; - } - } - if (!warned) { - process.env.NODE_ENV !== 'production' - ? warning( - false, - 'a `%s` tag (owner: `%s`) was passed a numeric string value ' + - 'for CSS property `%s` (value: `%s`) which will be treated ' + - 'as a unitless number in a future version of React.', - component._currentElement.type, - ownerName || 'unknown', - name, - value - ) - : void 0; - } - } - } - value = value.trim(); - } - return value + 'px'; -} +import dangerousStyleValue from '../dangerousStyleValue'; +import warnValidStyle from '../warnValidStyle'; /** * Sets the value for multiple styles on a node. If a value is specified as @@ -218,14 +27,19 @@ const setValueForStyles = function(node, styles, component) { if (!styles.hasOwnProperty(styleName)) { continue; } + var isCustomProperty = styleName.indexOf('--') === 0; if (process.env.NODE_ENV !== 'production') { - warnValidStyle(styleName, styles[styleName], component); + if (!isCustomProperty) { + warnValidStyle(styleName, styles[styleName], component); + } } - var styleValue = dangerousStyleValue(styleName, styles[styleName], component); - if (styleName === 'float' || styleName === 'cssFloat') { + var styleValue = dangerousStyleValue(styleName, styles[styleName], isCustomProperty); + if (styleName === 'float') { styleName = 'cssFloat'; } - if (styleValue) { + if (isCustomProperty) { + style.setProperty(styleName, styleValue); + } else if (styleValue) { style[styleName] = styleValue; } else { style[styleName] = ''; diff --git a/src/vendor/warnValidStyle/index.js b/src/vendor/warnValidStyle/index.js new file mode 100644 index 00000000..23e2c44a --- /dev/null +++ b/src/vendor/warnValidStyle/index.js @@ -0,0 +1,170 @@ +/* eslint-disable */ + +/** + * Copyright 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule warnValidStyle + */ + +'use strict'; + +var emptyFunction = require('fbjs/lib/emptyFunction'); + +var warnValidStyle = emptyFunction; + +if (process.env.NODE_ENV !== 'production') { + var camelizeStyleName = require('fbjs/lib/camelizeStyleName'); + var warning = require('fbjs/lib/warning'); + + function getComponentName(instanceOrFiber) { + if (typeof instanceOrFiber.getName === 'function') { + // Stack reconciler + const instance = ((instanceOrFiber: any): ReactInstance); + return instance.getName(); + } + if (typeof instanceOrFiber.tag === 'number') { + // Fiber reconciler + const fiber = ((instanceOrFiber: any): Fiber); + const { type } = fiber; + if (typeof type === 'string') { + return type; + } + if (typeof type === 'function') { + return type.displayName || type.name; + } + } + return null; + } + + // 'msTransform' is correct, but the other prefixes should be capitalized + var badVendoredStyleNamePattern = /^(?:webkit|moz|o)[A-Z]/; + + // style values shouldn't contain a semicolon + var badStyleValueWithSemicolonPattern = /;\s*$/; + + var warnedStyleNames = {}; + var warnedStyleValues = {}; + var warnedForNaNValue = false; + var warnedForInfinityValue = false; + + var warnHyphenatedStyleName = function(name, owner) { + if (warnedStyleNames.hasOwnProperty(name) && warnedStyleNames[name]) { + return; + } + + warnedStyleNames[name] = true; + warning( + false, + 'Unsupported style property %s. Did you mean %s?%s', + name, + camelizeStyleName(name), + checkRenderMessage(owner) + ); + }; + + var warnBadVendoredStyleName = function(name, owner) { + if (warnedStyleNames.hasOwnProperty(name) && warnedStyleNames[name]) { + return; + } + + warnedStyleNames[name] = true; + warning( + false, + 'Unsupported vendor-prefixed style property %s. Did you mean %s?%s', + name, + name.charAt(0).toUpperCase() + name.slice(1), + checkRenderMessage(owner) + ); + }; + + var warnStyleValueWithSemicolon = function(name, value, owner) { + if (warnedStyleValues.hasOwnProperty(value) && warnedStyleValues[value]) { + return; + } + + warnedStyleValues[value] = true; + warning( + false, + "Style property values shouldn't contain a semicolon.%s " + 'Try "%s: %s" instead.', + checkRenderMessage(owner), + name, + value.replace(badStyleValueWithSemicolonPattern, '') + ); + }; + + var warnStyleValueIsNaN = function(name, value, owner) { + if (warnedForNaNValue) { + return; + } + + warnedForNaNValue = true; + warning( + false, + '`NaN` is an invalid value for the `%s` css style property.%s', + name, + checkRenderMessage(owner) + ); + }; + + var warnStyleValueIsInfinity = function(name, value, owner) { + if (warnedForInfinityValue) { + return; + } + + warnedForInfinityValue = true; + warning( + false, + '`Infinity` is an invalid value for the `%s` css style property.%s', + name, + checkRenderMessage(owner) + ); + }; + + var checkRenderMessage = function(owner) { + var ownerName; + if (owner != null) { + // Stack passes the owner manually all the way to CSSPropertyOperations. + ownerName = getComponentName(owner); + } else { + // Fiber doesn't pass it but uses ReactDebugCurrentFiber to track it. + // It is only enabled in development and tracks host components too. + // var {getCurrentFiberOwnerName} = require('ReactDebugCurrentFiber'); + // ownerName = getCurrentFiberOwnerName(); + // TODO: also report the stack. + } + if (ownerName) { + return '\n\nCheck the render method of `' + ownerName + '`.'; + } + return ''; + }; + + warnValidStyle = function(name, value, component) { + var owner; + if (component) { + // TODO: this only works with Stack. Seems like we need to add unit tests? + // owner = component._currentElement._owner; + } + if (name.indexOf('-') > -1) { + warnHyphenatedStyleName(name, owner); + } else if (badVendoredStyleNamePattern.test(name)) { + warnBadVendoredStyleName(name, owner); + } else if (badStyleValueWithSemicolonPattern.test(value)) { + warnStyleValueWithSemicolon(name, value, owner); + } + + if (typeof value === 'number') { + if (isNaN(value)) { + warnStyleValueIsNaN(name, value, owner); + } else if (!isFinite(value)) { + warnStyleValueIsInfinity(name, value, owner); + } + } + }; +} + +export default warnValidStyle;