diff --git a/src/renderers/dom/fiber/ReactDOMFiberComponent.js b/src/renderers/dom/fiber/ReactDOMFiberComponent.js index b31104d479c28..f8f6f8f016e30 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberComponent.js +++ b/src/renderers/dom/fiber/ReactDOMFiberComponent.js @@ -231,20 +231,11 @@ function setInitialDOMProperties( } } else if (isCustomComponentTag) { DOMPropertyOperations.setValueForAttribute(domElement, propKey, nextProp); - } else if ( - DOMProperty.properties[propKey] || - DOMProperty.isCustomAttribute(propKey) - ) { + } else if (nextProp != null) { // If we're updating to null or undefined, we should remove the property // from the DOM node instead of inadvertently setting to a string. This // brings us in line with the same behavior we have on initial render. - if (nextProp != null) { - DOMPropertyOperations.setValueForProperty( - domElement, - propKey, - nextProp, - ); - } + DOMPropertyOperations.setValueForProperty(domElement, propKey, nextProp); } } } @@ -275,22 +266,13 @@ function updateDOMProperties( } else { DOMPropertyOperations.deleteValueForAttribute(domElement, propKey); } - } else if ( - DOMProperty.properties[propKey] || - DOMProperty.isCustomAttribute(propKey) - ) { + } else if (propValue != null) { + DOMPropertyOperations.setValueForProperty(domElement, propKey, propValue); + } else { // If we're updating to null or undefined, we should remove the property // from the DOM node instead of inadvertently setting to a string. This // brings us in line with the same behavior we have on initial render. - if (propValue != null) { - DOMPropertyOperations.setValueForProperty( - domElement, - propKey, - propValue, - ); - } else { - DOMPropertyOperations.deleteValueForProperty(domElement, propKey); - } + DOMPropertyOperations.deleteValueForProperty(domElement, propKey); } } } @@ -928,8 +910,7 @@ var ReactDOMFiberComponent = { var extraAttributeNames: Set = new Set(); var attributes = domElement.attributes; for (var i = 0; i < attributes.length; i++) { - // TODO: Do we need to lower case this to get case insensitive matches? - var name = attributes[i].name; + var name = attributes[i].name.toLowerCase(); switch (name) { // Built-in SSR attribute is whitelisted case 'data-reactroot': @@ -1013,28 +994,37 @@ var ReactDOMFiberComponent = { if (expectedStyle !== serverValue) { warnForPropDifference(propKey, serverValue, expectedStyle); } - } else if ( - isCustomComponentTag || - DOMProperty.isCustomAttribute(propKey) - ) { + } else if (isCustomComponentTag) { // $FlowFixMe - Should be inferred as not undefined. - extraAttributeNames.delete(propKey); + extraAttributeNames.delete(propKey.toLowerCase()); serverValue = DOMPropertyOperations.getValueForAttribute( domElement, propKey, nextProp, ); + if (nextProp !== serverValue) { warnForPropDifference(propKey, serverValue, nextProp); } - } else if ((propertyInfo = DOMProperty.properties[propKey])) { - // $FlowFixMe - Should be inferred as not undefined. - extraAttributeNames.delete(propertyInfo.attributeName); - serverValue = DOMPropertyOperations.getValueForProperty( - domElement, - propKey, - nextProp, - ); + } else if (DOMProperty.shouldSetAttribute(propKey, nextProp)) { + if ((propertyInfo = DOMProperty.getPropertyInfo(propKey))) { + // $FlowFixMe - Should be inferred as not undefined. + extraAttributeNames.delete(propertyInfo.attributeName); + serverValue = DOMPropertyOperations.getValueForProperty( + domElement, + propKey, + nextProp, + ); + } else { + // $FlowFixMe - Should be inferred as not undefined. + extraAttributeNames.delete(propKey.toLowerCase()); + serverValue = DOMPropertyOperations.getValueForAttribute( + domElement, + propKey, + nextProp, + ); + } + if (nextProp !== serverValue) { warnForPropDifference(propKey, serverValue, nextProp); } diff --git a/src/renderers/dom/shared/ARIADOMPropertyConfig.js b/src/renderers/dom/shared/ARIADOMPropertyConfig.js deleted file mode 100644 index 108278940cd1c..0000000000000 --- a/src/renderers/dom/shared/ARIADOMPropertyConfig.js +++ /dev/null @@ -1,74 +0,0 @@ -/** - * 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 ARIADOMPropertyConfig - */ - -'use strict'; - -var ARIADOMPropertyConfig = { - Properties: { - // Global States and Properties - 'aria-current': 0, // state - 'aria-details': 0, - 'aria-disabled': 0, // state - 'aria-hidden': 0, // state - 'aria-invalid': 0, // state - 'aria-keyshortcuts': 0, - 'aria-label': 0, - 'aria-roledescription': 0, - // Widget Attributes - 'aria-autocomplete': 0, - 'aria-checked': 0, - 'aria-expanded': 0, - 'aria-haspopup': 0, - 'aria-level': 0, - 'aria-modal': 0, - 'aria-multiline': 0, - 'aria-multiselectable': 0, - 'aria-orientation': 0, - 'aria-placeholder': 0, - 'aria-pressed': 0, - 'aria-readonly': 0, - 'aria-required': 0, - 'aria-selected': 0, - 'aria-sort': 0, - 'aria-valuemax': 0, - 'aria-valuemin': 0, - 'aria-valuenow': 0, - 'aria-valuetext': 0, - // Live Region Attributes - 'aria-atomic': 0, - 'aria-busy': 0, - 'aria-live': 0, - 'aria-relevant': 0, - // Drag-and-Drop Attributes - 'aria-dropeffect': 0, - 'aria-grabbed': 0, - // Relationship Attributes - 'aria-activedescendant': 0, - 'aria-colcount': 0, - 'aria-colindex': 0, - 'aria-colspan': 0, - 'aria-controls': 0, - 'aria-describedby': 0, - 'aria-errormessage': 0, - 'aria-flowto': 0, - 'aria-labelledby': 0, - 'aria-owns': 0, - 'aria-posinset': 0, - 'aria-rowcount': 0, - 'aria-rowindex': 0, - 'aria-rowspan': 0, - 'aria-setsize': 0, - }, - DOMAttributeNames: {}, - DOMPropertyNames: {}, -}; - -module.exports = ARIADOMPropertyConfig; diff --git a/src/renderers/dom/shared/DOMMarkupOperations.js b/src/renderers/dom/shared/DOMMarkupOperations.js index 11b9d9be11a5c..9323f7182776c 100644 --- a/src/renderers/dom/shared/DOMMarkupOperations.js +++ b/src/renderers/dom/shared/DOMMarkupOperations.js @@ -88,9 +88,7 @@ var DOMMarkupOperations = { * @return {?string} Markup string, or null if the property was invalid. */ createMarkupForProperty: function(name, value) { - var propertyInfo = DOMProperty.properties.hasOwnProperty(name) - ? DOMProperty.properties[name] - : null; + var propertyInfo = DOMProperty.getPropertyInfo(name); if (propertyInfo) { if (shouldIgnoreValue(propertyInfo, value)) { return ''; @@ -103,7 +101,7 @@ var DOMMarkupOperations = { return attributeName + '=""'; } return attributeName + '=' + quoteAttributeValueForBrowser(value); - } else if (DOMProperty.isCustomAttribute(name)) { + } else if (DOMProperty.shouldSetAttribute(name, value)) { if (value == null) { return ''; } diff --git a/src/renderers/dom/shared/DOMProperty.js b/src/renderers/dom/shared/DOMProperty.js index 793df14dd773f..d6ddf9c3b3ae5 100644 --- a/src/renderers/dom/shared/DOMProperty.js +++ b/src/renderers/dom/shared/DOMProperty.js @@ -13,6 +13,21 @@ var invariant = require('fbjs/lib/invariant'); +// These attributes should be all lowercase to allow for +// case insensitive checks +var RESERVED_PROPS = { + children: true, + dangerouslysetinnerhtml: true, + autofocus: true, + defaultvalue: true, + defaultchecked: true, + innerhtml: true, + suppresscontenteditablewarning: true, + onfocusin: true, + onfocusout: true, + style: true, +}; + function checkMask(value, bitmask) { return (value & bitmask) === bitmask; } @@ -32,11 +47,6 @@ var DOMPropertyInjection = { * Inject some specialized knowledge about the DOM. This takes a config object * with the following properties: * - * isCustomAttribute: function that given an attribute name will return true - * if it can be inserted into the DOM verbatim. Useful for data-* or aria-* - * attributes where it's impossible to enumerate all of the possible - * attribute names, - * * Properties: object mapping DOM property name to one of the * DOMPropertyInjection constants or null. If your attribute isn't in here, * it won't get written to the DOM. @@ -61,15 +71,8 @@ var DOMPropertyInjection = { var Properties = domPropertyConfig.Properties || {}; var DOMAttributeNamespaces = domPropertyConfig.DOMAttributeNamespaces || {}; var DOMAttributeNames = domPropertyConfig.DOMAttributeNames || {}; - var DOMPropertyNames = domPropertyConfig.DOMPropertyNames || {}; var DOMMutationMethods = domPropertyConfig.DOMMutationMethods || {}; - if (domPropertyConfig.isCustomAttribute) { - DOMProperty._isCustomAttributeFunctions.push( - domPropertyConfig.isCustomAttribute, - ); - } - for (var propName in Properties) { invariant( !DOMProperty.properties.hasOwnProperty(propName), @@ -111,30 +114,24 @@ var DOMPropertyInjection = { propName, ); - if (__DEV__) { - DOMProperty.getPossibleStandardName[lowerCased] = propName; - } - if (DOMAttributeNames.hasOwnProperty(propName)) { var attributeName = DOMAttributeNames[propName]; + propertyInfo.attributeName = attributeName; - if (__DEV__) { - DOMProperty.getPossibleStandardName[attributeName] = propName; - } } if (DOMAttributeNamespaces.hasOwnProperty(propName)) { propertyInfo.attributeNamespace = DOMAttributeNamespaces[propName]; } - if (DOMPropertyNames.hasOwnProperty(propName)) { - propertyInfo.propertyName = DOMPropertyNames[propName]; - } - if (DOMMutationMethods.hasOwnProperty(propName)) { propertyInfo.mutationMethod = DOMMutationMethods[propName]; } + // Downcase references to whitelist properties to check for membership + // without case-sensitivity. This allows the whitelist to pick up + // `allowfullscreen`, which should be written using the property configuration + // for `allowFullscreen` DOMProperty.properties[propName] = propertyInfo; } }, @@ -197,33 +194,58 @@ var DOMProperty = { properties: {}, /** - * Mapping from lowercase property names to the properly cased version, used - * to warn in the case of missing properties. Available only in __DEV__. - * - * autofocus is predefined, because adding it to the property whitelist - * causes unintended side effects. - * - * @type {Object} + * Checks whether a property name is a writeable attribute. + * @method */ - getPossibleStandardName: __DEV__ ? {autofocus: 'autoFocus'} : null, + shouldSetAttribute: function(name, value) { + if (DOMProperty.isReservedProp(name)) { + return false; + } - /** - * All of the isCustomAttribute() functions that have been injected. - */ - _isCustomAttributeFunctions: [], + if (value === null) { + return true; + } - /** - * Checks whether a property name is a custom attribute. - * @method - */ - isCustomAttribute: function(attributeName) { - for (var i = 0; i < DOMProperty._isCustomAttributeFunctions.length; i++) { - var isCustomAttributeFn = DOMProperty._isCustomAttributeFunctions[i]; - if (isCustomAttributeFn(attributeName)) { + var lowerCased = name.toLowerCase(); + + var propertyInfo = DOMProperty.properties[name]; + + switch (typeof value) { + case 'boolean': + if (propertyInfo) { + return true; + } + var prefix = lowerCased.slice(0, 5); + return prefix === 'data-' || prefix === 'aria-'; + case 'undefined': + case 'number': + case 'string': return true; - } + case 'object': + return true; + default: + // function, symbol + return false; } - return false; + }, + + getPropertyInfo(name) { + return DOMProperty.properties.hasOwnProperty(name) + ? DOMProperty.properties[name] + : null; + }, + + /** + * Checks to see if a property name is within the list of properties + * reserved for internal React operations. These properties should + * not be set on an HTML element. + * + * @private + * @param {string} name + * @return {boolean} If the name is within reserved props + */ + isReservedProp(name) { + return RESERVED_PROPS.hasOwnProperty(name.toLowerCase()); }, injection: DOMPropertyInjection, diff --git a/src/renderers/dom/shared/DOMPropertyOperations.js b/src/renderers/dom/shared/DOMPropertyOperations.js index 46846b3d3a868..96dead745ba6d 100644 --- a/src/renderers/dom/shared/DOMPropertyOperations.js +++ b/src/renderers/dom/shared/DOMPropertyOperations.js @@ -79,9 +79,7 @@ var DOMPropertyOperations = { */ getValueForProperty: function(node, name, expected) { if (__DEV__) { - var propertyInfo = DOMProperty.properties.hasOwnProperty(name) - ? DOMProperty.properties[name] - : null; + var propertyInfo = DOMProperty.getPropertyInfo(name); if (propertyInfo) { var mutationMethod = propertyInfo.mutationMethod; if (mutationMethod || propertyInfo.mustUseProperty) { @@ -164,10 +162,9 @@ var DOMPropertyOperations = { * @param {*} value */ setValueForProperty: function(node, name, value) { - var propertyInfo = DOMProperty.properties.hasOwnProperty(name) - ? DOMProperty.properties[name] - : null; - if (propertyInfo) { + var propertyInfo = DOMProperty.getPropertyInfo(name); + + if (propertyInfo && DOMProperty.shouldSetAttribute(name, value)) { var mutationMethod = propertyInfo.mutationMethod; if (mutationMethod) { mutationMethod(node, value); @@ -194,8 +191,12 @@ var DOMPropertyOperations = { node.setAttribute(attributeName, '' + value); } } - } else if (DOMProperty.isCustomAttribute(name)) { - DOMPropertyOperations.setValueForAttribute(node, name, value); + } else { + DOMPropertyOperations.setValueForAttribute( + node, + name, + DOMProperty.shouldSetAttribute(name, value) ? value : null, + ); return; } @@ -255,9 +256,7 @@ var DOMPropertyOperations = { * @param {string} name */ deleteValueForProperty: function(node, name) { - var propertyInfo = DOMProperty.properties.hasOwnProperty(name) - ? DOMProperty.properties[name] - : null; + var propertyInfo = DOMProperty.getPropertyInfo(name); if (propertyInfo) { var mutationMethod = propertyInfo.mutationMethod; if (mutationMethod) { @@ -272,7 +271,7 @@ var DOMPropertyOperations = { } else { node.removeAttribute(propertyInfo.attributeName); } - } else if (DOMProperty.isCustomAttribute(name)) { + } else { node.removeAttribute(name); } diff --git a/src/renderers/dom/shared/HTMLDOMPropertyConfig.js b/src/renderers/dom/shared/HTMLDOMPropertyConfig.js index 6c0f387873d1e..cf567c1a689a1 100644 --- a/src/renderers/dom/shared/HTMLDOMPropertyConfig.js +++ b/src/renderers/dom/shared/HTMLDOMPropertyConfig.js @@ -22,189 +22,72 @@ var HAS_OVERLOADED_BOOLEAN_VALUE = DOMProperty.injection.HAS_OVERLOADED_BOOLEAN_VALUE; var HTMLDOMPropertyConfig = { - isCustomAttribute: RegExp.prototype.test.bind( - new RegExp('^(data|aria)-[' + DOMProperty.ATTRIBUTE_NAME_CHAR + ']*$'), - ), Properties: { - /** - * Standard Properties - */ - accept: 0, - acceptCharset: 0, - accessKey: 0, - action: 0, allowFullScreen: HAS_BOOLEAN_VALUE, - allowTransparency: 0, - alt: 0, // specifies target context for links with `preload` type - as: 0, async: HAS_BOOLEAN_VALUE, - autoComplete: 0, // autoFocus is polyfilled/normalized by AutoFocusUtils // autoFocus: HAS_BOOLEAN_VALUE, autoPlay: HAS_BOOLEAN_VALUE, capture: HAS_BOOLEAN_VALUE, - cellPadding: 0, - cellSpacing: 0, - charSet: 0, - challenge: 0, checked: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE, - cite: 0, - classID: 0, - className: 0, cols: HAS_POSITIVE_NUMERIC_VALUE, - colSpan: 0, - content: 0, - contentEditable: 0, - contextMenu: 0, controls: HAS_BOOLEAN_VALUE, - controlsList: 0, - coords: 0, - crossOrigin: 0, - data: 0, // For `` acts as `src`. - dateTime: 0, default: HAS_BOOLEAN_VALUE, defer: HAS_BOOLEAN_VALUE, - dir: 0, disabled: HAS_BOOLEAN_VALUE, download: HAS_OVERLOADED_BOOLEAN_VALUE, - draggable: 0, - encType: 0, - form: 0, - formAction: 0, - formEncType: 0, - formMethod: 0, formNoValidate: HAS_BOOLEAN_VALUE, - formTarget: 0, - frameBorder: 0, - headers: 0, - height: 0, hidden: HAS_BOOLEAN_VALUE, - high: 0, - href: 0, - hrefLang: 0, - htmlFor: 0, - httpEquiv: 0, - id: 0, - inputMode: 0, - integrity: 0, - is: 0, - keyParams: 0, - keyType: 0, - kind: 0, - label: 0, - lang: 0, - list: 0, loop: HAS_BOOLEAN_VALUE, - low: 0, - manifest: 0, - marginHeight: 0, - marginWidth: 0, - max: 0, - maxLength: 0, - media: 0, - mediaGroup: 0, - method: 0, - min: 0, - minLength: 0, // Caution; `option.selected` is not updated if `select.multiple` is // disabled with `removeAttribute`. multiple: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE, muted: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE, - name: 0, - nonce: 0, noValidate: HAS_BOOLEAN_VALUE, open: HAS_BOOLEAN_VALUE, - optimum: 0, - pattern: 0, - placeholder: 0, playsInline: HAS_BOOLEAN_VALUE, - poster: 0, - preload: 0, - profile: 0, - radioGroup: 0, readOnly: HAS_BOOLEAN_VALUE, - referrerPolicy: 0, - rel: 0, required: HAS_BOOLEAN_VALUE, reversed: HAS_BOOLEAN_VALUE, - role: 0, rows: HAS_POSITIVE_NUMERIC_VALUE, rowSpan: HAS_NUMERIC_VALUE, - sandbox: 0, - scope: 0, scoped: HAS_BOOLEAN_VALUE, - scrolling: 0, seamless: HAS_BOOLEAN_VALUE, selected: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE, - shape: 0, size: HAS_POSITIVE_NUMERIC_VALUE, - sizes: 0, + start: HAS_NUMERIC_VALUE, // support for projecting regular DOM Elements via V1 named slots ( shadow dom ) - slot: 0, span: HAS_POSITIVE_NUMERIC_VALUE, - spellCheck: 0, - src: 0, - srcDoc: 0, - srcLang: 0, - srcSet: 0, - start: HAS_NUMERIC_VALUE, - step: 0, + // Style must be explicitly set in the attribute list. React components + // expect a style object style: 0, - summary: 0, - tabIndex: 0, - target: 0, - title: 0, - // Setting .type throws on non- tags - type: 0, - useMap: 0, + // itemScope is for for Microdata support. + // See http://schema.org/docs/gs.html + itemScope: HAS_BOOLEAN_VALUE, + // These attributes must stay in the white-list because they have + // different attribute names (see DOMAttributeNames below) + acceptCharset: 0, + className: 0, + htmlFor: 0, + httpEquiv: 0, + // Attributes with mutation methods must be specified in the whitelist value: 0, - width: 0, - wmode: 0, - wrap: 0, - - /** - * RDFa Properties - */ - about: 0, - datatype: 0, - inlist: 0, - prefix: 0, - // property is also supported for OpenGraph in meta tags. - property: 0, - resource: 0, - typeof: 0, - vocab: 0, - - /** - * Non-standard Properties - */ + // The following attributes expect boolean values. They must be in + // the whitelist to allow boolean attribute assignment: + autoComplete: 0, + // IE only true/false iFrame attribute + // https://msdn.microsoft.com/en-us/library/ms533072(v=vs.85).aspx + allowTransparency: 0, + contentEditable: 0, + draggable: 0, + spellCheck: 0, // autoCapitalize and autoCorrect are supported in Mobile Safari for // keyboard hints. autoCapitalize: 0, autoCorrect: 0, // autoSave allows WebKit/Blink to persist values of input fields on page reloads autoSave: 0, - // color is for Safari mask-icon link - color: 0, - // itemProp, itemScope, itemType are for - // Microdata support. See http://schema.org/docs/gs.html - itemProp: 0, - itemScope: HAS_BOOLEAN_VALUE, - itemType: 0, - // itemID and itemRef are for Microdata support as well but - // only specified in the WHATWG spec document. See - // https://html.spec.whatwg.org/multipage/microdata.html#microdata-dom-api - itemID: 0, - itemRef: 0, - // results show looking glass icon and recent searches on input - // search fields in WebKit/Blink - results: 0, - // IE-only attribute that specifies security restrictions on an iframe - // as an alternative to the sandbox attribute on IE<10 - security: 0, - // IE-only attribute that controls focus behavior - unselectable: 0, }, DOMAttributeNames: { acceptCharset: 'accept-charset', @@ -212,7 +95,6 @@ var HTMLDOMPropertyConfig = { htmlFor: 'for', httpEquiv: 'http-equiv', }, - DOMPropertyNames: {}, DOMMutationMethods: { value: function(node, value) { if (value == null) { diff --git a/src/renderers/dom/shared/ReactDOMInjection.js b/src/renderers/dom/shared/ReactDOMInjection.js index e507423f6ab76..a14c7cc2d6239 100644 --- a/src/renderers/dom/shared/ReactDOMInjection.js +++ b/src/renderers/dom/shared/ReactDOMInjection.js @@ -11,11 +11,9 @@ 'use strict'; -var ARIADOMPropertyConfig = require('ARIADOMPropertyConfig'); var DOMProperty = require('DOMProperty'); var HTMLDOMPropertyConfig = require('HTMLDOMPropertyConfig'); var SVGDOMPropertyConfig = require('SVGDOMPropertyConfig'); -DOMProperty.injection.injectDOMPropertyConfig(ARIADOMPropertyConfig); DOMProperty.injection.injectDOMPropertyConfig(HTMLDOMPropertyConfig); DOMProperty.injection.injectDOMPropertyConfig(SVGDOMPropertyConfig); diff --git a/src/renderers/dom/shared/SVGDOMPropertyConfig.js b/src/renderers/dom/shared/SVGDOMPropertyConfig.js index dbb9da3be1f90..657e2f3a5ce18 100644 --- a/src/renderers/dom/shared/SVGDOMPropertyConfig.js +++ b/src/renderers/dom/shared/SVGDOMPropertyConfig.js @@ -16,264 +16,105 @@ var NS = { xml: 'http://www.w3.org/XML/1998/namespace', }; -// We use attributes for everything SVG so let's avoid some duplication and run -// code instead. -// The following are all specified in the HTML config already so we exclude here. -// - class (as className) -// - color -// - height -// - id -// - lang -// - max -// - media -// - method -// - min -// - name -// - style -// - target -// - type -// - width -var ATTRS = { - accentHeight: 'accent-height', - accumulate: 0, - additive: 0, - alignmentBaseline: 'alignment-baseline', - allowReorder: 'allowReorder', - alphabetic: 0, - amplitude: 0, - arabicForm: 'arabic-form', - ascent: 0, - attributeName: 'attributeName', - attributeType: 'attributeType', - autoReverse: 'autoReverse', - azimuth: 0, - baseFrequency: 'baseFrequency', - baseProfile: 'baseProfile', - baselineShift: 'baseline-shift', - bbox: 0, - begin: 0, - bias: 0, - by: 0, - calcMode: 'calcMode', - capHeight: 'cap-height', - clip: 0, - clipPath: 'clip-path', - clipRule: 'clip-rule', - clipPathUnits: 'clipPathUnits', - colorInterpolation: 'color-interpolation', - colorInterpolationFilters: 'color-interpolation-filters', - colorProfile: 'color-profile', - colorRendering: 'color-rendering', - contentScriptType: 'contentScriptType', - contentStyleType: 'contentStyleType', - cursor: 0, - cx: 0, - cy: 0, - d: 0, - decelerate: 0, - descent: 0, - diffuseConstant: 'diffuseConstant', - direction: 0, - display: 0, - divisor: 0, - dominantBaseline: 'dominant-baseline', - dur: 0, - dx: 0, - dy: 0, - edgeMode: 'edgeMode', - elevation: 0, - enableBackground: 'enable-background', - end: 0, - exponent: 0, - externalResourcesRequired: 'externalResourcesRequired', - fill: 0, - fillOpacity: 'fill-opacity', - fillRule: 'fill-rule', - filter: 0, - filterRes: 'filterRes', - filterUnits: 'filterUnits', - floodColor: 'flood-color', - floodOpacity: 'flood-opacity', - focusable: 0, - fontFamily: 'font-family', - fontSize: 'font-size', - fontSizeAdjust: 'font-size-adjust', - fontStretch: 'font-stretch', - fontStyle: 'font-style', - fontVariant: 'font-variant', - fontWeight: 'font-weight', - format: 0, - from: 0, - fx: 0, - fy: 0, - g1: 0, - g2: 0, - glyphName: 'glyph-name', - glyphOrientationHorizontal: 'glyph-orientation-horizontal', - glyphOrientationVertical: 'glyph-orientation-vertical', - glyphRef: 'glyphRef', - gradientTransform: 'gradientTransform', - gradientUnits: 'gradientUnits', - hanging: 0, - horizAdvX: 'horiz-adv-x', - horizOriginX: 'horiz-origin-x', - ideographic: 0, - imageRendering: 'image-rendering', - in: 0, - in2: 0, - intercept: 0, - k: 0, - k1: 0, - k2: 0, - k3: 0, - k4: 0, - kernelMatrix: 'kernelMatrix', - kernelUnitLength: 'kernelUnitLength', - kerning: 0, - keyPoints: 'keyPoints', - keySplines: 'keySplines', - keyTimes: 'keyTimes', - lengthAdjust: 'lengthAdjust', - letterSpacing: 'letter-spacing', - lightingColor: 'lighting-color', - limitingConeAngle: 'limitingConeAngle', - local: 0, - markerEnd: 'marker-end', - markerMid: 'marker-mid', - markerStart: 'marker-start', - markerHeight: 'markerHeight', - markerUnits: 'markerUnits', - markerWidth: 'markerWidth', - mask: 0, - maskContentUnits: 'maskContentUnits', - maskUnits: 'maskUnits', - mathematical: 0, - mode: 0, - numOctaves: 'numOctaves', - offset: 0, - opacity: 0, - operator: 0, - order: 0, - orient: 0, - orientation: 0, - origin: 0, - overflow: 0, - overlinePosition: 'overline-position', - overlineThickness: 'overline-thickness', - paintOrder: 'paint-order', - panose1: 'panose-1', - pathLength: 'pathLength', - patternContentUnits: 'patternContentUnits', - patternTransform: 'patternTransform', - patternUnits: 'patternUnits', - pointerEvents: 'pointer-events', - points: 0, - pointsAtX: 'pointsAtX', - pointsAtY: 'pointsAtY', - pointsAtZ: 'pointsAtZ', - preserveAlpha: 'preserveAlpha', - preserveAspectRatio: 'preserveAspectRatio', - primitiveUnits: 'primitiveUnits', - r: 0, - radius: 0, - refX: 'refX', - refY: 'refY', - renderingIntent: 'rendering-intent', - repeatCount: 'repeatCount', - repeatDur: 'repeatDur', - requiredExtensions: 'requiredExtensions', - requiredFeatures: 'requiredFeatures', - restart: 0, - result: 0, - rotate: 0, - rx: 0, - ry: 0, - scale: 0, - seed: 0, - shapeRendering: 'shape-rendering', - slope: 0, - spacing: 0, - specularConstant: 'specularConstant', - specularExponent: 'specularExponent', - speed: 0, - spreadMethod: 'spreadMethod', - startOffset: 'startOffset', - stdDeviation: 'stdDeviation', - stemh: 0, - stemv: 0, - stitchTiles: 'stitchTiles', - stopColor: 'stop-color', - stopOpacity: 'stop-opacity', - strikethroughPosition: 'strikethrough-position', - strikethroughThickness: 'strikethrough-thickness', - string: 0, - stroke: 0, - strokeDasharray: 'stroke-dasharray', - strokeDashoffset: 'stroke-dashoffset', - strokeLinecap: 'stroke-linecap', - strokeLinejoin: 'stroke-linejoin', - strokeMiterlimit: 'stroke-miterlimit', - strokeOpacity: 'stroke-opacity', - strokeWidth: 'stroke-width', - surfaceScale: 'surfaceScale', - systemLanguage: 'systemLanguage', - tableValues: 'tableValues', - targetX: 'targetX', - targetY: 'targetY', - textAnchor: 'text-anchor', - textDecoration: 'text-decoration', - textRendering: 'text-rendering', - textLength: 'textLength', - to: 0, - transform: 0, - u1: 0, - u2: 0, - underlinePosition: 'underline-position', - underlineThickness: 'underline-thickness', - unicode: 0, - unicodeBidi: 'unicode-bidi', - unicodeRange: 'unicode-range', - unitsPerEm: 'units-per-em', - vAlphabetic: 'v-alphabetic', - vHanging: 'v-hanging', - vIdeographic: 'v-ideographic', - vMathematical: 'v-mathematical', - values: 0, - vectorEffect: 'vector-effect', - version: 0, - vertAdvY: 'vert-adv-y', - vertOriginX: 'vert-origin-x', - vertOriginY: 'vert-origin-y', - viewBox: 'viewBox', - viewTarget: 'viewTarget', - visibility: 0, - widths: 0, - wordSpacing: 'word-spacing', - writingMode: 'writing-mode', - x: 0, - xHeight: 'x-height', - x1: 0, - x2: 0, - xChannelSelector: 'xChannelSelector', - xlinkActuate: 'xlink:actuate', - xlinkArcrole: 'xlink:arcrole', - xlinkHref: 'xlink:href', - xlinkRole: 'xlink:role', - xlinkShow: 'xlink:show', - xlinkTitle: 'xlink:title', - xlinkType: 'xlink:type', - xmlBase: 'xml:base', - xmlns: 0, - xmlnsXlink: 'xmlns:xlink', - xmlLang: 'xml:lang', - xmlSpace: 'xml:space', - y: 0, - y1: 0, - y2: 0, - yChannelSelector: 'yChannelSelector', - z: 0, - zoomAndPan: 'zoomAndPan', -}; +/** + * This is a list of all SVG attributes that need special + * casing, namespacing, or boolean value assignment. + * + * SVG Attributes List: + * https://www.w3.org/TR/SVG/attindex.html + * SMIL Spec: + * https://www.w3.org/TR/smil + */ +var ATTRS = [ + 'accent-height', + 'alignment-baseline', + 'arabic-form', + 'baseline-shift', + 'cap-height', + 'clip-path', + 'clip-rule', + 'color-interpolation', + 'color-interpolation-filters', + 'color-profile', + 'color-rendering', + 'dominant-baseline', + 'enable-background', + 'fill-opacity', + 'fill-rule', + 'flood-color', + 'flood-opacity', + 'font-family', + 'font-size', + 'font-size-adjust', + 'font-stretch', + 'font-style', + 'font-variant', + 'font-weight', + 'glyph-name', + 'glyph-orientation-horizontal', + 'glyph-orientation-vertical', + 'horiz-adv-x', + 'horiz-origin-x', + 'image-rendering', + 'letter-spacing', + 'lighting-color', + 'marker-end', + 'marker-mid', + 'marker-start', + 'overline-position', + 'overline-thickness', + 'paint-order', + 'panose-1', + 'pointer-events', + 'rendering-intent', + 'shape-rendering', + 'stop-color', + 'stop-opacity', + 'strikethrough-position', + 'strikethrough-thickness', + 'stroke-dasharray', + 'stroke-dashoffset', + 'stroke-linecap', + 'stroke-linejoin', + 'stroke-miterlimit', + 'stroke-opacity', + 'stroke-width', + 'text-anchor', + 'text-decoration', + 'text-rendering', + 'underline-position', + 'underline-thickness', + 'unicode-bidi', + 'unicode-range', + 'units-per-em', + 'v-alphabetic', + 'v-hanging', + 'v-ideographic', + 'v-mathematical', + 'vector-effect', + 'vert-adv-y', + 'vert-origin-x', + 'vert-origin-y', + 'word-spacing', + 'writing-mode', + 'x-height', + 'xlink:actuate', + 'xlink:arcrole', + 'xlink:href', + 'xlink:role', + 'xlink:show', + 'xlink:title', + 'xlink:type', + 'xml:base', + 'xmlns:xlink', + 'xml:lang', + 'xml:space', + // The following attributes expect boolean values. They must be in + // the whitelist to allow boolean attribute assignment: + 'autoReverse', + 'externalResourcesRequired', + 'preserveAlpha', +]; var SVGDOMPropertyConfig = { Properties: {}, @@ -292,11 +133,14 @@ var SVGDOMPropertyConfig = { DOMAttributeNames: {}, }; -Object.keys(ATTRS).forEach(key => { - SVGDOMPropertyConfig.Properties[key] = 0; - if (ATTRS[key]) { - SVGDOMPropertyConfig.DOMAttributeNames[key] = ATTRS[key]; - } +var CAMELIZE = /[\-\:]([a-z])/g; +var capitalize = token => token[1].toUpperCase(); + +ATTRS.forEach(original => { + var reactName = original.replace(CAMELIZE, capitalize); + + SVGDOMPropertyConfig.Properties[reactName] = 0; + SVGDOMPropertyConfig.DOMAttributeNames[reactName] = original; }); module.exports = SVGDOMPropertyConfig; diff --git a/src/renderers/dom/shared/__tests__/DOMPropertyOperations-test.js b/src/renderers/dom/shared/__tests__/DOMPropertyOperations-test.js index c0033f1ba2dba..5b2c96a262a87 100644 --- a/src/renderers/dom/shared/__tests__/DOMPropertyOperations-test.js +++ b/src/renderers/dom/shared/__tests__/DOMPropertyOperations-test.js @@ -67,12 +67,13 @@ describe('DOMPropertyOperations', () => { // This ensures that we have consistent behavior. var obj = { toString: function() { - return ''; + return 'css-class'; }, }; + var container = document.createElement('div'); - ReactDOM.render(
, container); - expect(container.firstChild.getAttribute('role')).toBe(''); + ReactDOM.render(
, container); + expect(container.firstChild.getAttribute('class')).toBe('css-class'); }); it('should not remove empty attributes for special properties', () => { diff --git a/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js b/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js index f07cfcc9d7309..b52588ef0d575 100644 --- a/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js +++ b/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js @@ -145,22 +145,26 @@ describe('ReactDOMComponent', () => { it('should warn for unknown prop', () => { spyOn(console, 'error'); var container = document.createElement('div'); - ReactDOM.render(
, container); + ReactDOM.render(
{}} />, container); expectDev(console.error.calls.count(0)).toBe(1); expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe( - 'Warning: Unknown prop `foo` on
tag. Remove this prop from the element. ' + - 'For details, see https://fb.me/react-unknown-prop\n in div (at **)', + 'Warning: Invalid prop `foo` on
tag. Either remove this prop ' + + 'from the element, or pass a string or number value to keep ' + + 'it in the DOM. For details, see https://fb.me/react-unknown-prop' + + '\n in div (at **)', ); }); it('should group multiple unknown prop warnings together', () => { spyOn(console, 'error'); var container = document.createElement('div'); - ReactDOM.render(
, container); + ReactDOM.render(
{}} baz={() => {}} />, container); expectDev(console.error.calls.count(0)).toBe(1); expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe( - 'Warning: Unknown props `foo`, `baz` on
tag. Remove these props from the element. ' + - 'For details, see https://fb.me/react-unknown-prop\n in div (at **)', + 'Warning: Invalid props `foo`, `baz` on
tag. Either remove these ' + + 'props from the element, or pass a string or number value to keep ' + + 'them in the DOM. For details, see https://fb.me/react-unknown-prop' + + '\n in div (at **)', ); }); @@ -170,7 +174,7 @@ describe('ReactDOMComponent', () => { ReactDOM.render(
{}} />, container); expectDev(console.error.calls.count(0)).toBe(1); expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe( - 'Warning: Unknown event handler property onDblClick. Did you mean `onDoubleClick`?\n in div (at **)', + 'Warning: Unknown event handler property `onDblClick`. Did you mean `onDoubleClick`?\n in div (at **)', ); }); @@ -1002,6 +1006,15 @@ describe('ReactDOMComponent', () => { ); }); + it('should validate against use of innerHTML without case sensitivity', () => { + spyOn(console, 'error'); + mountComponent({innerhtml: 'Hi Jim!'}); + expectDev(console.error.calls.count()).toBe(1); + expectDev(console.error.calls.argsFor(0)[0]).toContain( + 'Directly setting property `innerHTML` is not permitted. ', + ); + }); + it('should validate use of dangerouslySetInnerHTML', () => { expect(function() { mountComponent({dangerouslySetInnerHTML: 'Hi Jim!'}); @@ -1593,6 +1606,18 @@ describe('ReactDOMComponent', () => { expectDev(console.error.calls.count()).toBe(2); }); + it('should warn about props that are no longer supported without case sensitivity', () => { + spyOn(console, 'error'); + ReactTestUtils.renderIntoDocument(
); + expectDev(console.error.calls.count()).toBe(0); + + ReactTestUtils.renderIntoDocument(
{}} />); + expectDev(console.error.calls.count()).toBe(1); + + ReactTestUtils.renderIntoDocument(
{}} />); + expectDev(console.error.calls.count()).toBe(2); + }); + it('should warn about props that are no longer supported (ssr)', () => { spyOn(console, 'error'); ReactDOMServer.renderToString(
); @@ -1605,16 +1630,28 @@ describe('ReactDOMComponent', () => { expectDev(console.error.calls.count()).toBe(2); }); + it('should warn about props that are no longer supported without case sensitivity (ssr)', () => { + spyOn(console, 'error'); + ReactDOMServer.renderToString(
); + expectDev(console.error.calls.count()).toBe(0); + + ReactDOMServer.renderToString(
{}} />); + expectDev(console.error.calls.count()).toBe(1); + + ReactDOMServer.renderToString(
{}} />); + expectDev(console.error.calls.count()).toBe(2); + }); + it('gives source code refs for unknown prop warning', () => { spyOn(console, 'error'); ReactTestUtils.renderIntoDocument(
); ReactTestUtils.renderIntoDocument(); expectDev(console.error.calls.count()).toBe(2); expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe( - 'Warning: Unknown DOM property class. Did you mean className?\n in div (at **)', + 'Warning: Invalid DOM property `class`. Did you mean `className`?\n in div (at **)', ); expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(1)[0])).toBe( - 'Warning: Unknown event handler property onclick. Did you mean ' + + 'Warning: Unknown event handler property `onclick`. Did you mean ' + '`onClick`?\n in input (at **)', ); }); @@ -1625,10 +1662,10 @@ describe('ReactDOMComponent', () => { ReactDOMServer.renderToString(); expectDev(console.error.calls.count()).toBe(2); expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe( - 'Warning: Unknown DOM property class. Did you mean className?\n in div (at **)', + 'Warning: Invalid DOM property `class`. Did you mean `className`?\n in div (at **)', ); expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(1)[0])).toBe( - 'Warning: Unknown event handler property onclick. Did you mean ' + + 'Warning: Unknown event handler property `onclick`. Did you mean ' + '`onClick`?\n in input (at **)', ); }); @@ -1643,7 +1680,7 @@ describe('ReactDOMComponent', () => { ReactTestUtils.renderIntoDocument(
, container); expectDev(console.error.calls.count()).toBe(1); expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe( - 'Warning: Unknown DOM property class. Did you mean className?\n in div (at **)', + 'Warning: Invalid DOM property `class`. Did you mean `className`?\n in div (at **)', ); }); @@ -1821,11 +1858,11 @@ describe('ReactDOMComponent', () => { expectDev(console.error.calls.count()).toBe(2); expectDev(console.error.calls.argsFor(0)[0]).toBe( - 'Warning: Unknown DOM property for. Did you mean htmlFor?\n in label', + 'Warning: Invalid DOM property `for`. Did you mean `htmlFor`?\n in label', ); expectDev(console.error.calls.argsFor(1)[0]).toBe( - 'Warning: Unknown DOM property autofocus. Did you mean autoFocus?\n in input', + 'Warning: Invalid DOM property `autofocus`. Did you mean `autoFocus`?\n in input', ); }); @@ -1842,11 +1879,11 @@ describe('ReactDOMComponent', () => { expectDev(console.error.calls.count()).toBe(2); expectDev(console.error.calls.argsFor(0)[0]).toBe( - 'Warning: Unknown DOM property for. Did you mean htmlFor?\n in label', + 'Warning: Invalid DOM property `for`. Did you mean `htmlFor`?\n in label', ); expectDev(console.error.calls.argsFor(1)[0]).toBe( - 'Warning: Unknown DOM property autofocus. Did you mean autoFocus?\n in input', + 'Warning: Invalid DOM property `autofocus`. Did you mean `autoFocus`?\n in input', ); }); }); @@ -1874,4 +1911,238 @@ describe('ReactDOMComponent', () => { expect(container.firstChild.innerHTML).toBe(html2); }); }); + + describe('Attributes with aliases', function() { + it('sets aliased attributes on HTML attributes', function() { + spyOn(console, 'error'); + + var el = ReactTestUtils.renderIntoDocument(
); + + expect(el.className).toBe('test'); + + expectDev(console.error.calls.argsFor(0)[0]).toContain( + 'Warning: Invalid DOM property `class`. Did you mean `className`?', + ); + }); + + it('sets incorrectly cased aliased attributes on HTML attributes with a warning', function() { + spyOn(console, 'error'); + + var el = ReactTestUtils.renderIntoDocument(
); + + expect(el.className).toBe('test'); + + expectDev(console.error.calls.argsFor(0)[0]).toContain( + 'Warning: Invalid DOM property `cLASS`. Did you mean `className`?', + ); + }); + + it('sets aliased attributes on SVG elements with a warning', function() { + spyOn(console, 'error'); + + var el = ReactTestUtils.renderIntoDocument( + , + ); + var text = el.querySelector('text'); + + expect(text.hasAttribute('arabic-form')).toBe(true); + + expectDev(console.error.calls.argsFor(0)[0]).toContain( + 'Warning: Invalid DOM property `arabic-form`. Did you mean `arabicForm`?', + ); + }); + + it('sets aliased attributes on custom elements', function() { + var el = ReactTestUtils.renderIntoDocument( +
, + ); + + expect(el.getAttribute('class')).toBe('test'); + }); + + it('aliased attributes on custom elements with bad casing', function() { + var el = ReactTestUtils.renderIntoDocument( +
, + ); + + expect(el.getAttribute('class')).toBe('test'); + }); + + it('updates aliased attributes on custom elements', function() { + var container = document.createElement('div'); + ReactDOM.render(
, container); + ReactDOM.render(
, container); + + expect(container.firstChild.getAttribute('class')).toBe('bar'); + }); + }); + + describe('Custom attributes', function() { + it('allows assignment of custom attributes with string values', function() { + var el = ReactTestUtils.renderIntoDocument(
); + + expect(el.getAttribute('whatever')).toBe('30'); + }); + + it('removes custom attributes', function() { + const container = document.createElement('div'); + ReactDOM.render(
, container); + + expect(container.firstChild.getAttribute('whatever')).toBe('30'); + + ReactDOM.render(
, container); + + expect(container.firstChild.hasAttribute('whatever')).toBe(false); + }); + + it('does not assign a boolean custom attributes as a string', function() { + spyOn(console, 'error'); + + var el = ReactTestUtils.renderIntoDocument(
); + + expect(el.hasAttribute('whatever')).toBe(false); + + expectDev(console.error.calls.argsFor(0)[0]).toContain( + 'Warning: Invalid prop `whatever` on
tag', + ); + }); + + it('does not assign an implicit boolean custom attributes', function() { + spyOn(console, 'error'); + + // eslint-disable-next-line react/jsx-boolean-value + var el = ReactTestUtils.renderIntoDocument(
); + + expect(el.hasAttribute('whatever')).toBe(false); + + expectDev(console.error.calls.argsFor(0)[0]).toContain( + 'Warning: Invalid prop `whatever` on
tag', + ); + }); + + it('assigns a numeric custom attributes as a string', function() { + var el = ReactTestUtils.renderIntoDocument(
); + + expect(el.getAttribute('whatever')).toBe('3'); + }); + + it('will not assign a function custom attributes', function() { + spyOn(console, 'error'); + + var el = ReactTestUtils.renderIntoDocument(
{}} />); + + expect(el.hasAttribute('whatever')).toBe(false); + + expectDev(console.error.calls.argsFor(0)[0]).toContain( + 'Warning: Invalid prop `whatever` on
tag', + ); + }); + + it('will assign an object custom attributes', function() { + var el = ReactTestUtils.renderIntoDocument(
); + expect(el.getAttribute('whatever')).toBe('[object Object]'); + }); + + it('allows cased data attributes', function() { + var el = ReactTestUtils.renderIntoDocument(
); + expect(el.getAttribute('data-foobar')).toBe('true'); + }); + + it('allows cased custom attributes', function() { + var el = ReactTestUtils.renderIntoDocument(
); + expect(el.getAttribute('foobar')).toBe('true'); + }); + + it('warns on NaN attributes', function() { + spyOn(console, 'error'); + + var el = ReactTestUtils.renderIntoDocument(
); + + expect(el.getAttribute('whatever')).toBe('NaN'); + + expectDev(console.error.calls.argsFor(0)[0]).toContain( + 'Warning: Received NaN for numeric attribute `whatever`. If this is ' + + 'expected, cast the value to a string.\n in div', + ); + }); + + it('removes a property when it becomes invalid', function() { + spyOn(console, 'error'); + + var container = document.createElement('div'); + ReactDOM.render(
, container); + ReactDOM.render(
{}} />, container); + var el = container.firstChild; + + expect(el.hasAttribute('whatever')).toBe(false); + + expectDev(console.error.calls.argsFor(0)[0]).toContain( + 'Warning: Invalid prop `whatever` on
tag.', + ); + }); + }); + + describe('Object stringification', function() { + it('allows objects on known properties', function() { + var el = ReactTestUtils.renderIntoDocument( +
, + ); + expect(el.getAttribute('allowtransparency')).toBe('[object Object]'); + }); + + it('should pass objects as attributes if they define toString', () => { + var obj = { + toString() { + return 'hello'; + }, + }; + var container = document.createElement('div'); + + ReactDOM.render(, container); + expect(container.firstChild.src).toBe('hello'); + + ReactDOM.render(, container); + expect(container.firstChild.getAttribute('arabic-form')).toBe('hello'); + + ReactDOM.render(
, container); + expect(container.firstChild.getAttribute('customAttribute')).toBe( + 'hello', + ); + }); + + it('passes objects on known SVG attributes if they do not define toString', () => { + var obj = {}; + var container = document.createElement('div'); + + ReactDOM.render(, container); + expect(container.firstChild.getAttribute('arabic-form')).toBe( + '[object Object]', + ); + }); + + it('passes objects on custom attributes if they do not define toString', () => { + var obj = {}; + var container = document.createElement('div'); + + ReactDOM.render(
, container); + expect(container.firstChild.getAttribute('customAttribute')).toBe( + '[object Object]', + ); + }); + + it('allows objects that inherit a custom toString method', function() { + var parent = {toString: () => 'hello.jpg'}; + var child = Object.create(parent); + var el = ReactTestUtils.renderIntoDocument(); + + expect(el.src).toBe('hello.jpg'); + }); + + it('assigns ajaxify (an important internal FB attribute)', function() { + var options = {toString: () => 'ajaxy'}; + var el = ReactTestUtils.renderIntoDocument(
); + + expect(el.getAttribute('ajaxify')).toBe('ajaxy'); + }); + }); }); diff --git a/src/renderers/dom/shared/__tests__/ReactDOMInvalidARIAHook-test.js b/src/renderers/dom/shared/__tests__/ReactDOMInvalidARIAHook-test.js index 60303ec95e8da..e8c5cb479c3ad 100644 --- a/src/renderers/dom/shared/__tests__/ReactDOMInvalidARIAHook-test.js +++ b/src/renderers/dom/shared/__tests__/ReactDOMInvalidARIAHook-test.js @@ -63,5 +63,27 @@ describe('ReactDOMInvalidARIAHook', () => { 'Did you mean aria-haspopup?', ); }); + + it('should warn for use of recognized camel case aria attributes', () => { + spyOn(console, 'error'); + // The valid attribute name is aria-haspopup. + mountComponent({ariaHasPopup: 'true'}); + expectDev(console.error.calls.count()).toBe(1); + expectDev(console.error.calls.argsFor(0)[0]).toContain( + 'Warning: Invalid ARIA attribute ariaHasPopup. ' + + 'Did you mean aria-haspopup?', + ); + }); + + it('should warn for use of unrecognized camel case aria attributes', () => { + spyOn(console, 'error'); + // The valid attribute name is aria-haspopup. + mountComponent({ariaSomethingInvalid: 'true'}); + expectDev(console.error.calls.count()).toBe(1); + expectDev(console.error.calls.argsFor(0)[0]).toContain( + 'Warning: Invalid ARIA attribute ariaSomethingInvalid. ARIA ' + + 'attributes follow the pattern aria-* and must be lowercase.', + ); + }); }); }); diff --git a/src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js b/src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js index 2f7c137cbb161..f857ddcd595fa 100644 --- a/src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js +++ b/src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js @@ -312,10 +312,6 @@ function expectMarkupMismatch(serverElement, clientElement) { return testMarkupMatch(serverElement, clientElement, false); } -// TODO: this is a hack for testing dynamic injection. Remove this when we decide -// how to do static injection instead. -let onAfterResetModules = null; - // When there is a test that renders on server and then on client and expects a logged // error, you want to see the error show up both on server and client. Unfortunately, // React refuses to issue the same error twice to avoid clogging up the console. @@ -323,9 +319,6 @@ let onAfterResetModules = null; function resetModules() { // First, reset the modules to load the client renderer. jest.resetModuleRegistry(); - if (typeof onAfterResetModules === 'function') { - onAfterResetModules(); - } // TODO: can we express this test with only public API? ExecutionEnvironment = require('ExecutionEnvironment'); @@ -341,9 +334,6 @@ function resetModules() { // Resetting is important because we want to avoid any shared state // influencing the tests. jest.resetModuleRegistry(); - if (typeof onAfterResetModules === 'function') { - onAfterResetModules(); - } require('ReactFeatureFlags').disableNewFiberFeatures = false; ReactDOMServer = require('react-dom/server'); } @@ -477,16 +467,14 @@ describe('ReactDOMServerIntegration', () => { expect(e.getAttribute('width')).toBe('30'); }); - // this seems like it might mask programmer error, but it's existing behavior. - itRenders('string prop with true value', async render => { - const e = await render(); - expect(e.getAttribute('href')).toBe('true'); + itRenders('no string prop with true value', async render => { + const e = await render(, 1); + expect(e.hasAttribute('href')).toBe(false); }); - // this seems like it might mask programmer error, but it's existing behavior. - itRenders('string prop with false value', async render => { - const e = await render(); - expect(e.getAttribute('href')).toBe('false'); + itRenders('no string prop with false value', async render => { + const e = await render(, 1); + expect(e.hasAttribute('href')).toBe(false); }); itRenders('no string prop with null value', async render => { @@ -624,6 +612,35 @@ describe('ReactDOMServerIntegration', () => { const e = await render(
); expect(e.hasAttribute('className')).toBe(false); }); + + itRenders('no badly cased className with a warning', async render => { + const e = await render(
, 1); + expect(e.hasAttribute('class')).toBe(false); + expect(e.hasAttribute('classname')).toBe(true); + }); + + itRenders('className prop when given the alias', async render => { + const e = await render(
, 1); + expect(e.className).toBe('test'); + }); + + itRenders( + 'no className prop when given a badly cased alias', + async render => { + const e = await render(
, 1); + expect(e.className).toBe('test'); + }, + ); + + itRenders('class for custom elements', async render => { + const e = await render(
, 0); + expect(e.getAttribute('class')).toBe('test'); + }); + + itRenders('className for custom elements', async render => { + const e = await render(
, 0); + expect(e.getAttribute('className')).toBe('test'); + }); }); describe('htmlFor property', function() { @@ -632,6 +649,12 @@ describe('ReactDOMServerIntegration', () => { expect(e.getAttribute('for')).toBe('myFor'); }); + itRenders('no badly cased htmlfor', async render => { + const e = await render(
, 1); + expect(e.hasAttribute('for')).toBe(false); + expect(e.getAttribute('htmlfor')).toBe('myFor'); + }); + itRenders('htmlFor with an empty string', async render => { const e = await render(
); expect(e.getAttribute('for')).toBe(''); @@ -653,6 +676,16 @@ describe('ReactDOMServerIntegration', () => { const e = await render(
); expect(e.hasAttribute('htmlFor')).toBe(false); }); + + itRenders('htmlFor attribute on custom elements', async render => { + const e = await render(
); + expect(e.getAttribute('htmlFor')).toBe('test'); + }); + + itRenders('for attribute on custom elements', async render => { + const e = await render(
); + expect(e.getAttribute('for')).toBe('test'); + }); }); describe('numeric properties', function() { @@ -778,10 +811,40 @@ describe('ReactDOMServerIntegration', () => { }); }); + describe('cased attributes', function() { + itRenders( + 'badly cased aliased HTML attribute with a warning', + async render => { + const e = await render(, 1); + expect(e.hasAttribute('http-equiv')).toBe(false); + expect(e.getAttribute('httpequiv')).toBe('refresh'); + }, + ); + + itRenders('badly cased SVG attribute with a warning', async render => { + const e = await render(, 1); + expect(e.getAttribute('textLength')).toBe('10'); + }); + + itRenders('no badly cased aliased SVG attribute alias', async render => { + const e = await render(, 1); + expect(e.hasAttribute('stroke-dasharray')).toBe(false); + expect(e.getAttribute('strokedasharray')).toBe('10 10'); + }); + + itRenders( + 'no badly cased original SVG attribute that is aliased', + async render => { + const e = await render(, 1); + expect(e.getAttribute('stroke-dasharray')).toBe('10 10'); + }, + ); + }); + describe('unknown attributes', function() { - itRenders('no unknown attributes', async render => { - const e = await render(
, 1); - expect(e.getAttribute('foo')).toBe(null); + itRenders('unknown attributes', async render => { + const e = await render(
); + expect(e.getAttribute('foo')).toBe('bar'); }); itRenders('unknown data- attributes', async render => { @@ -794,14 +857,34 @@ describe('ReactDOMServerIntegration', () => { expect(e.hasAttribute('data-foo')).toBe(false); }); + itRenders('unknown data- attributes with casing', async render => { + const e = await render(
); + expect(e.getAttribute('data-fooBar')).toBe('true'); + }); + + itRenders('unknown data- attributes with boolean true', async render => { + const e = await render(
); + expect(e.getAttribute('data-fooBar')).toBe('true'); + }); + + itRenders('unknown data- attributes with boolean false', async render => { + const e = await render(
); + expect(e.getAttribute('data-fooBar')).toBe('false'); + }); + itRenders( - 'no unknown attributes for non-standard elements', + 'no unknown data- attributes with casing and null value', async render => { - const e = await render(, 1); - expect(e.getAttribute('foo')).toBe(null); + const e = await render(
); + expect(e.hasAttribute('data-fooBar')).toBe(false); }, ); + itRenders('custom attributes for non-standard elements', async render => { + const e = await render(); + expect(e.getAttribute('foo')).toBe('bar'); + }); + itRenders('unknown attributes for custom elements', async render => { const e = await render(); expect(e.getAttribute('foo')).toBe('bar'); @@ -830,6 +913,11 @@ describe('ReactDOMServerIntegration', () => { expect(e.hasAttribute('foo')).toBe(false); }, ); + + itRenders('cased custom attributes', async render => { + const e = await render(
); + expect(e.getAttribute('fooBar')).toBe('test'); + }); }); itRenders('no HTML events', async render => { @@ -1172,7 +1260,7 @@ describe('ReactDOMServerIntegration', () => { expect(e.namespaceURI).toBe('http://www.w3.org/2000/svg'); }); - itRenders('svg element with an xlink', async render => { + itRenders('svg child element', async render => { let e = await render( , ); @@ -1185,6 +1273,20 @@ describe('ReactDOMServerIntegration', () => { ); }); + itRenders('no svg child element with a badly cased', async render => { + let e = await render( + , + 1, + ); + e = e.firstChild; + expect(e.hasAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe( + false, + ); + expect(e.getAttribute('xlinkhref')).toBe( + 'http://i.imgur.com/w7GCRPb.png', + ); + }); + itRenders('a math element', async render => { const e = await render(); expect(e.childNodes.length).toBe(0); @@ -2573,34 +2675,4 @@ describe('ReactDOMServerIntegration', () => {
"}} />, )); }); - - describe('dynamic injection', () => { - beforeEach(() => { - // HACK: we reset modules several times during the test which breaks - // dynamic injection. So we resort to telling resetModules() to run - // our custom init code every time after resetting. We could have a nicer - // way to do this, but this is the only test that needs it, and it will - // be removed anyway when we switch to static injection. - onAfterResetModules = () => { - const DOMProperty = require('DOMProperty'); - DOMProperty.injection.injectDOMPropertyConfig({ - isCustomAttribute: function(name) { - return name.indexOf('foo-') === 0; - }, - Properties: {foobar: null}, - }); - }; - resetModules(); - }); - - afterEach(() => { - onAfterResetModules = null; - }); - - itRenders('injected attributes', async render => { - const e = await render(
, 0); - expect(e.getAttribute('foobar')).toBe('simple'); - expect(e.getAttribute('foo-xyz')).toBe('simple'); - }); - }); }); diff --git a/src/renderers/dom/shared/hooks/ReactDOMInvalidARIAHook.js b/src/renderers/dom/shared/hooks/ReactDOMInvalidARIAHook.js index 7385c7a17639b..545d469f95fd2 100644 --- a/src/renderers/dom/shared/hooks/ReactDOMInvalidARIAHook.js +++ b/src/renderers/dom/shared/hooks/ReactDOMInvalidARIAHook.js @@ -15,6 +15,9 @@ var DOMProperty = require('DOMProperty'); var warnedProperties = {}; var rARIA = new RegExp('^(aria)-[' + DOMProperty.ATTRIBUTE_NAME_CHAR + ']*$'); +var rARIACamel = new RegExp( + '^(aria)[A-Z][' + DOMProperty.ATTRIBUTE_NAME_CHAR + ']*$', +); if (__DEV__) { var warning = require('fbjs/lib/warning'); @@ -23,6 +26,8 @@ if (__DEV__) { ReactDebugCurrentFrame, } = require('ReactGlobalSharedState'); var {getStackAddendumByID} = ReactComponentTreeHook; + + var validAriaProperties = require('./validAriaProperties'); } function getStackAddendum(debugID) { @@ -41,12 +46,42 @@ function validateProperty(tagName, name, debugID) { return true; } + if (rARIACamel.test(name)) { + var ariaName = 'aria-' + name.slice(4).toLowerCase(); + var correctName = validAriaProperties.hasOwnProperty(ariaName) + ? ariaName + : null; + + // If this is an aria-* attribute, but is not listed in the known DOM + // DOM properties, then it is an invalid aria-* attribute. + if (correctName == null) { + warning( + false, + 'Invalid ARIA attribute %s. ARIA attributes follow the pattern aria-* and must be lowercase.%s', + name, + getStackAddendum(debugID), + ); + warnedProperties[name] = true; + return true; + } + // aria-* attributes should be lowercase; suggest the lowercase version. + if (name !== correctName) { + warning( + false, + 'Invalid ARIA attribute %s. Did you mean %s?%s', + name, + correctName, + getStackAddendum(debugID), + ); + warnedProperties[name] = true; + return true; + } + } + if (rARIA.test(name)) { var lowerCasedName = name.toLowerCase(); - var standardName = DOMProperty.getPossibleStandardName.hasOwnProperty( - lowerCasedName, - ) - ? DOMProperty.getPossibleStandardName[lowerCasedName] + var standardName = validAriaProperties.hasOwnProperty(lowerCasedName) + ? lowerCasedName : null; // If this is an aria-* attribute, but is not listed in the known DOM diff --git a/src/renderers/dom/shared/hooks/ReactDOMUnknownPropertyHook.js b/src/renderers/dom/shared/hooks/ReactDOMUnknownPropertyHook.js index e68bf66d913fe..e26e0faa57732 100644 --- a/src/renderers/dom/shared/hooks/ReactDOMUnknownPropertyHook.js +++ b/src/renderers/dom/shared/hooks/ReactDOMUnknownPropertyHook.js @@ -35,39 +35,20 @@ function getStackAddendum(debugID) { } if (__DEV__) { - var reactProps = { - children: true, - dangerouslySetInnerHTML: true, - key: true, - ref: true, - - autoFocus: true, - defaultValue: true, - defaultChecked: true, - innerHTML: true, - suppressContentEditableWarning: true, - onFocusIn: true, - onFocusOut: true, - }; var warnedProperties = {}; var EVENT_NAME_REGEX = /^on[A-Z]/; + var ARIA_NAME_REGEX = /^aria-/i; + var possibleStandardNames = require('possibleStandardNames'); - var validateProperty = function(tagName, name, debugID) { - if ( - DOMProperty.properties.hasOwnProperty(name) || - DOMProperty.isCustomAttribute(name) - ) { - return true; - } - if ( - (reactProps.hasOwnProperty(name) && reactProps[name]) || - (warnedProperties.hasOwnProperty(name) && warnedProperties[name]) - ) { + var validateProperty = function(tagName, name, value, debugID) { + if (warnedProperties.hasOwnProperty(name) && warnedProperties[name]) { return true; } + if (EventPluginRegistry.registrationNameModules.hasOwnProperty(name)) { return true; } + if ( EventPluginRegistry.plugins.length === 0 && EVENT_NAME_REGEX.test(name) @@ -76,54 +57,100 @@ if (__DEV__) { // Don't check events in this case. return true; } - warnedProperties[name] = true; - var lowerCasedName = name.toLowerCase(); - - // data-* attributes should be lowercase; suggest the lowercase version - var standardName = DOMProperty.isCustomAttribute(lowerCasedName) - ? lowerCasedName - : DOMProperty.getPossibleStandardName.hasOwnProperty(lowerCasedName) - ? DOMProperty.getPossibleStandardName[lowerCasedName] - : null; + var lowerCasedName = name.toLowerCase(); var registrationName = EventPluginRegistry.possibleRegistrationNames.hasOwnProperty( lowerCasedName, ) ? EventPluginRegistry.possibleRegistrationNames[lowerCasedName] : null; - if (standardName != null) { + if (registrationName != null) { warning( false, - 'Unknown DOM property %s. Did you mean %s?%s', + 'Unknown event handler property `%s`. Did you mean `%s`?%s', name, - standardName, + registrationName, getStackAddendum(debugID), ); + warnedProperties[name] = true; + return true; + } + + // Let the ARIA attribute hook validate ARIA attributes + if (ARIA_NAME_REGEX.test(name)) { return true; - } else if (registrationName != null) { + } + + if (lowerCasedName === 'onfocusin' || lowerCasedName === 'onfocusout') { warning( false, - 'Unknown event handler property %s. Did you mean `%s`?%s', + 'React uses onFocus and onBlur instead of onFocusIn and onFocusOut. ' + + 'All React events are normalized to bubble, so onFocusIn and onFocusOut ' + + 'are not needed/supported by React.', + ); + warnedProperties[name] = true; + return true; + } + + if (lowerCasedName === 'innerhtml') { + warning( + false, + 'Directly setting property `innerHTML` is not permitted. ' + + 'For more information, lookup documentation on `dangerouslySetInnerHTML`.', + ); + warnedProperties[name] = true; + return true; + } + + if (typeof value === 'number' && isNaN(value)) { + warning( + false, + 'Received NaN for numeric attribute `%s`. If this is expected, cast ' + + 'the value to a string.%s', name, - registrationName, getStackAddendum(debugID), ); + warnedProperties[name] = true; return true; - } else { - // We were unable to guess which prop the user intended. - // It is likely that the user was just blindly spreading/forwarding props - // Components should be careful to only render valid props/attributes. - // Warning will be invoked in warnUnknownProperties to allow grouping. + } + + // Known attributes should match the casing specified in the property config. + if (possibleStandardNames.hasOwnProperty(lowerCasedName)) { + var standardName = possibleStandardNames[lowerCasedName]; + if (standardName !== name) { + warning( + false, + 'Invalid DOM property `%s`. Did you mean `%s`?%s', + name, + standardName, + getStackAddendum(debugID), + ); + warnedProperties[name] = true; + return true; + } + } + + // Now that we've validated casing, do not validate + // data types for reserved props + if (DOMProperty.isReservedProp(name)) { + return true; + } + + // Warn when a known attribute is a bad type + if (!DOMProperty.shouldSetAttribute(name, value)) { + warnedProperties[name] = true; return false; } + + return true; }; } var warnUnknownProperties = function(type, props, debugID) { var unknownProps = []; for (var key in props) { - var isValid = validateProperty(type, key, debugID); + var isValid = validateProperty(type, key, props[key], debugID); if (!isValid) { unknownProps.push(key); } @@ -134,7 +161,8 @@ var warnUnknownProperties = function(type, props, debugID) { if (unknownProps.length === 1) { warning( false, - 'Unknown prop %s on <%s> tag. Remove this prop from the element. ' + + 'Invalid prop %s on <%s> tag. Either remove this prop from the element, ' + + 'or pass a string or number value to keep it in the DOM. ' + 'For details, see https://fb.me/react-unknown-prop%s', unknownPropString, type, @@ -143,7 +171,8 @@ var warnUnknownProperties = function(type, props, debugID) { } else if (unknownProps.length > 1) { warning( false, - 'Unknown props %s on <%s> tag. Remove these props from the element. ' + + 'Invalid props %s on <%s> tag. Either remove these props from the element, ' + + 'or pass a string or number value to keep them in the DOM. ' + 'For details, see https://fb.me/react-unknown-prop%s', unknownPropString, type, diff --git a/src/renderers/dom/shared/hooks/possibleStandardNames.js b/src/renderers/dom/shared/hooks/possibleStandardNames.js new file mode 100644 index 0000000000000..1e7ef05597ad5 --- /dev/null +++ b/src/renderers/dom/shared/hooks/possibleStandardNames.js @@ -0,0 +1,312 @@ +/** + * 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 possibleStandardNames + */ + +var possibleStandardNames = { + // HTML + acceptcharset: 'acceptCharset', + 'accept-charset': 'acceptCharset', + accesskey: 'accessKey', + allowfullscreen: 'allowFullScreen', + allowtransparency: 'allowTransparency', + autocapitalize: 'autoCapitalize', + autocomplete: 'autoComplete', + autocorrect: 'autoCorrect', + autofocus: 'autoFocus', + autoplay: 'autoPlay', + autosave: 'autoSave', + cellpadding: 'cellPadding', + cellspacing: 'cellSpacing', + charset: 'charSet', + class: 'className', + classid: 'classID', + classname: 'className', + children: 'children', + colspan: 'colSpan', + contenteditable: 'contentEditable', + contextmenu: 'contextMenu', + controlslist: 'controlsList', + crossorigin: 'crossOrigin', + dangerouslysetinnerhtml: 'dangerouslySetInnerHTML', + datetime: 'dateTime', + defaultvalue: 'defaultValue', + defaultchecked: 'defaultChecked', + enctype: 'encType', + for: 'htmlFor', + formMethod: 'formMethod', + formaction: 'formAction', + formenctype: 'formEncType', + formnovalidate: 'formNoValidate', + formtarget: 'formTarget', + frameborder: 'frameBorder', + hreflang: 'hrefLang', + htmlfor: 'htmlFor', + httpequiv: 'httpEquiv', + 'http-equiv': 'httpEquiv', + innerhtml: 'innerHTML', + inputmode: 'inputMode', + itemid: 'itemID', + itemprop: 'itemProp', + itemref: 'itemRef', + itemscope: 'itemScope', + itemtype: 'itemType', + keyparams: 'keyParams', + keytype: 'keyType', + marginWidth: 'marginWidth', + marginheight: 'marginHeight', + maxlength: 'maxLength', + mediagroup: 'mediaGroup', + minLength: 'minlength', + novalidate: 'noValidate', + playsinline: 'playsInline', + radiogroup: 'radioGroup', + readonly: 'readOnly', + referrerpolicy: 'referrerPolicy', + rowspan: 'rowSpan', + spellcheck: 'spellCheck', + srcdoc: 'srcDoc', + srclang: 'srcLang', + srcset: 'srcSet', + tabindex: 'tabIndex', + usemap: 'useMap', + + // SVG + accentheight: 'accentHeight', + 'accent-height': 'accentHeight', + alignmentbaseline: 'alignmentBaseline', + 'alignment-baseline': 'alignmentBaseline', + allowreorder: 'allowReorder', + 'arabic-form': 'arabicForm', + arabicform: 'arabicForm', + attributename: 'attributeName', + attributetype: 'attributeType', + autoreverse: 'autoReverse', + basefrequency: 'baseFrequency', + baseprofile: 'baseProfile', + 'baseline-shift': 'baselineShift', + baselineshift: 'baselineShift', + calcmode: 'calcMode', + 'cap-height': 'capHeight', + capheight: 'capHeight', + 'clip-path': 'clipPath', + clippath: 'clipPath', + 'clip-rule': 'clipRule', + cliprule: 'clipRule', + clippathunits: 'clipPathUnits', + 'color-interpolation': 'colorInterpolation', + colorinterpolation: 'colorInterpolation', + 'color-interpolation-filters': 'colorInterpolationFilters', + colorinterpolationfilters: 'colorInterpolationFilters', + 'color-profile': 'colorProfile', + colorprofile: 'colorProfile', + 'color-rendering': 'colorRendering', + colorrendering: 'colorRendering', + contentscripttype: 'contentScriptType', + contentstyletype: 'contentStyleType', + diffuseconstant: 'diffuseConstant', + 'dominant-baseline': 'dominantBaseline', + dominantbaseline: 'dominantBaseline', + edgemode: 'edgeMode', + 'enable-background': 'enableBackground', + enablebackground: 'enableBackground', + externalresourcesrequired: 'externalResourcesRequired', + 'fill-opacity': 'fillOpacity', + fillopacity: 'fillOpacity', + 'fill-rule': 'fillRule', + fillrule: 'fillRule', + filterres: 'filterRes', + filterunits: 'filterUnits', + floodcolor: 'floodColor', + 'flood-color': 'floodColor', + 'flood-opacity': 'floodOpacity', + floodOpacity: 'floodOpacity', + 'font-family': 'fontFamily', + fontfamily: 'fontFamily', + 'font-size': 'fontSize', + fontsize: 'fontSize', + 'font-size-adjust': 'fontSizeAdjust', + fontsizeadjust: 'fontSizeAdjust', + 'font-stretch': 'fontStretch', + fontstretch: 'fontStretch', + 'font-style': 'fontStyle', + fontstyle: 'fontStyle', + 'font-variant': 'fontVariant', + fontvariant: 'fontVariant', + 'font-weight': 'fontWeight', + fontweight: 'fontWeight', + 'glyph-name': 'glyphName', + glyphname: 'glyphName', + 'glyph-orientation-horizontal': 'glyphOrientationHorizontal', + glyphorientationhorizontal: 'glyphOrientationHorizontal', + 'glyph-orientation-vertical': 'glyphOrientationVertical', + glyphorientationvertical: 'glyphOrientationVertical', + glyphref: 'glyphRef', + gradienttransform: 'gradientTransform', + gradientunits: 'gradientUnits', + 'horiz-adv-x': 'horizAdvX', + horizadvx: 'horizAdvX', + 'horiz-origin-x': 'horizOriginX', + horizoriginx: 'horizOriginX', + 'image-rendering': 'imageRendering', + imagerendering: 'imageRendering', + kernelmatrix: 'kernelMatrix', + kernelunitlength: 'kernelUnitLength', + keypoints: 'keyPoints', + keysplines: 'keySplines', + keytimes: 'keyTimes', + lengthadjust: 'lengthAdjust', + 'letter-spacing': 'letterSpacing', + letterspacing: 'letterSpacing', + 'lighting-color': 'lightingColor', + lightingcolor: 'lightingColor', + limitingconeangle: 'limitingConeAngle', + 'marker-end': 'markerEnd', + markerend: 'markerEnd', + 'marker-mid': 'markerMid', + markermid: 'markerMid', + 'marker-start': 'markerStart', + markerstart: 'markerStart', + markerheight: 'markerHeight', + markerunits: 'markerUnits', + markerwidth: 'markerWidth', + maskcontentunits: 'maskContentUnits', + maskunits: 'maskUnits', + numoctaves: 'numOctaves', + 'overline-position': 'overlinePosition', + overlineposition: 'overlinePosition', + 'overline-thickness': 'overlineThickness', + overlinethickness: 'overlineThickness', + 'paint-order': 'paintOrder', + paintorder: 'paintOrder', + 'panose-1': 'panose1', + pathlength: 'pathLength', + patterncontentunits: 'patternContentUnits', + patterntransform: 'patternTransform', + patternunits: 'patternUnits', + 'pointer-events': 'pointerEvents', + pointerevents: 'pointerEvents', + pointsatx: 'pointsAtX', + pointsaty: 'pointsAtY', + pointsatz: 'pointsAtZ', + preservealpha: 'preserveAlpha', + preserveaspectratio: 'preserveAspectRatio', + primitiveunits: 'primitiveUnits', + refx: 'refX', + refy: 'refY', + 'rendering-intent': 'renderingIntent', + renderingintent: 'renderingIntent', + repeatcount: 'repeatCount', + repeatdur: 'repeatDur', + requiredextensions: 'requiredExtensions', + requiredfeatures: 'requiredFeatures', + 'shape-rendering': 'shapeRendering', + shaperendering: 'shapeRendering', + specularconstant: 'specularConstant', + specularexponent: 'specularExponent', + spreadmethod: 'spreadMethod', + startoffset: 'startOffset', + stddeviation: 'stdDeviation', + stitchtiles: 'stitchTiles', + 'stop-color': 'stopColor', + stopcolor: 'stopColor', + stopopacity: 'stopOpacity', + 'stop-opacity': 'stopOpacity', + 'strikethrough-position': 'strikethroughPosition', + strikethroughposition: 'strikethroughPosition', + strikethroughthickness: 'strikethroughThickness', + 'strikethrough-thickness': 'strikethroughThickness', + 'stroke-dasharray': 'strokeDasharray', + strokedasharray: 'strokeDasharray', + 'stroke-dashoffset': 'strokeDashoffset', + strokedashoffset: 'strokeDashoffset', + 'stroke-linecap': 'strokeLinecap', + strokelinecap: 'strokeLinecap', + 'stroke-linejoin': 'strokeLinejoin', + strokelinejoin: 'strokeLinejoin', + 'stroke-miterlimit': 'strokeMiterlimit', + strokemiterlimit: 'strokeMiterlimit', + 'stroke-opacity': 'strokeOpacity', + strokeopacity: 'strokeOpacity', + 'stroke-width': 'strokeWidth', + suppresscontenteditablewarning: 'suppressContentEditableWarning', + surfacescale: 'surfaceScale', + systemlanguage: 'systemLanguage', + tablevalues: 'tableValues', + targetx: 'targetX', + targety: 'targetY', + 'text-anchor': 'textAnchor', + textanchor: 'textAnchor', + 'text-decoration': 'textDecoration', + textdecoration: 'textDecoration', + 'text-rendering': 'textRendering', + textrendering: 'textRendering', + textlength: 'textLength', + 'underline-position': 'underlinePosition', + underlineposition: 'underlinePosition', + 'underline-thickness': 'underlineThickness', + underlinethickness: 'underlineThickness', + 'unicode-bidi': 'unicodeBidi', + unicodebidi: 'unicodeBidi', + 'unicode-range': 'unicodeRange', + unicoderange: 'unicodeRange', + 'units-per-em': 'unitsPerEm', + unitsperem: 'unitsPerEm', + 'v-alphabetic': 'vAlphabetic', + valphabetic: 'vAlphabetic', + 'v-hanging': 'vHanging', + vhanging: 'vHanging', + 'v-ideographic': 'vIdeographic', + videographic: 'vIdeographic', + 'v-mathematical': 'vMathematical', + vmathematical: 'vMathematical', + 'vector-effect': 'vectorEffect', + vectoreffect: 'vectorEffect', + 'vert-adv-y': 'vertAdvY', + vertadvy: 'vertAdvY', + 'vert-origin-x': 'vertOriginX', + vertoriginx: 'vertOriginX', + 'vert-origin-y': 'vertOriginY', + vertoriginy: 'vertOriginY', + viewbox: 'viewBox', + viewtarget: 'viewTarget', + 'word-spacing': 'wordSpacing', + wordspacing: 'wordSpacing', + 'writing-mode': 'writingMode', + writingmode: 'writingMode', + 'x-height': 'xHeight', + xheight: 'xHeight', + xchannelselector: 'xChannelSelector', + 'xlink:actuate': 'xlinkActuate', + xlinkactuate: 'xlinkActuate', + 'xlink:arcrole': 'xlinkArcrole', + xlinkarcrole: 'xlinkArcrole', + 'xlink:href': 'xlinkHref', + xlinkhref: 'xlinkHref', + 'xlink:role': 'xlinkRole', + xlinkrole: 'xlinkRole', + 'xlink:show': 'xlinkShow', + xlinkshow: 'xlinkShow', + 'xlink:title': 'xlinkTitle', + xlinktitle: 'xlinkTitle', + 'xlink:type': 'xlinkType', + xlinktype: 'xlinkType', + 'xml:base': 'xmlBase', + xmlbase: 'xmlBase', + 'xmlns:xlink': 'xmlnsXlink', + xmlnsxlink: 'xmlnsXlink', + 'xml:lang': 'xmlLang', + xmllang: 'xmlLang', + 'xml:space': 'xmlSpace', + xmlspace: 'xmlSpace', + ychannelselector: 'yChannelSelector', + zoomandpan: 'zoomAndPan', +}; + +module.exports = possibleStandardNames; diff --git a/src/renderers/dom/shared/hooks/validAriaProperties.js b/src/renderers/dom/shared/hooks/validAriaProperties.js new file mode 100644 index 0000000000000..9a75a6201b8a4 --- /dev/null +++ b/src/renderers/dom/shared/hooks/validAriaProperties.js @@ -0,0 +1,69 @@ +/** + * 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 validAriaProperties + */ + +'use strict'; + +var ariaProperties = { + 'aria-current': 0, // state + 'aria-details': 0, + 'aria-disabled': 0, // state + 'aria-hidden': 0, // state + 'aria-invalid': 0, // state + 'aria-keyshortcuts': 0, + 'aria-label': 0, + 'aria-roledescription': 0, + // Widget Attributes + 'aria-autocomplete': 0, + 'aria-checked': 0, + 'aria-expanded': 0, + 'aria-haspopup': 0, + 'aria-level': 0, + 'aria-modal': 0, + 'aria-multiline': 0, + 'aria-multiselectable': 0, + 'aria-orientation': 0, + 'aria-placeholder': 0, + 'aria-pressed': 0, + 'aria-readonly': 0, + 'aria-required': 0, + 'aria-selected': 0, + 'aria-sort': 0, + 'aria-valuemax': 0, + 'aria-valuemin': 0, + 'aria-valuenow': 0, + 'aria-valuetext': 0, + // Live Region Attributes + 'aria-atomic': 0, + 'aria-busy': 0, + 'aria-live': 0, + 'aria-relevant': 0, + // Drag-and-Drop Attributes + 'aria-dropeffect': 0, + 'aria-grabbed': 0, + // Relationship Attributes + 'aria-activedescendant': 0, + 'aria-colcount': 0, + 'aria-colindex': 0, + 'aria-colspan': 0, + 'aria-controls': 0, + 'aria-describedby': 0, + 'aria-errormessage': 0, + 'aria-flowto': 0, + 'aria-labelledby': 0, + 'aria-owns': 0, + 'aria-posinset': 0, + 'aria-rowcount': 0, + 'aria-rowindex': 0, + 'aria-rowspan': 0, + 'aria-setsize': 0, +}; + +module.exports = ariaProperties; diff --git a/src/renderers/dom/shared/utils/assertValidProps.js b/src/renderers/dom/shared/utils/assertValidProps.js index 27f7b39641f95..5de6bf0032bee 100644 --- a/src/renderers/dom/shared/utils/assertValidProps.js +++ b/src/renderers/dom/shared/utils/assertValidProps.js @@ -63,11 +63,6 @@ function assertValidProps( ); } if (__DEV__) { - warning( - props.innerHTML == null, - 'Directly setting property `innerHTML` is not permitted. ' + - 'For more information, lookup documentation on `dangerouslySetInnerHTML`.', - ); warning( props.suppressContentEditableWarning || !props.contentEditable || @@ -77,12 +72,6 @@ function assertValidProps( 'those nodes are unexpectedly modified or duplicated. This is ' + 'probably not intentional.', ); - warning( - props.onFocusIn == null && props.onFocusOut == null, - 'React uses onFocus and onBlur instead of onFocusIn and onFocusOut. ' + - 'All React events are normalized to bubble, so onFocusIn and onFocusOut ' + - 'are not needed/supported by React.', - ); } invariant( props.style == null || typeof props.style === 'object', diff --git a/src/renderers/dom/stack/client/ReactDOMComponent.js b/src/renderers/dom/stack/client/ReactDOMComponent.js index 5453df69b531e..6d1a3f0b274db 100644 --- a/src/renderers/dom/stack/client/ReactDOMComponent.js +++ b/src/renderers/dom/stack/client/ReactDOMComponent.js @@ -57,11 +57,6 @@ var CONTENT_TYPES = {string: true, number: true}; var STYLE = 'style'; var HTML = '__html'; -var RESERVED_PROPS = { - children: null, - dangerouslySetInnerHTML: null, - suppressContentEditableWarning: null, -}; function getDeclarationErrorAddendum(internalInstance) { if (internalInstance) { @@ -108,11 +103,6 @@ function assertValidProps(component, props) { ); } if (__DEV__) { - warning( - props.innerHTML == null, - 'Directly setting property `innerHTML` is not permitted. ' + - 'For more information, lookup documentation on `dangerouslySetInnerHTML`.', - ); warning( props.suppressContentEditableWarning || !props.contentEditable || @@ -122,12 +112,6 @@ function assertValidProps(component, props) { 'those nodes are unexpectedly modified or duplicated. This is ' + 'probably not intentional.', ); - warning( - props.onFocusIn == null && props.onFocusOut == null, - 'React uses onFocus and onBlur instead of onFocusIn and onFocusOut. ' + - 'All React events are normalized to bubble, so onFocusIn and onFocusOut ' + - 'are not needed/supported by React.', - ); } invariant( props.style == null || typeof props.style === 'object', @@ -710,7 +694,7 @@ ReactDOMComponent.Mixin = { } var markup = null; if (this._tag != null && isCustomComponent(this._tag, props)) { - if (!RESERVED_PROPS.hasOwnProperty(propKey)) { + if (!DOMProperty.isReservedProp(propKey)) { markup = DOMMarkupOperations.createMarkupForCustomAttribute( propKey, propValue, @@ -965,15 +949,12 @@ ReactDOMComponent.Mixin = { } } else if (registrationNameModules.hasOwnProperty(propKey)) { // Do nothing for event names. - } else if (isCustomComponent(this._tag, lastProps)) { - if (!RESERVED_PROPS.hasOwnProperty(propKey)) { + } else if (!DOMProperty.isReservedProp(propKey)) { + if (isCustomComponent(this._tag, lastProps)) { DOMPropertyOperations.deleteValueForAttribute(getNode(this), propKey); + } else { + DOMPropertyOperations.deleteValueForProperty(getNode(this), propKey); } - } else if ( - DOMProperty.properties[propKey] || - DOMProperty.isCustomAttribute(propKey) - ) { - DOMPropertyOperations.deleteValueForProperty(getNode(this), propKey); } } for (propKey in nextProps) { @@ -1022,17 +1003,14 @@ ReactDOMComponent.Mixin = { ensureListeningTo(this, propKey, transaction); } } else if (isCustomComponentTag) { - if (!RESERVED_PROPS.hasOwnProperty(propKey)) { + if (!DOMProperty.isReservedProp(propKey)) { DOMPropertyOperations.setValueForAttribute( getNode(this), propKey, nextProp, ); } - } else if ( - DOMProperty.properties[propKey] || - DOMProperty.isCustomAttribute(propKey) - ) { + } else if (!DOMProperty.isReservedProp(propKey)) { var node = getNode(this); // If we're updating to null or undefined, we should remove the property // from the DOM node instead of inadvertently setting to a string. This