[change] support CSS custom properties

Update 'setValueForStyles' and style validation to support defining and
using custom properties.

Fix #516
This commit is contained in:
Nicolas Gallagher
2017-07-27 16:46:08 -07:00
parent fee909d26a
commit 4e3d8dbb02
12 changed files with 292 additions and 220 deletions
+2 -1
View File
@@ -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';
+15 -11
View File
@@ -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);
}
}
}
}
+2 -1
View File
@@ -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;
}
+9 -1
View File
@@ -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);
};
+53
View File
@@ -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;
+11 -197
View File
@@ -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] = '';
+170
View File
@@ -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;