@samitouri / QOS-React / commits / 2999811b07

Custom Attributes Scenario 2: Write badly cased attributes. Remove most of whitelist. (#10385)

* Allow custom attributes. Add flag to toggle custom attribute behavior * Update custom attribute logic - Only allow string attributes - Remove custom attribute feature flag - Add additional tests for data, aria, and custom attributes * Allow numbers and booleans custom attributes. Cut isCustomAttribute * Cover objects with custom attributes in warning test * Rename DOMProperty.isWriteable to shouldSetAttribute * Rework conditions in shouldSetProperty to avoid edge cases * Update unknown property warning to include custom attribute information * Remove ref and key from reserved props * Ensure SSR test coverage for DOMProperty injections * Add ajaxify attribute for internal FB support * Ajaxify is a stringifiable object attribute * Remove non-case sensitive standard attributes. Make ARIA hook dev only. * Update test name for custom attributes on custom elements * Remove SSR custom injection test * Remove case sensitive props * Remove onAfterResetModules hooks in SSR render tests * Add back a few attributes and explain why they are needed * Remove possibleStandardNames from DOMProperty.js * Fix typo in HTMLPropertyConfig comment * Remove duplicative comment * Add back loop boolean property * Do not allow assignment of attributes that are aliased * Update custom attribute test to check value, not just presence * Address case where class is assigned as an attribute on custom elements. Improve SSR tests * Cover cases where className and for are given to custom elements * Remove unnecessary spys on console.error. Reduce extra space in tests * Cover cased custom attributes in SSR tests * Custom attributes are case sensitive * Allow uppercase letters in custom attributes. Address associated edge cases * Allow improperly cased aliased attributes. Add additional tests * Handle special properties like onFocusOut * Add some comments to document where casing matters. Remove DOMPropertyNames * Fix spelling mistake in ajaxify HTML property comment * Make ARIA enforcement dev-only * Remove alias test that covers multiple aliases for one property * Fix typo in comment * Build SVG aliases dynamically * Remove unused DOMPropertyNames reference * Do not translate bad casings of aliased attributes - classname writes to the DOM as classname - class does not write to the DOM - cLASS does not write to the DOM - arabic-form does not write to the DOM * Revise the way custom booleans are treated - Custom attributes can not have boolean values unless they are aria or data attributes - Attributes with boolean values have been added back to the whitelist - Warnings now exclude booleans from supported types - Associated test coverage * Add developer warnings for NaN and ARIA hooks * Use string comparison instead of regex to check for data and aria attributes. * Warn about unsupported properties without case sensitivity * Remove attributes that are updated to invalid values * Support object property values with toString methods. Allow boolean props to coerce objects * Add back ajaxify test * Address bad references in ReactDOMComponent-test. Format. * Revert changes to the docs We'll update them separately * Allow all objects and pass incorrect aliases

Nathan Hunzaker committed Aug 15, 2017 at 12:00 UTC 2999811b07a5eb9e04680457a0b56195abd7e08c
18 files changed +1183 -746
src/renderers/dom/fiber/ReactDOMFiberComponent.js
+29 -39
@@ -227,20 +227,11 @@ function setInitialDOMProperties(
227 }
228 } else if (isCustomComponentTag) {
229 DOMPropertyOperations.setValueForAttribute(domElement, propKey, nextProp);
230 - } else if (
231 - DOMProperty.properties[propKey] ||
232 - DOMProperty.isCustomAttribute(propKey)
233 - ) {
230 + } else if (nextProp != null) {
231 // If we're updating to null or undefined, we should remove the property
232 // from the DOM node instead of inadvertently setting to a string. This
233 // brings us in line with the same behavior we have on initial render.
237 - if (nextProp != null) {
238 - DOMPropertyOperations.setValueForProperty(
239 - domElement,
240 - propKey,
241 - nextProp,
242 - );
243 - }
234 + DOMPropertyOperations.setValueForProperty(domElement, propKey, nextProp);
235 }
236 }
237 }
@@ -271,22 +262,13 @@ function updateDOMProperties(
262 } else {
263 DOMPropertyOperations.deleteValueForAttribute(domElement, propKey);
264 }
274 - } else if (
275 - DOMProperty.properties[propKey] ||
276 - DOMProperty.isCustomAttribute(propKey)
277 - ) {
265 + } else if (propValue != null) {
266 + DOMPropertyOperations.setValueForProperty(domElement, propKey, propValue);
267 + } else {
268 // If we're updating to null or undefined, we should remove the property
269 // from the DOM node instead of inadvertently setting to a string. This
270 // brings us in line with the same behavior we have on initial render.
281 - if (propValue != null) {
282 - DOMPropertyOperations.setValueForProperty(
283 - domElement,
284 - propKey,
285 - propValue,
286 - );
287 - } else {
288 - DOMPropertyOperations.deleteValueForProperty(domElement, propKey);
289 - }
271 + DOMPropertyOperations.deleteValueForProperty(domElement, propKey);
272 }
273 }
274 }
@@ -901,8 +883,7 @@ var ReactDOMFiberComponent = {
883 var extraAttributeNames: Set<string> = new Set();
884 var attributes = domElement.attributes;
885 for (var i = 0; i < attributes.length; i++) {
904 - // TODO: Do we need to lower case this to get case insensitive matches?
905 - var name = attributes[i].name;
886 + var name = attributes[i].name.toLowerCase();
887 switch (name) {
888 // Built-in SSR attribute is whitelisted
889 case 'data-reactroot':
@@ -986,28 +967,37 @@ var ReactDOMFiberComponent = {
967 if (expectedStyle !== serverValue) {
968 warnForPropDifference(propKey, serverValue, expectedStyle);
969 }
989 - } else if (
990 - isCustomComponentTag ||
991 - DOMProperty.isCustomAttribute(propKey)
992 - ) {
970 + } else if (isCustomComponentTag) {
971 // $FlowFixMe - Should be inferred as not undefined.
994 - extraAttributeNames.delete(propKey);
972 + extraAttributeNames.delete(propKey.toLowerCase());
973 serverValue = DOMPropertyOperations.getValueForAttribute(
974 domElement,
975 propKey,
976 nextProp,
977 );
978 +
979 if (nextProp !== serverValue) {
980 warnForPropDifference(propKey, serverValue, nextProp);
981 }
1003 - } else if ((propertyInfo = DOMProperty.properties[propKey])) {
1004 - // $FlowFixMe - Should be inferred as not undefined.
1005 - extraAttributeNames.delete(propertyInfo.attributeName);
1006 - serverValue = DOMPropertyOperations.getValueForProperty(
1007 - domElement,
1008 - propKey,
1009 - nextProp,
1010 - );
982 + } else if (DOMProperty.shouldSetAttribute(propKey, nextProp)) {
983 + if ((propertyInfo = DOMProperty.getPropertyInfo(propKey))) {
984 + // $FlowFixMe - Should be inferred as not undefined.
985 + extraAttributeNames.delete(propertyInfo.attributeName);
986 + serverValue = DOMPropertyOperations.getValueForProperty(
987 + domElement,
988 + propKey,
989 + nextProp,
990 + );
991 + } else {
992 + // $FlowFixMe - Should be inferred as not undefined.
993 + extraAttributeNames.delete(propKey.toLowerCase());
994 + serverValue = DOMPropertyOperations.getValueForAttribute(
995 + domElement,
996 + propKey,
997 + nextProp,
998 + );
999 + }
1000 +
1001 if (nextProp !== serverValue) {
1002 warnForPropDifference(propKey, serverValue, nextProp);
1003 }
src/renderers/dom/shared/ARIADOMPropertyConfig.js deleted
-74
@@ -1,74 +0,0 @@
1 -/**
2 - * Copyright 2013-present, Facebook, Inc.
3 - * All rights reserved.
4 - *
5 - * This source code is licensed under the BSD-style license found in the
6 - * LICENSE file in the root directory of this source tree. An additional grant
7 - * of patent rights can be found in the PATENTS file in the same directory.
8 - *
9 - * @providesModule ARIADOMPropertyConfig
10 - */
11 -
12 -'use strict';
13 -
14 -var ARIADOMPropertyConfig = {
15 - Properties: {
16 - // Global States and Properties
17 - 'aria-current': 0, // state
18 - 'aria-details': 0,
19 - 'aria-disabled': 0, // state
20 - 'aria-hidden': 0, // state
21 - 'aria-invalid': 0, // state
22 - 'aria-keyshortcuts': 0,
23 - 'aria-label': 0,
24 - 'aria-roledescription': 0,
25 - // Widget Attributes
26 - 'aria-autocomplete': 0,
27 - 'aria-checked': 0,
28 - 'aria-expanded': 0,
29 - 'aria-haspopup': 0,
30 - 'aria-level': 0,
31 - 'aria-modal': 0,
32 - 'aria-multiline': 0,
33 - 'aria-multiselectable': 0,
34 - 'aria-orientation': 0,
35 - 'aria-placeholder': 0,
36 - 'aria-pressed': 0,
37 - 'aria-readonly': 0,
38 - 'aria-required': 0,
39 - 'aria-selected': 0,
40 - 'aria-sort': 0,
41 - 'aria-valuemax': 0,
42 - 'aria-valuemin': 0,
43 - 'aria-valuenow': 0,
44 - 'aria-valuetext': 0,
45 - // Live Region Attributes
46 - 'aria-atomic': 0,
47 - 'aria-busy': 0,
48 - 'aria-live': 0,
49 - 'aria-relevant': 0,
50 - // Drag-and-Drop Attributes
51 - 'aria-dropeffect': 0,
52 - 'aria-grabbed': 0,
53 - // Relationship Attributes
54 - 'aria-activedescendant': 0,
55 - 'aria-colcount': 0,
56 - 'aria-colindex': 0,
57 - 'aria-colspan': 0,
58 - 'aria-controls': 0,
59 - 'aria-describedby': 0,
60 - 'aria-errormessage': 0,
61 - 'aria-flowto': 0,
62 - 'aria-labelledby': 0,
63 - 'aria-owns': 0,
64 - 'aria-posinset': 0,
65 - 'aria-rowcount': 0,
66 - 'aria-rowindex': 0,
67 - 'aria-rowspan': 0,
68 - 'aria-setsize': 0,
69 - },
70 - DOMAttributeNames: {},
71 - DOMPropertyNames: {},
72 -};
73 -
74 -module.exports = ARIADOMPropertyConfig;
src/renderers/dom/shared/DOMMarkupOperations.js
+2 -4
@@ -88,9 +88,7 @@ var DOMMarkupOperations = {
88 * @return {?string} Markup string, or null if the property was invalid.
89 */
90 createMarkupForProperty: function(name, value) {
91 - var propertyInfo = DOMProperty.properties.hasOwnProperty(name)
92 - ? DOMProperty.properties[name]
93 - : null;
91 + var propertyInfo = DOMProperty.getPropertyInfo(name);
92 if (propertyInfo) {
93 if (shouldIgnoreValue(propertyInfo, value)) {
94 return '';
@@ -103,7 +101,7 @@ var DOMMarkupOperations = {
101 return attributeName + '=""';
102 }
103 return attributeName + '=' + quoteAttributeValueForBrowser(value);
106 - } else if (DOMProperty.isCustomAttribute(name)) {
104 + } else if (DOMProperty.shouldSetAttribute(name, value)) {
105 if (value == null) {
106 return '';
107 }
src/renderers/dom/shared/DOMProperty.js
+67 -45
@@ -13,6 +13,21 @@
13
14 var invariant = require('fbjs/lib/invariant');
15
16 +// These attributes should be all lowercase to allow for
17 +// case insensitive checks
18 +var RESERVED_PROPS = {
19 + children: true,
20 + dangerouslysetinnerhtml: true,
21 + autofocus: true,
22 + defaultvalue: true,
23 + defaultchecked: true,
24 + innerhtml: true,
25 + suppresscontenteditablewarning: true,
26 + onfocusin: true,
27 + onfocusout: true,
28 + style: true,
29 +};
30 +
31 function checkMask(value, bitmask) {
32 return (value & bitmask) === bitmask;
33 }
@@ -32,11 +47,6 @@ var DOMPropertyInjection = {
47 * Inject some specialized knowledge about the DOM. This takes a config object
48 * with the following properties:
49 *
35 - * isCustomAttribute: function that given an attribute name will return true
36 - * if it can be inserted into the DOM verbatim. Useful for data-* or aria-*
37 - * attributes where it's impossible to enumerate all of the possible
38 - * attribute names,
39 - *
50 * Properties: object mapping DOM property name to one of the
51 * DOMPropertyInjection constants or null. If your attribute isn't in here,
52 * it won't get written to the DOM.
@@ -61,15 +71,8 @@ var DOMPropertyInjection = {
71 var Properties = domPropertyConfig.Properties || {};
72 var DOMAttributeNamespaces = domPropertyConfig.DOMAttributeNamespaces || {};
73 var DOMAttributeNames = domPropertyConfig.DOMAttributeNames || {};
64 - var DOMPropertyNames = domPropertyConfig.DOMPropertyNames || {};
74 var DOMMutationMethods = domPropertyConfig.DOMMutationMethods || {};
75
67 - if (domPropertyConfig.isCustomAttribute) {
68 - DOMProperty._isCustomAttributeFunctions.push(
69 - domPropertyConfig.isCustomAttribute,
70 - );
71 - }
72 -
76 for (var propName in Properties) {
77 invariant(
78 !DOMProperty.properties.hasOwnProperty(propName),
@@ -111,30 +114,24 @@ var DOMPropertyInjection = {
114 propName,
115 );
116
114 - if (__DEV__) {
115 - DOMProperty.getPossibleStandardName[lowerCased] = propName;
116 - }
117 -
117 if (DOMAttributeNames.hasOwnProperty(propName)) {
118 var attributeName = DOMAttributeNames[propName];
119 +
120 propertyInfo.attributeName = attributeName;
121 - if (__DEV__) {
122 - DOMProperty.getPossibleStandardName[attributeName] = propName;
123 - }
121 }
122
123 if (DOMAttributeNamespaces.hasOwnProperty(propName)) {
124 propertyInfo.attributeNamespace = DOMAttributeNamespaces[propName];
125 }
126
130 - if (DOMPropertyNames.hasOwnProperty(propName)) {
131 - propertyInfo.propertyName = DOMPropertyNames[propName];
132 - }
133 -
127 if (DOMMutationMethods.hasOwnProperty(propName)) {
128 propertyInfo.mutationMethod = DOMMutationMethods[propName];
129 }
130
131 + // Downcase references to whitelist properties to check for membership
132 + // without case-sensitivity. This allows the whitelist to pick up
133 + // `allowfullscreen`, which should be written using the property configuration
134 + // for `allowFullscreen`
135 DOMProperty.properties[propName] = propertyInfo;
136 }
137 },
@@ -197,33 +194,58 @@ var DOMProperty = {
194 properties: {},
195
196 /**
200 - * Mapping from lowercase property names to the properly cased version, used
201 - * to warn in the case of missing properties. Available only in __DEV__.
202 - *
203 - * autofocus is predefined, because adding it to the property whitelist
204 - * causes unintended side effects.
205 - *
206 - * @type {Object}
197 + * Checks whether a property name is a writeable attribute.
198 + * @method
199 */
208 - getPossibleStandardName: __DEV__ ? {autofocus: 'autoFocus'} : null,
200 + shouldSetAttribute: function(name, value) {
201 + if (DOMProperty.isReservedProp(name)) {
202 + return false;
203 + }
204
210 - /**
211 - * All of the isCustomAttribute() functions that have been injected.
212 - */
213 - _isCustomAttributeFunctions: [],
205 + if (value === null) {
206 + return true;
207 + }
208
215 - /**
216 - * Checks whether a property name is a custom attribute.
217 - * @method
218 - */
219 - isCustomAttribute: function(attributeName) {
220 - for (var i = 0; i < DOMProperty._isCustomAttributeFunctions.length; i++) {
221 - var isCustomAttributeFn = DOMProperty._isCustomAttributeFunctions[i];
222 - if (isCustomAttributeFn(attributeName)) {
209 + var lowerCased = name.toLowerCase();
210 +
211 + var propertyInfo = DOMProperty.properties[name];
212 +
213 + switch (typeof value) {
214 + case 'boolean':
215 + if (propertyInfo) {
216 + return true;
217 + }
218 + var prefix = lowerCased.slice(0, 5);
219 + return prefix === 'data-' || prefix === 'aria-';
220 + case 'undefined':
221 + case 'number':
222 + case 'string':
223 return true;
224 - }
224 + case 'object':
225 + return true;
226 + default:
227 + // function, symbol
228 + return false;
229 }
226 - return false;
230 + },
231 +
232 + getPropertyInfo(name) {
233 + return DOMProperty.properties.hasOwnProperty(name)
234 + ? DOMProperty.properties[name]
235 + : null;
236 + },
237 +
238 + /**
239 + * Checks to see if a property name is within the list of properties
240 + * reserved for internal React operations. These properties should
241 + * not be set on an HTML element.
242 + *
243 + * @private
244 + * @param {string} name
245 + * @return {boolean} If the name is within reserved props
246 + */
247 + isReservedProp(name) {
248 + return RESERVED_PROPS.hasOwnProperty(name.toLowerCase());
249 },
250
251 injection: DOMPropertyInjection,
src/renderers/dom/shared/DOMPropertyOperations.js
+12 -13
@@ -79,9 +79,7 @@ var DOMPropertyOperations = {
79 */
80 getValueForProperty: function(node, name, expected) {
81 if (__DEV__) {
82 - var propertyInfo = DOMProperty.properties.hasOwnProperty(name)
83 - ? DOMProperty.properties[name]
84 - : null;
82 + var propertyInfo = DOMProperty.getPropertyInfo(name);
83 if (propertyInfo) {
84 var mutationMethod = propertyInfo.mutationMethod;
85 if (mutationMethod || propertyInfo.mustUseProperty) {
@@ -164,10 +162,9 @@ var DOMPropertyOperations = {
162 * @param {*} value
163 */
164 setValueForProperty: function(node, name, value) {
167 - var propertyInfo = DOMProperty.properties.hasOwnProperty(name)
168 - ? DOMProperty.properties[name]
169 - : null;
170 - if (propertyInfo) {
165 + var propertyInfo = DOMProperty.getPropertyInfo(name);
166 +
167 + if (propertyInfo && DOMProperty.shouldSetAttribute(name, value)) {
168 var mutationMethod = propertyInfo.mutationMethod;
169 if (mutationMethod) {
170 mutationMethod(node, value);
@@ -194,8 +191,12 @@ var DOMPropertyOperations = {
191 node.setAttribute(attributeName, '' + value);
192 }
193 }
197 - } else if (DOMProperty.isCustomAttribute(name)) {
198 - DOMPropertyOperations.setValueForAttribute(node, name, value);
194 + } else {
195 + DOMPropertyOperations.setValueForAttribute(
196 + node,
197 + name,
198 + DOMProperty.shouldSetAttribute(name, value) ? value : null,
199 + );
200 return;
201 }
202
@@ -255,9 +256,7 @@ var DOMPropertyOperations = {
256 * @param {string} name
257 */
258 deleteValueForProperty: function(node, name) {
258 - var propertyInfo = DOMProperty.properties.hasOwnProperty(name)
259 - ? DOMProperty.properties[name]
260 - : null;
259 + var propertyInfo = DOMProperty.getPropertyInfo(name);
260 if (propertyInfo) {
261 var mutationMethod = propertyInfo.mutationMethod;
262 if (mutationMethod) {
@@ -272,7 +271,7 @@ var DOMPropertyOperations = {
271 } else {
272 node.removeAttribute(propertyInfo.attributeName);
273 }
275 - } else if (DOMProperty.isCustomAttribute(name)) {
274 + } else {
275 node.removeAttribute(name);
276 }
277
src/renderers/dom/shared/HTMLDOMPropertyConfig.js
+22 -140
@@ -22,189 +22,72 @@ var HAS_OVERLOADED_BOOLEAN_VALUE =
22 DOMProperty.injection.HAS_OVERLOADED_BOOLEAN_VALUE;
23
24 var HTMLDOMPropertyConfig = {
25 - isCustomAttribute: RegExp.prototype.test.bind(
26 - new RegExp('^(data|aria)-[' + DOMProperty.ATTRIBUTE_NAME_CHAR + ']*$'),
27 - ),
25 Properties: {
29 - /**
30 - * Standard Properties
31 - */
32 - accept: 0,
33 - acceptCharset: 0,
34 - accessKey: 0,
35 - action: 0,
26 allowFullScreen: HAS_BOOLEAN_VALUE,
37 - allowTransparency: 0,
38 - alt: 0,
27 // specifies target context for links with `preload` type
40 - as: 0,
28 async: HAS_BOOLEAN_VALUE,
42 - autoComplete: 0,
29 // autoFocus is polyfilled/normalized by AutoFocusUtils
30 // autoFocus: HAS_BOOLEAN_VALUE,
31 autoPlay: HAS_BOOLEAN_VALUE,
32 capture: HAS_BOOLEAN_VALUE,
47 - cellPadding: 0,
48 - cellSpacing: 0,
49 - charSet: 0,
50 - challenge: 0,
33 checked: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
52 - cite: 0,
53 - classID: 0,
54 - className: 0,
34 cols: HAS_POSITIVE_NUMERIC_VALUE,
56 - colSpan: 0,
57 - content: 0,
58 - contentEditable: 0,
59 - contextMenu: 0,
35 controls: HAS_BOOLEAN_VALUE,
61 - controlsList: 0,
62 - coords: 0,
63 - crossOrigin: 0,
64 - data: 0, // For `<object />` acts as `src`.
65 - dateTime: 0,
36 default: HAS_BOOLEAN_VALUE,
37 defer: HAS_BOOLEAN_VALUE,
68 - dir: 0,
38 disabled: HAS_BOOLEAN_VALUE,
39 download: HAS_OVERLOADED_BOOLEAN_VALUE,
71 - draggable: 0,
72 - encType: 0,
73 - form: 0,
74 - formAction: 0,
75 - formEncType: 0,
76 - formMethod: 0,
40 formNoValidate: HAS_BOOLEAN_VALUE,
78 - formTarget: 0,
79 - frameBorder: 0,
80 - headers: 0,
81 - height: 0,
41 hidden: HAS_BOOLEAN_VALUE,
83 - high: 0,
84 - href: 0,
85 - hrefLang: 0,
86 - htmlFor: 0,
87 - httpEquiv: 0,
88 - id: 0,
89 - inputMode: 0,
90 - integrity: 0,
91 - is: 0,
92 - keyParams: 0,
93 - keyType: 0,
94 - kind: 0,
95 - label: 0,
96 - lang: 0,
97 - list: 0,
42 loop: HAS_BOOLEAN_VALUE,
99 - low: 0,
100 - manifest: 0,
101 - marginHeight: 0,
102 - marginWidth: 0,
103 - max: 0,
104 - maxLength: 0,
105 - media: 0,
106 - mediaGroup: 0,
107 - method: 0,
108 - min: 0,
109 - minLength: 0,
43 // Caution; `option.selected` is not updated if `select.multiple` is
44 // disabled with `removeAttribute`.
45 multiple: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
46 muted: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
114 - name: 0,
115 - nonce: 0,
47 noValidate: HAS_BOOLEAN_VALUE,
48 open: HAS_BOOLEAN_VALUE,
118 - optimum: 0,
119 - pattern: 0,
120 - placeholder: 0,
49 playsInline: HAS_BOOLEAN_VALUE,
122 - poster: 0,
123 - preload: 0,
124 - profile: 0,
125 - radioGroup: 0,
50 readOnly: HAS_BOOLEAN_VALUE,
127 - referrerPolicy: 0,
128 - rel: 0,
51 required: HAS_BOOLEAN_VALUE,
52 reversed: HAS_BOOLEAN_VALUE,
131 - role: 0,
53 rows: HAS_POSITIVE_NUMERIC_VALUE,
54 rowSpan: HAS_NUMERIC_VALUE,
134 - sandbox: 0,
135 - scope: 0,
55 scoped: HAS_BOOLEAN_VALUE,
137 - scrolling: 0,
56 seamless: HAS_BOOLEAN_VALUE,
57 selected: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
140 - shape: 0,
58 size: HAS_POSITIVE_NUMERIC_VALUE,
142 - sizes: 0,
59 + start: HAS_NUMERIC_VALUE,
60 // support for projecting regular DOM Elements via V1 named slots ( shadow dom )
144 - slot: 0,
61 span: HAS_POSITIVE_NUMERIC_VALUE,
146 - spellCheck: 0,
147 - src: 0,
148 - srcDoc: 0,
149 - srcLang: 0,
150 - srcSet: 0,
151 - start: HAS_NUMERIC_VALUE,
152 - step: 0,
62 + // Style must be explicitly set in the attribute list. React components
63 + // expect a style object
64 style: 0,
154 - summary: 0,
155 - tabIndex: 0,
156 - target: 0,
157 - title: 0,
158 - // Setting .type throws on non-<input> tags
159 - type: 0,
160 - useMap: 0,
65 + // itemScope is for for Microdata support.
66 + // See http://schema.org/docs/gs.html
67 + itemScope: HAS_BOOLEAN_VALUE,
68 + // These attributes must stay in the white-list because they have
69 + // different attribute names (see DOMAttributeNames below)
70 + acceptCharset: 0,
71 + className: 0,
72 + htmlFor: 0,
73 + httpEquiv: 0,
74 + // Attributes with mutation methods must be specified in the whitelist
75 value: 0,
162 - width: 0,
163 - wmode: 0,
164 - wrap: 0,
165 -
166 - /**
167 - * RDFa Properties
168 - */
169 - about: 0,
170 - datatype: 0,
171 - inlist: 0,
172 - prefix: 0,
173 - // property is also supported for OpenGraph in meta tags.
174 - property: 0,
175 - resource: 0,
176 - typeof: 0,
177 - vocab: 0,
178 -
179 - /**
180 - * Non-standard Properties
181 - */
76 + // The following attributes expect boolean values. They must be in
77 + // the whitelist to allow boolean attribute assignment:
78 + autoComplete: 0,
79 + // IE only true/false iFrame attribute
80 + // https://msdn.microsoft.com/en-us/library/ms533072(v=vs.85).aspx
81 + allowTransparency: 0,
82 + contentEditable: 0,
83 + draggable: 0,
84 + spellCheck: 0,
85 // autoCapitalize and autoCorrect are supported in Mobile Safari for
86 // keyboard hints.
87 autoCapitalize: 0,
88 autoCorrect: 0,
89 // autoSave allows WebKit/Blink to persist values of input fields on page reloads
90 autoSave: 0,
188 - // color is for Safari mask-icon link
189 - color: 0,
190 - // itemProp, itemScope, itemType are for
191 - // Microdata support. See http://schema.org/docs/gs.html
192 - itemProp: 0,
193 - itemScope: HAS_BOOLEAN_VALUE,
194 - itemType: 0,
195 - // itemID and itemRef are for Microdata support as well but
196 - // only specified in the WHATWG spec document. See
197 - // https://html.spec.whatwg.org/multipage/microdata.html#microdata-dom-api
198 - itemID: 0,
199 - itemRef: 0,
200 - // results show looking glass icon and recent searches on input
201 - // search fields in WebKit/Blink
202 - results: 0,
203 - // IE-only attribute that specifies security restrictions on an iframe
204 - // as an alternative to the sandbox attribute on IE<10
205 - security: 0,
206 - // IE-only attribute that controls focus behavior
207 - unselectable: 0,
91 },
92 DOMAttributeNames: {
93 acceptCharset: 'accept-charset',
@@ -212,7 +95,6 @@ var HTMLDOMPropertyConfig = {
95 htmlFor: 'for',
96 httpEquiv: 'http-equiv',
97 },
215 - DOMPropertyNames: {},
98 DOMMutationMethods: {
99 value: function(node, value) {
100 if (value == null) {
src/renderers/dom/shared/ReactDOMInjection.js
-2
@@ -11,11 +11,9 @@
11
12 'use strict';
13
14 -var ARIADOMPropertyConfig = require('ARIADOMPropertyConfig');
14 var DOMProperty = require('DOMProperty');
15 var HTMLDOMPropertyConfig = require('HTMLDOMPropertyConfig');
16 var SVGDOMPropertyConfig = require('SVGDOMPropertyConfig');
17
19 -DOMProperty.injection.injectDOMPropertyConfig(ARIADOMPropertyConfig);
18 DOMProperty.injection.injectDOMPropertyConfig(HTMLDOMPropertyConfig);
19 DOMProperty.injection.injectDOMPropertyConfig(SVGDOMPropertyConfig);
src/renderers/dom/shared/SVGDOMPropertyConfig.js
+107 -263
@@ -16,264 +16,105 @@ var NS = {
16 xml: 'http://www.w3.org/XML/1998/namespace',
17 };
18
19 -// We use attributes for everything SVG so let's avoid some duplication and run
20 -// code instead.
21 -// The following are all specified in the HTML config already so we exclude here.
22 -// - class (as className)
23 -// - color
24 -// - height
25 -// - id
26 -// - lang
27 -// - max
28 -// - media
29 -// - method
30 -// - min
31 -// - name
32 -// - style
33 -// - target
34 -// - type
35 -// - width
36 -var ATTRS = {
37 - accentHeight: 'accent-height',
38 - accumulate: 0,
39 - additive: 0,
40 - alignmentBaseline: 'alignment-baseline',
41 - allowReorder: 'allowReorder',
42 - alphabetic: 0,
43 - amplitude: 0,
44 - arabicForm: 'arabic-form',
45 - ascent: 0,
46 - attributeName: 'attributeName',
47 - attributeType: 'attributeType',
48 - autoReverse: 'autoReverse',
49 - azimuth: 0,
50 - baseFrequency: 'baseFrequency',
51 - baseProfile: 'baseProfile',
52 - baselineShift: 'baseline-shift',
53 - bbox: 0,
54 - begin: 0,
55 - bias: 0,
56 - by: 0,
57 - calcMode: 'calcMode',
58 - capHeight: 'cap-height',
59 - clip: 0,
60 - clipPath: 'clip-path',
61 - clipRule: 'clip-rule',
62 - clipPathUnits: 'clipPathUnits',
63 - colorInterpolation: 'color-interpolation',
64 - colorInterpolationFilters: 'color-interpolation-filters',
65 - colorProfile: 'color-profile',
66 - colorRendering: 'color-rendering',
67 - contentScriptType: 'contentScriptType',
68 - contentStyleType: 'contentStyleType',
69 - cursor: 0,
70 - cx: 0,
71 - cy: 0,
72 - d: 0,
73 - decelerate: 0,
74 - descent: 0,
75 - diffuseConstant: 'diffuseConstant',
76 - direction: 0,
77 - display: 0,
78 - divisor: 0,
79 - dominantBaseline: 'dominant-baseline',
80 - dur: 0,
81 - dx: 0,
82 - dy: 0,
83 - edgeMode: 'edgeMode',
84 - elevation: 0,
85 - enableBackground: 'enable-background',
86 - end: 0,
87 - exponent: 0,
88 - externalResourcesRequired: 'externalResourcesRequired',
89 - fill: 0,
90 - fillOpacity: 'fill-opacity',
91 - fillRule: 'fill-rule',
92 - filter: 0,
93 - filterRes: 'filterRes',
94 - filterUnits: 'filterUnits',
95 - floodColor: 'flood-color',
96 - floodOpacity: 'flood-opacity',
97 - focusable: 0,
98 - fontFamily: 'font-family',
99 - fontSize: 'font-size',
100 - fontSizeAdjust: 'font-size-adjust',
101 - fontStretch: 'font-stretch',
102 - fontStyle: 'font-style',
103 - fontVariant: 'font-variant',
104 - fontWeight: 'font-weight',
105 - format: 0,
106 - from: 0,
107 - fx: 0,
108 - fy: 0,
109 - g1: 0,
110 - g2: 0,
111 - glyphName: 'glyph-name',
112 - glyphOrientationHorizontal: 'glyph-orientation-horizontal',
113 - glyphOrientationVertical: 'glyph-orientation-vertical',
114 - glyphRef: 'glyphRef',
115 - gradientTransform: 'gradientTransform',
116 - gradientUnits: 'gradientUnits',
117 - hanging: 0,
118 - horizAdvX: 'horiz-adv-x',
119 - horizOriginX: 'horiz-origin-x',
120 - ideographic: 0,
121 - imageRendering: 'image-rendering',
122 - in: 0,
123 - in2: 0,
124 - intercept: 0,
125 - k: 0,
126 - k1: 0,
127 - k2: 0,
128 - k3: 0,
129 - k4: 0,
130 - kernelMatrix: 'kernelMatrix',
131 - kernelUnitLength: 'kernelUnitLength',
132 - kerning: 0,
133 - keyPoints: 'keyPoints',
134 - keySplines: 'keySplines',
135 - keyTimes: 'keyTimes',
136 - lengthAdjust: 'lengthAdjust',
137 - letterSpacing: 'letter-spacing',
138 - lightingColor: 'lighting-color',
139 - limitingConeAngle: 'limitingConeAngle',
140 - local: 0,
141 - markerEnd: 'marker-end',
142 - markerMid: 'marker-mid',
143 - markerStart: 'marker-start',
144 - markerHeight: 'markerHeight',
145 - markerUnits: 'markerUnits',
146 - markerWidth: 'markerWidth',
147 - mask: 0,
148 - maskContentUnits: 'maskContentUnits',
149 - maskUnits: 'maskUnits',
150 - mathematical: 0,
151 - mode: 0,
152 - numOctaves: 'numOctaves',
153 - offset: 0,
154 - opacity: 0,
155 - operator: 0,
156 - order: 0,
157 - orient: 0,
158 - orientation: 0,
159 - origin: 0,
160 - overflow: 0,
161 - overlinePosition: 'overline-position',
162 - overlineThickness: 'overline-thickness',
163 - paintOrder: 'paint-order',
164 - panose1: 'panose-1',
165 - pathLength: 'pathLength',
166 - patternContentUnits: 'patternContentUnits',
167 - patternTransform: 'patternTransform',
168 - patternUnits: 'patternUnits',
169 - pointerEvents: 'pointer-events',
170 - points: 0,
171 - pointsAtX: 'pointsAtX',
172 - pointsAtY: 'pointsAtY',
173 - pointsAtZ: 'pointsAtZ',
174 - preserveAlpha: 'preserveAlpha',
175 - preserveAspectRatio: 'preserveAspectRatio',
176 - primitiveUnits: 'primitiveUnits',
177 - r: 0,
178 - radius: 0,
179 - refX: 'refX',
180 - refY: 'refY',
181 - renderingIntent: 'rendering-intent',
182 - repeatCount: 'repeatCount',
183 - repeatDur: 'repeatDur',
184 - requiredExtensions: 'requiredExtensions',
185 - requiredFeatures: 'requiredFeatures',
186 - restart: 0,
187 - result: 0,
188 - rotate: 0,
189 - rx: 0,
190 - ry: 0,
191 - scale: 0,
192 - seed: 0,
193 - shapeRendering: 'shape-rendering',
194 - slope: 0,
195 - spacing: 0,
196 - specularConstant: 'specularConstant',
197 - specularExponent: 'specularExponent',
198 - speed: 0,
199 - spreadMethod: 'spreadMethod',
200 - startOffset: 'startOffset',
201 - stdDeviation: 'stdDeviation',
202 - stemh: 0,
203 - stemv: 0,
204 - stitchTiles: 'stitchTiles',
205 - stopColor: 'stop-color',
206 - stopOpacity: 'stop-opacity',
207 - strikethroughPosition: 'strikethrough-position',
208 - strikethroughThickness: 'strikethrough-thickness',
209 - string: 0,
210 - stroke: 0,
211 - strokeDasharray: 'stroke-dasharray',
212 - strokeDashoffset: 'stroke-dashoffset',
213 - strokeLinecap: 'stroke-linecap',
214 - strokeLinejoin: 'stroke-linejoin',
215 - strokeMiterlimit: 'stroke-miterlimit',
216 - strokeOpacity: 'stroke-opacity',
217 - strokeWidth: 'stroke-width',
218 - surfaceScale: 'surfaceScale',
219 - systemLanguage: 'systemLanguage',
220 - tableValues: 'tableValues',
221 - targetX: 'targetX',
222 - targetY: 'targetY',
223 - textAnchor: 'text-anchor',
224 - textDecoration: 'text-decoration',
225 - textRendering: 'text-rendering',
226 - textLength: 'textLength',
227 - to: 0,
228 - transform: 0,
229 - u1: 0,
230 - u2: 0,
231 - underlinePosition: 'underline-position',
232 - underlineThickness: 'underline-thickness',
233 - unicode: 0,
234 - unicodeBidi: 'unicode-bidi',
235 - unicodeRange: 'unicode-range',
236 - unitsPerEm: 'units-per-em',
237 - vAlphabetic: 'v-alphabetic',
238 - vHanging: 'v-hanging',
239 - vIdeographic: 'v-ideographic',
240 - vMathematical: 'v-mathematical',
241 - values: 0,
242 - vectorEffect: 'vector-effect',
243 - version: 0,
244 - vertAdvY: 'vert-adv-y',
245 - vertOriginX: 'vert-origin-x',
246 - vertOriginY: 'vert-origin-y',
247 - viewBox: 'viewBox',
248 - viewTarget: 'viewTarget',
249 - visibility: 0,
250 - widths: 0,
251 - wordSpacing: 'word-spacing',
252 - writingMode: 'writing-mode',
253 - x: 0,
254 - xHeight: 'x-height',
255 - x1: 0,
256 - x2: 0,
257 - xChannelSelector: 'xChannelSelector',
258 - xlinkActuate: 'xlink:actuate',
259 - xlinkArcrole: 'xlink:arcrole',
260 - xlinkHref: 'xlink:href',
261 - xlinkRole: 'xlink:role',
262 - xlinkShow: 'xlink:show',
263 - xlinkTitle: 'xlink:title',
264 - xlinkType: 'xlink:type',
265 - xmlBase: 'xml:base',
266 - xmlns: 0,
267 - xmlnsXlink: 'xmlns:xlink',
268 - xmlLang: 'xml:lang',
269 - xmlSpace: 'xml:space',
270 - y: 0,
271 - y1: 0,
272 - y2: 0,
273 - yChannelSelector: 'yChannelSelector',
274 - z: 0,
275 - zoomAndPan: 'zoomAndPan',
276 -};
19 +/**
20 + * This is a list of all SVG attributes that need special
21 + * casing, namespacing, or boolean value assignment.
22 + *
23 + * SVG Attributes List:
24 + * https://www.w3.org/TR/SVG/attindex.html
25 + * SMIL Spec:
26 + * https://www.w3.org/TR/smil
27 + */
28 +var ATTRS = [
29 + 'accent-height',
30 + 'alignment-baseline',
31 + 'arabic-form',
32 + 'baseline-shift',
33 + 'cap-height',
34 + 'clip-path',
35 + 'clip-rule',
36 + 'color-interpolation',
37 + 'color-interpolation-filters',
38 + 'color-profile',
39 + 'color-rendering',
40 + 'dominant-baseline',
41 + 'enable-background',
42 + 'fill-opacity',
43 + 'fill-rule',
44 + 'flood-color',
45 + 'flood-opacity',
46 + 'font-family',
47 + 'font-size',
48 + 'font-size-adjust',
49 + 'font-stretch',
50 + 'font-style',
51 + 'font-variant',
52 + 'font-weight',
53 + 'glyph-name',
54 + 'glyph-orientation-horizontal',
55 + 'glyph-orientation-vertical',
56 + 'horiz-adv-x',
57 + 'horiz-origin-x',
58 + 'image-rendering',
59 + 'letter-spacing',
60 + 'lighting-color',
61 + 'marker-end',
62 + 'marker-mid',
63 + 'marker-start',
64 + 'overline-position',
65 + 'overline-thickness',
66 + 'paint-order',
67 + 'panose-1',
68 + 'pointer-events',
69 + 'rendering-intent',
70 + 'shape-rendering',
71 + 'stop-color',
72 + 'stop-opacity',
73 + 'strikethrough-position',
74 + 'strikethrough-thickness',
75 + 'stroke-dasharray',
76 + 'stroke-dashoffset',
77 + 'stroke-linecap',
78 + 'stroke-linejoin',
79 + 'stroke-miterlimit',
80 + 'stroke-opacity',
81 + 'stroke-width',
82 + 'text-anchor',
83 + 'text-decoration',
84 + 'text-rendering',
85 + 'underline-position',
86 + 'underline-thickness',
87 + 'unicode-bidi',
88 + 'unicode-range',
89 + 'units-per-em',
90 + 'v-alphabetic',
91 + 'v-hanging',
92 + 'v-ideographic',
93 + 'v-mathematical',
94 + 'vector-effect',
95 + 'vert-adv-y',
96 + 'vert-origin-x',
97 + 'vert-origin-y',
98 + 'word-spacing',
99 + 'writing-mode',
100 + 'x-height',
101 + 'xlink:actuate',
102 + 'xlink:arcrole',
103 + 'xlink:href',
104 + 'xlink:role',
105 + 'xlink:show',
106 + 'xlink:title',
107 + 'xlink:type',
108 + 'xml:base',
109 + 'xmlns:xlink',
110 + 'xml:lang',
111 + 'xml:space',
112 + // The following attributes expect boolean values. They must be in
113 + // the whitelist to allow boolean attribute assignment:
114 + 'autoReverse',
115 + 'externalResourcesRequired',
116 + 'preserveAlpha',
117 +];
118
119 var SVGDOMPropertyConfig = {
120 Properties: {},
@@ -292,11 +133,14 @@ var SVGDOMPropertyConfig = {
133 DOMAttributeNames: {},
134 };
135
295 -Object.keys(ATTRS).forEach(key => {
296 - SVGDOMPropertyConfig.Properties[key] = 0;
297 - if (ATTRS[key]) {
298 - SVGDOMPropertyConfig.DOMAttributeNames[key] = ATTRS[key];
299 - }
136 +var CAMELIZE = /[\-\:]([a-z])/g;
137 +var capitalize = token => token[1].toUpperCase();
138 +
139 +ATTRS.forEach(original => {
140 + var reactName = original.replace(CAMELIZE, capitalize);
141 +
142 + SVGDOMPropertyConfig.Properties[reactName] = 0;
143 + SVGDOMPropertyConfig.DOMAttributeNames[reactName] = original;
144 });
145
146 module.exports = SVGDOMPropertyConfig;
src/renderers/dom/shared/__tests__/DOMPropertyOperations-test.js
+4 -3
@@ -67,12 +67,13 @@ describe('DOMPropertyOperations', () => {
67 // This ensures that we have consistent behavior.
68 var obj = {
69 toString: function() {
70 - return '<html>';
70 + return 'css-class';
71 },
72 };
73 +
74 var container = document.createElement('div');
74 - ReactDOM.render(<div role={obj} />, container);
75 - expect(container.firstChild.getAttribute('role')).toBe('<html>');
75 + ReactDOM.render(<div className={obj} />, container);
76 + expect(container.firstChild.getAttribute('class')).toBe('css-class');
77 });
78
79 it('should not remove empty attributes for special properties', () => {
src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
+287 -16
@@ -145,22 +145,26 @@ describe('ReactDOMComponent', () => {
145 it('should warn for unknown prop', () => {
146 spyOn(console, 'error');
147 var container = document.createElement('div');
148 - ReactDOM.render(<div foo="bar" />, container);
148 + ReactDOM.render(<div foo={() => {}} />, container);
149 expectDev(console.error.calls.count(0)).toBe(1);
150 expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
151 - 'Warning: Unknown prop `foo` on <div> tag. Remove this prop from the element. ' +
152 - 'For details, see https://fb.me/react-unknown-prop\n in div (at **)',
151 + 'Warning: Invalid prop `foo` on <div> tag. Either remove this prop ' +
152 + 'from the element, or pass a string or number value to keep ' +
153 + 'it in the DOM. For details, see https://fb.me/react-unknown-prop' +
154 + '\n in div (at **)',
155 );
156 });
157
158 it('should group multiple unknown prop warnings together', () => {
159 spyOn(console, 'error');
160 var container = document.createElement('div');
159 - ReactDOM.render(<div foo="bar" baz="qux" />, container);
161 + ReactDOM.render(<div foo={() => {}} baz={() => {}} />, container);
162 expectDev(console.error.calls.count(0)).toBe(1);
163 expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
162 - 'Warning: Unknown props `foo`, `baz` on <div> tag. Remove these props from the element. ' +
163 - 'For details, see https://fb.me/react-unknown-prop\n in div (at **)',
164 + 'Warning: Invalid props `foo`, `baz` on <div> tag. Either remove these ' +
165 + 'props from the element, or pass a string or number value to keep ' +
166 + 'them in the DOM. For details, see https://fb.me/react-unknown-prop' +
167 + '\n in div (at **)',
168 );
169 });
170
@@ -170,7 +174,7 @@ describe('ReactDOMComponent', () => {
174 ReactDOM.render(<div onDblClick={() => {}} />, container);
175 expectDev(console.error.calls.count(0)).toBe(1);
176 expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
173 - 'Warning: Unknown event handler property onDblClick. Did you mean `onDoubleClick`?\n in div (at **)',
177 + 'Warning: Unknown event handler property `onDblClick`. Did you mean `onDoubleClick`?\n in div (at **)',
178 );
179 });
180
@@ -1002,6 +1006,15 @@ describe('ReactDOMComponent', () => {
1006 );
1007 });
1008
1009 + it('should validate against use of innerHTML without case sensitivity', () => {
1010 + spyOn(console, 'error');
1011 + mountComponent({innerhtml: '<span>Hi Jim!</span>'});
1012 + expectDev(console.error.calls.count()).toBe(1);
1013 + expectDev(console.error.calls.argsFor(0)[0]).toContain(
1014 + 'Directly setting property `innerHTML` is not permitted. ',
1015 + );
1016 + });
1017 +
1018 it('should validate use of dangerouslySetInnerHTML', () => {
1019 expect(function() {
1020 mountComponent({dangerouslySetInnerHTML: '<span>Hi Jim!</span>'});
@@ -1593,6 +1606,18 @@ describe('ReactDOMComponent', () => {
1606 expectDev(console.error.calls.count()).toBe(2);
1607 });
1608
1609 + it('should warn about props that are no longer supported without case sensitivity', () => {
1610 + spyOn(console, 'error');
1611 + ReactTestUtils.renderIntoDocument(<div />);
1612 + expectDev(console.error.calls.count()).toBe(0);
1613 +
1614 + ReactTestUtils.renderIntoDocument(<div onfocusin={() => {}} />);
1615 + expectDev(console.error.calls.count()).toBe(1);
1616 +
1617 + ReactTestUtils.renderIntoDocument(<div onfocusout={() => {}} />);
1618 + expectDev(console.error.calls.count()).toBe(2);
1619 + });
1620 +
1621 it('should warn about props that are no longer supported (ssr)', () => {
1622 spyOn(console, 'error');
1623 ReactDOMServer.renderToString(<div />);
@@ -1605,16 +1630,28 @@ describe('ReactDOMComponent', () => {
1630 expectDev(console.error.calls.count()).toBe(2);
1631 });
1632
1633 + it('should warn about props that are no longer supported without case sensitivity (ssr)', () => {
1634 + spyOn(console, 'error');
1635 + ReactDOMServer.renderToString(<div />);
1636 + expectDev(console.error.calls.count()).toBe(0);
1637 +
1638 + ReactDOMServer.renderToString(<div onfocusin={() => {}} />);
1639 + expectDev(console.error.calls.count()).toBe(1);
1640 +
1641 + ReactDOMServer.renderToString(<div onfocusout={() => {}} />);
1642 + expectDev(console.error.calls.count()).toBe(2);
1643 + });
1644 +
1645 it('gives source code refs for unknown prop warning', () => {
1646 spyOn(console, 'error');
1647 ReactTestUtils.renderIntoDocument(<div class="paladin" />);
1648 ReactTestUtils.renderIntoDocument(<input type="text" onclick="1" />);
1649 expectDev(console.error.calls.count()).toBe(2);
1650 expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
1614 - 'Warning: Unknown DOM property class. Did you mean className?\n in div (at **)',
1651 + 'Warning: Invalid DOM property `class`. Did you mean `className`?\n in div (at **)',
1652 );
1653 expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(1)[0])).toBe(
1617 - 'Warning: Unknown event handler property onclick. Did you mean ' +
1654 + 'Warning: Unknown event handler property `onclick`. Did you mean ' +
1655 '`onClick`?\n in input (at **)',
1656 );
1657 });
@@ -1625,10 +1662,10 @@ describe('ReactDOMComponent', () => {
1662 ReactDOMServer.renderToString(<input type="text" onclick="1" />);
1663 expectDev(console.error.calls.count()).toBe(2);
1664 expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
1628 - 'Warning: Unknown DOM property class. Did you mean className?\n in div (at **)',
1665 + 'Warning: Invalid DOM property `class`. Did you mean `className`?\n in div (at **)',
1666 );
1667 expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(1)[0])).toBe(
1631 - 'Warning: Unknown event handler property onclick. Did you mean ' +
1668 + 'Warning: Unknown event handler property `onclick`. Did you mean ' +
1669 '`onClick`?\n in input (at **)',
1670 );
1671 });
@@ -1643,7 +1680,7 @@ describe('ReactDOMComponent', () => {
1680 ReactTestUtils.renderIntoDocument(<div class="paladin" />, container);
1681 expectDev(console.error.calls.count()).toBe(1);
1682 expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
1646 - 'Warning: Unknown DOM property class. Did you mean className?\n in div (at **)',
1683 + 'Warning: Invalid DOM property `class`. Did you mean `className`?\n in div (at **)',
1684 );
1685 });
1686
@@ -1821,11 +1858,11 @@ describe('ReactDOMComponent', () => {
1858 expectDev(console.error.calls.count()).toBe(2);
1859
1860 expectDev(console.error.calls.argsFor(0)[0]).toBe(
1824 - 'Warning: Unknown DOM property for. Did you mean htmlFor?\n in label',
1861 + 'Warning: Invalid DOM property `for`. Did you mean `htmlFor`?\n in label',
1862 );
1863
1864 expectDev(console.error.calls.argsFor(1)[0]).toBe(
1828 - 'Warning: Unknown DOM property autofocus. Did you mean autoFocus?\n in input',
1865 + 'Warning: Invalid DOM property `autofocus`. Did you mean `autoFocus`?\n in input',
1866 );
1867 });
1868
@@ -1842,11 +1879,11 @@ describe('ReactDOMComponent', () => {
1879 expectDev(console.error.calls.count()).toBe(2);
1880
1881 expectDev(console.error.calls.argsFor(0)[0]).toBe(
1845 - 'Warning: Unknown DOM property for. Did you mean htmlFor?\n in label',
1882 + 'Warning: Invalid DOM property `for`. Did you mean `htmlFor`?\n in label',
1883 );
1884
1885 expectDev(console.error.calls.argsFor(1)[0]).toBe(
1849 - 'Warning: Unknown DOM property autofocus. Did you mean autoFocus?\n in input',
1886 + 'Warning: Invalid DOM property `autofocus`. Did you mean `autoFocus`?\n in input',
1887 );
1888 });
1889 });
@@ -1874,4 +1911,238 @@ describe('ReactDOMComponent', () => {
1911 expect(container.firstChild.innerHTML).toBe(html2);
1912 });
1913 });
1914 +
1915 + describe('Attributes with aliases', function() {
1916 + it('sets aliased attributes on HTML attributes', function() {
1917 + spyOn(console, 'error');
1918 +
1919 + var el = ReactTestUtils.renderIntoDocument(<div class="test" />);
1920 +
1921 + expect(el.className).toBe('test');
1922 +
1923 + expectDev(console.error.calls.argsFor(0)[0]).toContain(
1924 + 'Warning: Invalid DOM property `class`. Did you mean `className`?',
1925 + );
1926 + });
1927 +
1928 + it('sets incorrectly cased aliased attributes on HTML attributes with a warning', function() {
1929 + spyOn(console, 'error');
1930 +
1931 + var el = ReactTestUtils.renderIntoDocument(<div cLASS="test" />);
1932 +
1933 + expect(el.className).toBe('test');
1934 +
1935 + expectDev(console.error.calls.argsFor(0)[0]).toContain(
1936 + 'Warning: Invalid DOM property `cLASS`. Did you mean `className`?',
1937 + );
1938 + });
1939 +
1940 + it('sets aliased attributes on SVG elements with a warning', function() {
1941 + spyOn(console, 'error');
1942 +
1943 + var el = ReactTestUtils.renderIntoDocument(
1944 + <svg><text arabic-form="initial" /></svg>,
1945 + );
1946 + var text = el.querySelector('text');
1947 +
1948 + expect(text.hasAttribute('arabic-form')).toBe(true);
1949 +
1950 + expectDev(console.error.calls.argsFor(0)[0]).toContain(
1951 + 'Warning: Invalid DOM property `arabic-form`. Did you mean `arabicForm`?',
1952 + );
1953 + });
1954 +
1955 + it('sets aliased attributes on custom elements', function() {
1956 + var el = ReactTestUtils.renderIntoDocument(
1957 + <div is="custom-element" class="test" />,
1958 + );
1959 +
1960 + expect(el.getAttribute('class')).toBe('test');
1961 + });
1962 +
1963 + it('aliased attributes on custom elements with bad casing', function() {
1964 + var el = ReactTestUtils.renderIntoDocument(
1965 + <div is="custom-element" claSS="test" />,
1966 + );
1967 +
1968 + expect(el.getAttribute('class')).toBe('test');
1969 + });
1970 +
1971 + it('updates aliased attributes on custom elements', function() {
1972 + var container = document.createElement('div');
1973 + ReactDOM.render(<div is="custom-element" class="foo" />, container);
1974 + ReactDOM.render(<div is="custom-element" class="bar" />, container);
1975 +
1976 + expect(container.firstChild.getAttribute('class')).toBe('bar');
1977 + });
1978 + });
1979 +
1980 + describe('Custom attributes', function() {
1981 + it('allows assignment of custom attributes with string values', function() {
1982 + var el = ReactTestUtils.renderIntoDocument(<div whatever="30" />);
1983 +
1984 + expect(el.getAttribute('whatever')).toBe('30');
1985 + });
1986 +
1987 + it('removes custom attributes', function() {
1988 + const container = document.createElement('div');
1989 + ReactDOM.render(<div whatever="30" />, container);
1990 +
1991 + expect(container.firstChild.getAttribute('whatever')).toBe('30');
1992 +
1993 + ReactDOM.render(<div whatever={null} />, container);
1994 +
1995 + expect(container.firstChild.hasAttribute('whatever')).toBe(false);
1996 + });
1997 +
1998 + it('does not assign a boolean custom attributes as a string', function() {
1999 + spyOn(console, 'error');
2000 +
2001 + var el = ReactTestUtils.renderIntoDocument(<div whatever={true} />);
2002 +
2003 + expect(el.hasAttribute('whatever')).toBe(false);
2004 +
2005 + expectDev(console.error.calls.argsFor(0)[0]).toContain(
2006 + 'Warning: Invalid prop `whatever` on <div> tag',
2007 + );
2008 + });
2009 +
2010 + it('does not assign an implicit boolean custom attributes', function() {
2011 + spyOn(console, 'error');
2012 +
2013 + // eslint-disable-next-line react/jsx-boolean-value
2014 + var el = ReactTestUtils.renderIntoDocument(<div whatever />);
2015 +
2016 + expect(el.hasAttribute('whatever')).toBe(false);
2017 +
2018 + expectDev(console.error.calls.argsFor(0)[0]).toContain(
2019 + 'Warning: Invalid prop `whatever` on <div> tag',
2020 + );
2021 + });
2022 +
2023 + it('assigns a numeric custom attributes as a string', function() {
2024 + var el = ReactTestUtils.renderIntoDocument(<div whatever={3} />);
2025 +
2026 + expect(el.getAttribute('whatever')).toBe('3');
2027 + });
2028 +
2029 + it('will not assign a function custom attributes', function() {
2030 + spyOn(console, 'error');
2031 +
2032 + var el = ReactTestUtils.renderIntoDocument(<div whatever={() => {}} />);
2033 +
2034 + expect(el.hasAttribute('whatever')).toBe(false);
2035 +
2036 + expectDev(console.error.calls.argsFor(0)[0]).toContain(
2037 + 'Warning: Invalid prop `whatever` on <div> tag',
2038 + );
2039 + });
2040 +
2041 + it('will assign an object custom attributes', function() {
2042 + var el = ReactTestUtils.renderIntoDocument(<div whatever={{}} />);
2043 + expect(el.getAttribute('whatever')).toBe('[object Object]');
2044 + });
2045 +
2046 + it('allows cased data attributes', function() {
2047 + var el = ReactTestUtils.renderIntoDocument(<div data-fooBar="true" />);
2048 + expect(el.getAttribute('data-foobar')).toBe('true');
2049 + });
2050 +
2051 + it('allows cased custom attributes', function() {
2052 + var el = ReactTestUtils.renderIntoDocument(<div fooBar="true" />);
2053 + expect(el.getAttribute('foobar')).toBe('true');
2054 + });
2055 +
2056 + it('warns on NaN attributes', function() {
2057 + spyOn(console, 'error');
2058 +
2059 + var el = ReactTestUtils.renderIntoDocument(<div whatever={NaN} />);
2060 +
2061 + expect(el.getAttribute('whatever')).toBe('NaN');
2062 +
2063 + expectDev(console.error.calls.argsFor(0)[0]).toContain(
2064 + 'Warning: Received NaN for numeric attribute `whatever`. If this is ' +
2065 + 'expected, cast the value to a string.\n in div',
2066 + );
2067 + });
2068 +
2069 + it('removes a property when it becomes invalid', function() {
2070 + spyOn(console, 'error');
2071 +
2072 + var container = document.createElement('div');
2073 + ReactDOM.render(<div whatever={0} />, container);
2074 + ReactDOM.render(<div whatever={() => {}} />, container);
2075 + var el = container.firstChild;
2076 +
2077 + expect(el.hasAttribute('whatever')).toBe(false);
2078 +
2079 + expectDev(console.error.calls.argsFor(0)[0]).toContain(
2080 + 'Warning: Invalid prop `whatever` on <div> tag.',
2081 + );
2082 + });
2083 + });
2084 +
2085 + describe('Object stringification', function() {
2086 + it('allows objects on known properties', function() {
2087 + var el = ReactTestUtils.renderIntoDocument(
2088 + <div allowTransparency={{}} />,
2089 + );
2090 + expect(el.getAttribute('allowtransparency')).toBe('[object Object]');
2091 + });
2092 +
2093 + it('should pass objects as attributes if they define toString', () => {
2094 + var obj = {
2095 + toString() {
2096 + return 'hello';
2097 + },
2098 + };
2099 + var container = document.createElement('div');
2100 +
2101 + ReactDOM.render(<img src={obj} />, container);
2102 + expect(container.firstChild.src).toBe('hello');
2103 +
2104 + ReactDOM.render(<svg arabicForm={obj} />, container);
2105 + expect(container.firstChild.getAttribute('arabic-form')).toBe('hello');
2106 +
2107 + ReactDOM.render(<div customAttribute={obj} />, container);
2108 + expect(container.firstChild.getAttribute('customAttribute')).toBe(
2109 + 'hello',
2110 + );
2111 + });
2112 +
2113 + it('passes objects on known SVG attributes if they do not define toString', () => {
2114 + var obj = {};
2115 + var container = document.createElement('div');
2116 +
2117 + ReactDOM.render(<svg arabicForm={obj} />, container);
2118 + expect(container.firstChild.getAttribute('arabic-form')).toBe(
2119 + '[object Object]',
2120 + );
2121 + });
2122 +
2123 + it('passes objects on custom attributes if they do not define toString', () => {
2124 + var obj = {};
2125 + var container = document.createElement('div');
2126 +
2127 + ReactDOM.render(<div customAttribute={obj} />, container);
2128 + expect(container.firstChild.getAttribute('customAttribute')).toBe(
2129 + '[object Object]',
2130 + );
2131 + });
2132 +
2133 + it('allows objects that inherit a custom toString method', function() {
2134 + var parent = {toString: () => 'hello.jpg'};
2135 + var child = Object.create(parent);
2136 + var el = ReactTestUtils.renderIntoDocument(<img src={child} />);
2137 +
2138 + expect(el.src).toBe('hello.jpg');
2139 + });
2140 +
2141 + it('assigns ajaxify (an important internal FB attribute)', function() {
2142 + var options = {toString: () => 'ajaxy'};
2143 + var el = ReactTestUtils.renderIntoDocument(<div ajaxify={options} />);
2144 +
2145 + expect(el.getAttribute('ajaxify')).toBe('ajaxy');
2146 + });
2147 + });
2148 });
src/renderers/dom/shared/__tests__/ReactDOMInvalidARIAHook-test.js
+22
@@ -63,5 +63,27 @@ describe('ReactDOMInvalidARIAHook', () => {
63 'Did you mean aria-haspopup?',
64 );
65 });
66 +
67 + it('should warn for use of recognized camel case aria attributes', () => {
68 + spyOn(console, 'error');
69 + // The valid attribute name is aria-haspopup.
70 + mountComponent({ariaHasPopup: 'true'});
71 + expectDev(console.error.calls.count()).toBe(1);
72 + expectDev(console.error.calls.argsFor(0)[0]).toContain(
73 + 'Warning: Invalid ARIA attribute ariaHasPopup. ' +
74 + 'Did you mean aria-haspopup?',
75 + );
76 + });
77 +
78 + it('should warn for use of unrecognized camel case aria attributes', () => {
79 + spyOn(console, 'error');
80 + // The valid attribute name is aria-haspopup.
81 + mountComponent({ariaSomethingInvalid: 'true'});
82 + expectDev(console.error.calls.count()).toBe(1);
83 + expectDev(console.error.calls.argsFor(0)[0]).toContain(
84 + 'Warning: Invalid ARIA attribute ariaSomethingInvalid. ARIA ' +
85 + 'attributes follow the pattern aria-* and must be lowercase.',
86 + );
87 + });
88 });
89 });
src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js
+127 -55
@@ -312,10 +312,6 @@ function expectMarkupMismatch(serverElement, clientElement) {
312 return testMarkupMatch(serverElement, clientElement, false);
313 }
314
315 -// TODO: this is a hack for testing dynamic injection. Remove this when we decide
316 -// how to do static injection instead.
317 -let onAfterResetModules = null;
318 -
315 // When there is a test that renders on server and then on client and expects a logged
316 // error, you want to see the error show up both on server and client. Unfortunately,
317 // React refuses to issue the same error twice to avoid clogging up the console.
@@ -323,9 +319,6 @@ let onAfterResetModules = null;
319 function resetModules() {
320 // First, reset the modules to load the client renderer.
321 jest.resetModuleRegistry();
326 - if (typeof onAfterResetModules === 'function') {
327 - onAfterResetModules();
328 - }
322
323 // TODO: can we express this test with only public API?
324 ExecutionEnvironment = require('ExecutionEnvironment');
@@ -341,9 +334,6 @@ function resetModules() {
334 // Resetting is important because we want to avoid any shared state
335 // influencing the tests.
336 jest.resetModuleRegistry();
344 - if (typeof onAfterResetModules === 'function') {
345 - onAfterResetModules();
346 - }
337 require('ReactFeatureFlags').disableNewFiberFeatures = false;
338 ReactDOMServer = require('react-dom/server');
339 }
@@ -477,16 +467,14 @@ describe('ReactDOMServerIntegration', () => {
467 expect(e.getAttribute('width')).toBe('30');
468 });
469
480 - // this seems like it might mask programmer error, but it's existing behavior.
481 - itRenders('string prop with true value', async render => {
482 - const e = await render(<a href={true} />);
483 - expect(e.getAttribute('href')).toBe('true');
470 + itRenders('no string prop with true value', async render => {
471 + const e = await render(<a href={true} />, 1);
472 + expect(e.hasAttribute('href')).toBe(false);
473 });
474
486 - // this seems like it might mask programmer error, but it's existing behavior.
487 - itRenders('string prop with false value', async render => {
488 - const e = await render(<a href={false} />);
489 - expect(e.getAttribute('href')).toBe('false');
475 + itRenders('no string prop with false value', async render => {
476 + const e = await render(<a href={false} />, 1);
477 + expect(e.hasAttribute('href')).toBe(false);
478 });
479
480 itRenders('no string prop with null value', async render => {
@@ -624,6 +612,35 @@ describe('ReactDOMServerIntegration', () => {
612 const e = await render(<div className={null} />);
613 expect(e.hasAttribute('className')).toBe(false);
614 });
615 +
616 + itRenders('no badly cased className with a warning', async render => {
617 + const e = await render(<div classname="test" />, 1);
618 + expect(e.hasAttribute('class')).toBe(false);
619 + expect(e.hasAttribute('classname')).toBe(true);
620 + });
621 +
622 + itRenders('className prop when given the alias', async render => {
623 + const e = await render(<div class="test" />, 1);
624 + expect(e.className).toBe('test');
625 + });
626 +
627 + itRenders(
628 + 'no className prop when given a badly cased alias',
629 + async render => {
630 + const e = await render(<div cLASs="test" />, 1);
631 + expect(e.className).toBe('test');
632 + },
633 + );
634 +
635 + itRenders('class for custom elements', async render => {
636 + const e = await render(<div is="custom-element" class="test" />, 0);
637 + expect(e.getAttribute('class')).toBe('test');
638 + });
639 +
640 + itRenders('className for custom elements', async render => {
641 + const e = await render(<div is="custom-element" className="test" />, 0);
642 + expect(e.getAttribute('className')).toBe('test');
643 + });
644 });
645
646 describe('htmlFor property', function() {
@@ -632,6 +649,12 @@ describe('ReactDOMServerIntegration', () => {
649 expect(e.getAttribute('for')).toBe('myFor');
650 });
651
652 + itRenders('no badly cased htmlfor', async render => {
653 + const e = await render(<div htmlfor="myFor" />, 1);
654 + expect(e.hasAttribute('for')).toBe(false);
655 + expect(e.getAttribute('htmlfor')).toBe('myFor');
656 + });
657 +
658 itRenders('htmlFor with an empty string', async render => {
659 const e = await render(<div htmlFor="" />);
660 expect(e.getAttribute('for')).toBe('');
@@ -653,6 +676,16 @@ describe('ReactDOMServerIntegration', () => {
676 const e = await render(<div htmlFor={null} />);
677 expect(e.hasAttribute('htmlFor')).toBe(false);
678 });
679 +
680 + itRenders('htmlFor attribute on custom elements', async render => {
681 + const e = await render(<div is="custom-element" htmlFor="test" />);
682 + expect(e.getAttribute('htmlFor')).toBe('test');
683 + });
684 +
685 + itRenders('for attribute on custom elements', async render => {
686 + const e = await render(<div is="custom-element" for="test" />);
687 + expect(e.getAttribute('for')).toBe('test');
688 + });
689 });
690
691 describe('numeric properties', function() {
@@ -778,10 +811,40 @@ describe('ReactDOMServerIntegration', () => {
811 });
812 });
813
814 + describe('cased attributes', function() {
815 + itRenders(
816 + 'badly cased aliased HTML attribute with a warning',
817 + async render => {
818 + const e = await render(<meta httpequiv="refresh" />, 1);
819 + expect(e.hasAttribute('http-equiv')).toBe(false);
820 + expect(e.getAttribute('httpequiv')).toBe('refresh');
821 + },
822 + );
823 +
824 + itRenders('badly cased SVG attribute with a warning', async render => {
825 + const e = await render(<text textlength="10" />, 1);
826 + expect(e.getAttribute('textLength')).toBe('10');
827 + });
828 +
829 + itRenders('no badly cased aliased SVG attribute alias', async render => {
830 + const e = await render(<text strokedasharray="10 10" />, 1);
831 + expect(e.hasAttribute('stroke-dasharray')).toBe(false);
832 + expect(e.getAttribute('strokedasharray')).toBe('10 10');
833 + });
834 +
835 + itRenders(
836 + 'no badly cased original SVG attribute that is aliased',
837 + async render => {
838 + const e = await render(<text stroke-dasharray="10 10" />, 1);
839 + expect(e.getAttribute('stroke-dasharray')).toBe('10 10');
840 + },
841 + );
842 + });
843 +
844 describe('unknown attributes', function() {
782 - itRenders('no unknown attributes', async render => {
783 - const e = await render(<div foo="bar" />, 1);
784 - expect(e.getAttribute('foo')).toBe(null);
845 + itRenders('unknown attributes', async render => {
846 + const e = await render(<div foo="bar" />);
847 + expect(e.getAttribute('foo')).toBe('bar');
848 });
849
850 itRenders('unknown data- attributes', async render => {
@@ -794,14 +857,34 @@ describe('ReactDOMServerIntegration', () => {
857 expect(e.hasAttribute('data-foo')).toBe(false);
858 });
859
860 + itRenders('unknown data- attributes with casing', async render => {
861 + const e = await render(<div data-fooBar="true" />);
862 + expect(e.getAttribute('data-fooBar')).toBe('true');
863 + });
864 +
865 + itRenders('unknown data- attributes with boolean true', async render => {
866 + const e = await render(<div data-fooBar={true} />);
867 + expect(e.getAttribute('data-fooBar')).toBe('true');
868 + });
869 +
870 + itRenders('unknown data- attributes with boolean false', async render => {
871 + const e = await render(<div data-fooBar={false} />);
872 + expect(e.getAttribute('data-fooBar')).toBe('false');
873 + });
874 +
875 itRenders(
798 - 'no unknown attributes for non-standard elements',
876 + 'no unknown data- attributes with casing and null value',
877 async render => {
800 - const e = await render(<nonstandard foo="bar" />, 1);
801 - expect(e.getAttribute('foo')).toBe(null);
878 + const e = await render(<div data-fooBar={null} />);
879 + expect(e.hasAttribute('data-fooBar')).toBe(false);
880 },
881 );
882
883 + itRenders('custom attributes for non-standard elements', async render => {
884 + const e = await render(<nonstandard foo="bar" />);
885 + expect(e.getAttribute('foo')).toBe('bar');
886 + });
887 +
888 itRenders('unknown attributes for custom elements', async render => {
889 const e = await render(<custom-element foo="bar" />);
890 expect(e.getAttribute('foo')).toBe('bar');
@@ -830,6 +913,11 @@ describe('ReactDOMServerIntegration', () => {
913 expect(e.hasAttribute('foo')).toBe(false);
914 },
915 );
916 +
917 + itRenders('cased custom attributes', async render => {
918 + const e = await render(<div fooBar="test" />);
919 + expect(e.getAttribute('fooBar')).toBe('test');
920 + });
921 });
922
923 itRenders('no HTML events', async render => {
@@ -1172,7 +1260,7 @@ describe('ReactDOMServerIntegration', () => {
1260 expect(e.namespaceURI).toBe('http://www.w3.org/2000/svg');
1261 });
1262
1175 - itRenders('svg element with an xlink', async render => {
1263 + itRenders('svg child element', async render => {
1264 let e = await render(
1265 <svg><image xlinkHref="http://i.imgur.com/w7GCRPb.png" /></svg>,
1266 );
@@ -1185,6 +1273,20 @@ describe('ReactDOMServerIntegration', () => {
1273 );
1274 });
1275
1276 + itRenders('no svg child element with a badly cased', async render => {
1277 + let e = await render(
1278 + <svg><image xlinkhref="http://i.imgur.com/w7GCRPb.png" /></svg>,
1279 + 1,
1280 + );
1281 + e = e.firstChild;
1282 + expect(e.hasAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe(
1283 + false,
1284 + );
1285 + expect(e.getAttribute('xlinkhref')).toBe(
1286 + 'http://i.imgur.com/w7GCRPb.png',
1287 + );
1288 + });
1289 +
1290 itRenders('a math element', async render => {
1291 const e = await render(<math />);
1292 expect(e.childNodes.length).toBe(0);
@@ -2573,34 +2675,4 @@ describe('ReactDOMServerIntegration', () => {
2675 <div dangerouslySetInnerHTML={{__html: "<span id='child2'/>"}} />,
2676 ));
2677 });
2576 -
2577 - describe('dynamic injection', () => {
2578 - beforeEach(() => {
2579 - // HACK: we reset modules several times during the test which breaks
2580 - // dynamic injection. So we resort to telling resetModules() to run
2581 - // our custom init code every time after resetting. We could have a nicer
2582 - // way to do this, but this is the only test that needs it, and it will
2583 - // be removed anyway when we switch to static injection.
2584 - onAfterResetModules = () => {
2585 - const DOMProperty = require('DOMProperty');
2586 - DOMProperty.injection.injectDOMPropertyConfig({
2587 - isCustomAttribute: function(name) {
2588 - return name.indexOf('foo-') === 0;
2589 - },
2590 - Properties: {foobar: null},
2591 - });
2592 - };
2593 - resetModules();
2594 - });
2595 -
2596 - afterEach(() => {
2597 - onAfterResetModules = null;
2598 - });
2599 -
2600 - itRenders('injected attributes', async render => {
2601 - const e = await render(<div foobar="simple" foo-xyz="simple" />, 0);
2602 - expect(e.getAttribute('foobar')).toBe('simple');
2603 - expect(e.getAttribute('foo-xyz')).toBe('simple');
2604 - });
2605 - });
2678 });
src/renderers/dom/shared/hooks/ReactDOMInvalidARIAHook.js
+39 -4
@@ -15,6 +15,9 @@ var DOMProperty = require('DOMProperty');
15
16 var warnedProperties = {};
17 var rARIA = new RegExp('^(aria)-[' + DOMProperty.ATTRIBUTE_NAME_CHAR + ']*$');
18 +var rARIACamel = new RegExp(
19 + '^(aria)[A-Z][' + DOMProperty.ATTRIBUTE_NAME_CHAR + ']*$',
20 +);
21
22 if (__DEV__) {
23 var warning = require('fbjs/lib/warning');
@@ -23,6 +26,8 @@ if (__DEV__) {
26 ReactDebugCurrentFrame,
27 } = require('ReactGlobalSharedState');
28 var {getStackAddendumByID} = ReactComponentTreeHook;
29 +
30 + var validAriaProperties = require('./validAriaProperties');
31 }
32
33 function getStackAddendum(debugID) {
@@ -41,12 +46,42 @@ function validateProperty(tagName, name, debugID) {
46 return true;
47 }
48
49 + if (rARIACamel.test(name)) {
50 + var ariaName = 'aria-' + name.slice(4).toLowerCase();
51 + var correctName = validAriaProperties.hasOwnProperty(ariaName)
52 + ? ariaName
53 + : null;
54 +
55 + // If this is an aria-* attribute, but is not listed in the known DOM
56 + // DOM properties, then it is an invalid aria-* attribute.
57 + if (correctName == null) {
58 + warning(
59 + false,
60 + 'Invalid ARIA attribute %s. ARIA attributes follow the pattern aria-* and must be lowercase.%s',
61 + name,
62 + getStackAddendum(debugID),
63 + );
64 + warnedProperties[name] = true;
65 + return true;
66 + }
67 + // aria-* attributes should be lowercase; suggest the lowercase version.
68 + if (name !== correctName) {
69 + warning(
70 + false,
71 + 'Invalid ARIA attribute %s. Did you mean %s?%s',
72 + name,
73 + correctName,
74 + getStackAddendum(debugID),
75 + );
76 + warnedProperties[name] = true;
77 + return true;
78 + }
79 + }
80 +
81 if (rARIA.test(name)) {
82 var lowerCasedName = name.toLowerCase();
46 - var standardName = DOMProperty.getPossibleStandardName.hasOwnProperty(
47 - lowerCasedName,
48 - )
49 - ? DOMProperty.getPossibleStandardName[lowerCasedName]
83 + var standardName = validAriaProperties.hasOwnProperty(lowerCasedName)
84 + ? lowerCasedName
85 : null;
86
87 // If this is an aria-* attribute, but is not listed in the known DOM
src/renderers/dom/shared/hooks/ReactDOMUnknownPropertyHook.js
+77 -48
@@ -35,39 +35,20 @@ function getStackAddendum(debugID) {
35 }
36
37 if (__DEV__) {
38 - var reactProps = {
39 - children: true,
40 - dangerouslySetInnerHTML: true,
41 - key: true,
42 - ref: true,
43 -
44 - autoFocus: true,
45 - defaultValue: true,
46 - defaultChecked: true,
47 - innerHTML: true,
48 - suppressContentEditableWarning: true,
49 - onFocusIn: true,
50 - onFocusOut: true,
51 - };
38 var warnedProperties = {};
39 var EVENT_NAME_REGEX = /^on[A-Z]/;
40 + var ARIA_NAME_REGEX = /^aria-/i;
41 + var possibleStandardNames = require('possibleStandardNames');
42
55 - var validateProperty = function(tagName, name, debugID) {
56 - if (
57 - DOMProperty.properties.hasOwnProperty(name) ||
58 - DOMProperty.isCustomAttribute(name)
59 - ) {
60 - return true;
61 - }
62 - if (
63 - (reactProps.hasOwnProperty(name) && reactProps[name]) ||
64 - (warnedProperties.hasOwnProperty(name) && warnedProperties[name])
65 - ) {
43 + var validateProperty = function(tagName, name, value, debugID) {
44 + if (warnedProperties.hasOwnProperty(name) && warnedProperties[name]) {
45 return true;
46 }
47 +
48 if (EventPluginRegistry.registrationNameModules.hasOwnProperty(name)) {
49 return true;
50 }
51 +
52 if (
53 EventPluginRegistry.plugins.length === 0 &&
54 EVENT_NAME_REGEX.test(name)
@@ -76,54 +57,100 @@ if (__DEV__) {
57 // Don't check events in this case.
58 return true;
59 }
79 - warnedProperties[name] = true;
80 - var lowerCasedName = name.toLowerCase();
81 -
82 - // data-* attributes should be lowercase; suggest the lowercase version
83 - var standardName = DOMProperty.isCustomAttribute(lowerCasedName)
84 - ? lowerCasedName
85 - : DOMProperty.getPossibleStandardName.hasOwnProperty(lowerCasedName)
86 - ? DOMProperty.getPossibleStandardName[lowerCasedName]
87 - : null;
60
61 + var lowerCasedName = name.toLowerCase();
62 var registrationName = EventPluginRegistry.possibleRegistrationNames.hasOwnProperty(
63 lowerCasedName,
64 )
65 ? EventPluginRegistry.possibleRegistrationNames[lowerCasedName]
66 : null;
67
95 - if (standardName != null) {
68 + if (registrationName != null) {
69 warning(
70 false,
98 - 'Unknown DOM property %s. Did you mean %s?%s',
71 + 'Unknown event handler property `%s`. Did you mean `%s`?%s',
72 name,
100 - standardName,
73 + registrationName,
74 getStackAddendum(debugID),
75 );
76 + warnedProperties[name] = true;
77 + return true;
78 + }
79 +
80 + // Let the ARIA attribute hook validate ARIA attributes
81 + if (ARIA_NAME_REGEX.test(name)) {
82 + return true;
83 + }
84 +
85 + if (lowerCasedName === 'onfocusin' || lowerCasedName === 'onfocusout') {
86 + warning(
87 + false,
88 + 'React uses onFocus and onBlur instead of onFocusIn and onFocusOut. ' +
89 + 'All React events are normalized to bubble, so onFocusIn and onFocusOut ' +
90 + 'are not needed/supported by React.',
91 + );
92 + warnedProperties[name] = true;
93 return true;
104 - } else if (registrationName != null) {
94 + }
95 +
96 + if (lowerCasedName === 'innerhtml') {
97 warning(
98 false,
107 - 'Unknown event handler property %s. Did you mean `%s`?%s',
99 + 'Directly setting property `innerHTML` is not permitted. ' +
100 + 'For more information, lookup documentation on `dangerouslySetInnerHTML`.',
101 + );
102 + warnedProperties[name] = true;
103 + return true;
104 + }
105 +
106 + if (typeof value === 'number' && isNaN(value)) {
107 + warning(
108 + false,
109 + 'Received NaN for numeric attribute `%s`. If this is expected, cast ' +
110 + 'the value to a string.%s',
111 name,
109 - registrationName,
112 getStackAddendum(debugID),
113 );
114 + warnedProperties[name] = true;
115 + return true;
116 + }
117 +
118 + // Known attributes should match the casing specified in the property config.
119 + if (possibleStandardNames.hasOwnProperty(lowerCasedName)) {
120 + var standardName = possibleStandardNames[lowerCasedName];
121 + if (standardName !== name) {
122 + warning(
123 + false,
124 + 'Invalid DOM property `%s`. Did you mean `%s`?%s',
125 + name,
126 + standardName,
127 + getStackAddendum(debugID),
128 + );
129 + warnedProperties[name] = true;
130 + return true;
131 + }
132 + }
133 +
134 + // Now that we've validated casing, do not validate
135 + // data types for reserved props
136 + if (DOMProperty.isReservedProp(name)) {
137 return true;
113 - } else {
114 - // We were unable to guess which prop the user intended.
115 - // It is likely that the user was just blindly spreading/forwarding props
116 - // Components should be careful to only render valid props/attributes.
117 - // Warning will be invoked in warnUnknownProperties to allow grouping.
138 + }
139 +
140 + // Warn when a known attribute is a bad type
141 + if (!DOMProperty.shouldSetAttribute(name, value)) {
142 + warnedProperties[name] = true;
143 return false;
144 }
145 +
146 + return true;
147 };
148 }
149
150 var warnUnknownProperties = function(type, props, debugID) {
151 var unknownProps = [];
152 for (var key in props) {
126 - var isValid = validateProperty(type, key, debugID);
153 + var isValid = validateProperty(type, key, props[key], debugID);
154 if (!isValid) {
155 unknownProps.push(key);
156 var value = props[key];
@@ -146,7 +173,8 @@ var warnUnknownProperties = function(type, props, debugID) {
173 if (unknownProps.length === 1) {
174 warning(
175 false,
149 - 'Unknown prop %s on <%s> tag. Remove this prop from the element. ' +
176 + 'Invalid prop %s on <%s> tag. Either remove this prop from the element, ' +
177 + 'or pass a string or number value to keep it in the DOM. ' +
178 'For details, see https://fb.me/react-unknown-prop%s',
179 unknownPropString,
180 type,
@@ -155,7 +183,8 @@ var warnUnknownProperties = function(type, props, debugID) {
183 } else if (unknownProps.length > 1) {
184 warning(
185 false,
158 - 'Unknown props %s on <%s> tag. Remove these props from the element. ' +
186 + 'Invalid props %s on <%s> tag. Either remove these props from the element, ' +
187 + 'or pass a string or number value to keep them in the DOM. ' +
188 'For details, see https://fb.me/react-unknown-prop%s',
189 unknownPropString,
190 type,
src/renderers/dom/shared/hooks/possibleStandardNames.js new
+312
@@ -0,0 +1,312 @@
1 +/**
2 + * Copyright 2013-present, Facebook, Inc.
3 + * All rights reserved.
4 + *
5 + * This source code is licensed under the BSD-style license found in the
6 + * LICENSE file in the root directory of this source tree. An additional grant
7 + * of patent rights can be found in the PATENTS file in the same directory.
8 + *
9 + * @providesModule possibleStandardNames
10 + */
11 +
12 +var possibleStandardNames = {
13 + // HTML
14 + acceptcharset: 'acceptCharset',
15 + 'accept-charset': 'acceptCharset',
16 + accesskey: 'accessKey',
17 + allowfullscreen: 'allowFullScreen',
18 + allowtransparency: 'allowTransparency',
19 + autocapitalize: 'autoCapitalize',
20 + autocomplete: 'autoComplete',
21 + autocorrect: 'autoCorrect',
22 + autofocus: 'autoFocus',
23 + autoplay: 'autoPlay',
24 + autosave: 'autoSave',
25 + cellpadding: 'cellPadding',
26 + cellspacing: 'cellSpacing',
27 + charset: 'charSet',
28 + class: 'className',
29 + classid: 'classID',
30 + classname: 'className',
31 + children: 'children',
32 + colspan: 'colSpan',
33 + contenteditable: 'contentEditable',
34 + contextmenu: 'contextMenu',
35 + controlslist: 'controlsList',
36 + crossorigin: 'crossOrigin',
37 + dangerouslysetinnerhtml: 'dangerouslySetInnerHTML',
38 + datetime: 'dateTime',
39 + defaultvalue: 'defaultValue',
40 + defaultchecked: 'defaultChecked',
41 + enctype: 'encType',
42 + for: 'htmlFor',
43 + formMethod: 'formMethod',
44 + formaction: 'formAction',
45 + formenctype: 'formEncType',
46 + formnovalidate: 'formNoValidate',
47 + formtarget: 'formTarget',
48 + frameborder: 'frameBorder',
49 + hreflang: 'hrefLang',
50 + htmlfor: 'htmlFor',
51 + httpequiv: 'httpEquiv',
52 + 'http-equiv': 'httpEquiv',
53 + innerhtml: 'innerHTML',
54 + inputmode: 'inputMode',
55 + itemid: 'itemID',
56 + itemprop: 'itemProp',
57 + itemref: 'itemRef',
58 + itemscope: 'itemScope',
59 + itemtype: 'itemType',
60 + keyparams: 'keyParams',
61 + keytype: 'keyType',
62 + marginWidth: 'marginWidth',
63 + marginheight: 'marginHeight',
64 + maxlength: 'maxLength',
65 + mediagroup: 'mediaGroup',
66 + minLength: 'minlength',
67 + novalidate: 'noValidate',
68 + playsinline: 'playsInline',
69 + radiogroup: 'radioGroup',
70 + readonly: 'readOnly',
71 + referrerpolicy: 'referrerPolicy',
72 + rowspan: 'rowSpan',
73 + spellcheck: 'spellCheck',
74 + srcdoc: 'srcDoc',
75 + srclang: 'srcLang',
76 + srcset: 'srcSet',
77 + tabindex: 'tabIndex',
78 + usemap: 'useMap',
79 +
80 + // SVG
81 + accentheight: 'accentHeight',
82 + 'accent-height': 'accentHeight',
83 + alignmentbaseline: 'alignmentBaseline',
84 + 'alignment-baseline': 'alignmentBaseline',
85 + allowreorder: 'allowReorder',
86 + 'arabic-form': 'arabicForm',
87 + arabicform: 'arabicForm',
88 + attributename: 'attributeName',
89 + attributetype: 'attributeType',
90 + autoreverse: 'autoReverse',
91 + basefrequency: 'baseFrequency',
92 + baseprofile: 'baseProfile',
93 + 'baseline-shift': 'baselineShift',
94 + baselineshift: 'baselineShift',
95 + calcmode: 'calcMode',
96 + 'cap-height': 'capHeight',
97 + capheight: 'capHeight',
98 + 'clip-path': 'clipPath',
99 + clippath: 'clipPath',
100 + 'clip-rule': 'clipRule',
101 + cliprule: 'clipRule',
102 + clippathunits: 'clipPathUnits',
103 + 'color-interpolation': 'colorInterpolation',
104 + colorinterpolation: 'colorInterpolation',
105 + 'color-interpolation-filters': 'colorInterpolationFilters',
106 + colorinterpolationfilters: 'colorInterpolationFilters',
107 + 'color-profile': 'colorProfile',
108 + colorprofile: 'colorProfile',
109 + 'color-rendering': 'colorRendering',
110 + colorrendering: 'colorRendering',
111 + contentscripttype: 'contentScriptType',
112 + contentstyletype: 'contentStyleType',
113 + diffuseconstant: 'diffuseConstant',
114 + 'dominant-baseline': 'dominantBaseline',
115 + dominantbaseline: 'dominantBaseline',
116 + edgemode: 'edgeMode',
117 + 'enable-background': 'enableBackground',
118 + enablebackground: 'enableBackground',
119 + externalresourcesrequired: 'externalResourcesRequired',
120 + 'fill-opacity': 'fillOpacity',
121 + fillopacity: 'fillOpacity',
122 + 'fill-rule': 'fillRule',
123 + fillrule: 'fillRule',
124 + filterres: 'filterRes',
125 + filterunits: 'filterUnits',
126 + floodcolor: 'floodColor',
127 + 'flood-color': 'floodColor',
128 + 'flood-opacity': 'floodOpacity',
129 + floodOpacity: 'floodOpacity',
130 + 'font-family': 'fontFamily',
131 + fontfamily: 'fontFamily',
132 + 'font-size': 'fontSize',
133 + fontsize: 'fontSize',
134 + 'font-size-adjust': 'fontSizeAdjust',
135 + fontsizeadjust: 'fontSizeAdjust',
136 + 'font-stretch': 'fontStretch',
137 + fontstretch: 'fontStretch',
138 + 'font-style': 'fontStyle',
139 + fontstyle: 'fontStyle',
140 + 'font-variant': 'fontVariant',
141 + fontvariant: 'fontVariant',
142 + 'font-weight': 'fontWeight',
143 + fontweight: 'fontWeight',
144 + 'glyph-name': 'glyphName',
145 + glyphname: 'glyphName',
146 + 'glyph-orientation-horizontal': 'glyphOrientationHorizontal',
147 + glyphorientationhorizontal: 'glyphOrientationHorizontal',
148 + 'glyph-orientation-vertical': 'glyphOrientationVertical',
149 + glyphorientationvertical: 'glyphOrientationVertical',
150 + glyphref: 'glyphRef',
151 + gradienttransform: 'gradientTransform',
152 + gradientunits: 'gradientUnits',
153 + 'horiz-adv-x': 'horizAdvX',
154 + horizadvx: 'horizAdvX',
155 + 'horiz-origin-x': 'horizOriginX',
156 + horizoriginx: 'horizOriginX',
157 + 'image-rendering': 'imageRendering',
158 + imagerendering: 'imageRendering',
159 + kernelmatrix: 'kernelMatrix',
160 + kernelunitlength: 'kernelUnitLength',
161 + keypoints: 'keyPoints',
162 + keysplines: 'keySplines',
163 + keytimes: 'keyTimes',
164 + lengthadjust: 'lengthAdjust',
165 + 'letter-spacing': 'letterSpacing',
166 + letterspacing: 'letterSpacing',
167 + 'lighting-color': 'lightingColor',
168 + lightingcolor: 'lightingColor',
169 + limitingconeangle: 'limitingConeAngle',
170 + 'marker-end': 'markerEnd',
171 + markerend: 'markerEnd',
172 + 'marker-mid': 'markerMid',
173 + markermid: 'markerMid',
174 + 'marker-start': 'markerStart',
175 + markerstart: 'markerStart',
176 + markerheight: 'markerHeight',
177 + markerunits: 'markerUnits',
178 + markerwidth: 'markerWidth',
179 + maskcontentunits: 'maskContentUnits',
180 + maskunits: 'maskUnits',
181 + numoctaves: 'numOctaves',
182 + 'overline-position': 'overlinePosition',
183 + overlineposition: 'overlinePosition',
184 + 'overline-thickness': 'overlineThickness',
185 + overlinethickness: 'overlineThickness',
186 + 'paint-order': 'paintOrder',
187 + paintorder: 'paintOrder',
188 + 'panose-1': 'panose1',
189 + pathlength: 'pathLength',
190 + patterncontentunits: 'patternContentUnits',
191 + patterntransform: 'patternTransform',
192 + patternunits: 'patternUnits',
193 + 'pointer-events': 'pointerEvents',
194 + pointerevents: 'pointerEvents',
195 + pointsatx: 'pointsAtX',
196 + pointsaty: 'pointsAtY',
197 + pointsatz: 'pointsAtZ',
198 + preservealpha: 'preserveAlpha',
199 + preserveaspectratio: 'preserveAspectRatio',
200 + primitiveunits: 'primitiveUnits',
201 + refx: 'refX',
202 + refy: 'refY',
203 + 'rendering-intent': 'renderingIntent',
204 + renderingintent: 'renderingIntent',
205 + repeatcount: 'repeatCount',
206 + repeatdur: 'repeatDur',
207 + requiredextensions: 'requiredExtensions',
208 + requiredfeatures: 'requiredFeatures',
209 + 'shape-rendering': 'shapeRendering',
210 + shaperendering: 'shapeRendering',
211 + specularconstant: 'specularConstant',
212 + specularexponent: 'specularExponent',
213 + spreadmethod: 'spreadMethod',
214 + startoffset: 'startOffset',
215 + stddeviation: 'stdDeviation',
216 + stitchtiles: 'stitchTiles',
217 + 'stop-color': 'stopColor',
218 + stopcolor: 'stopColor',
219 + stopopacity: 'stopOpacity',
220 + 'stop-opacity': 'stopOpacity',
221 + 'strikethrough-position': 'strikethroughPosition',
222 + strikethroughposition: 'strikethroughPosition',
223 + strikethroughthickness: 'strikethroughThickness',
224 + 'strikethrough-thickness': 'strikethroughThickness',
225 + 'stroke-dasharray': 'strokeDasharray',
226 + strokedasharray: 'strokeDasharray',
227 + 'stroke-dashoffset': 'strokeDashoffset',
228 + strokedashoffset: 'strokeDashoffset',
229 + 'stroke-linecap': 'strokeLinecap',
230 + strokelinecap: 'strokeLinecap',
231 + 'stroke-linejoin': 'strokeLinejoin',
232 + strokelinejoin: 'strokeLinejoin',
233 + 'stroke-miterlimit': 'strokeMiterlimit',
234 + strokemiterlimit: 'strokeMiterlimit',
235 + 'stroke-opacity': 'strokeOpacity',
236 + strokeopacity: 'strokeOpacity',
237 + 'stroke-width': 'strokeWidth',
238 + suppresscontenteditablewarning: 'suppressContentEditableWarning',
239 + surfacescale: 'surfaceScale',
240 + systemlanguage: 'systemLanguage',
241 + tablevalues: 'tableValues',
242 + targetx: 'targetX',
243 + targety: 'targetY',
244 + 'text-anchor': 'textAnchor',
245 + textanchor: 'textAnchor',
246 + 'text-decoration': 'textDecoration',
247 + textdecoration: 'textDecoration',
248 + 'text-rendering': 'textRendering',
249 + textrendering: 'textRendering',
250 + textlength: 'textLength',
251 + 'underline-position': 'underlinePosition',
252 + underlineposition: 'underlinePosition',
253 + 'underline-thickness': 'underlineThickness',
254 + underlinethickness: 'underlineThickness',
255 + 'unicode-bidi': 'unicodeBidi',
256 + unicodebidi: 'unicodeBidi',
257 + 'unicode-range': 'unicodeRange',
258 + unicoderange: 'unicodeRange',
259 + 'units-per-em': 'unitsPerEm',
260 + unitsperem: 'unitsPerEm',
261 + 'v-alphabetic': 'vAlphabetic',
262 + valphabetic: 'vAlphabetic',
263 + 'v-hanging': 'vHanging',
264 + vhanging: 'vHanging',
265 + 'v-ideographic': 'vIdeographic',
266 + videographic: 'vIdeographic',
267 + 'v-mathematical': 'vMathematical',
268 + vmathematical: 'vMathematical',
269 + 'vector-effect': 'vectorEffect',
270 + vectoreffect: 'vectorEffect',
271 + 'vert-adv-y': 'vertAdvY',
272 + vertadvy: 'vertAdvY',
273 + 'vert-origin-x': 'vertOriginX',
274 + vertoriginx: 'vertOriginX',
275 + 'vert-origin-y': 'vertOriginY',
276 + vertoriginy: 'vertOriginY',
277 + viewbox: 'viewBox',
278 + viewtarget: 'viewTarget',
279 + 'word-spacing': 'wordSpacing',
280 + wordspacing: 'wordSpacing',
281 + 'writing-mode': 'writingMode',
282 + writingmode: 'writingMode',
283 + 'x-height': 'xHeight',
284 + xheight: 'xHeight',
285 + xchannelselector: 'xChannelSelector',
286 + 'xlink:actuate': 'xlinkActuate',
287 + xlinkactuate: 'xlinkActuate',
288 + 'xlink:arcrole': 'xlinkArcrole',
289 + xlinkarcrole: 'xlinkArcrole',
290 + 'xlink:href': 'xlinkHref',
291 + xlinkhref: 'xlinkHref',
292 + 'xlink:role': 'xlinkRole',
293 + xlinkrole: 'xlinkRole',
294 + 'xlink:show': 'xlinkShow',
295 + xlinkshow: 'xlinkShow',
296 + 'xlink:title': 'xlinkTitle',
297 + xlinktitle: 'xlinkTitle',
298 + 'xlink:type': 'xlinkType',
299 + xlinktype: 'xlinkType',
300 + 'xml:base': 'xmlBase',
301 + xmlbase: 'xmlBase',
302 + 'xmlns:xlink': 'xmlnsXlink',
303 + xmlnsxlink: 'xmlnsXlink',
304 + 'xml:lang': 'xmlLang',
305 + xmllang: 'xmlLang',
306 + 'xml:space': 'xmlSpace',
307 + xmlspace: 'xmlSpace',
308 + ychannelselector: 'yChannelSelector',
309 + zoomandpan: 'zoomAndPan',
310 +};
311 +
312 +module.exports = possibleStandardNames;
src/renderers/dom/shared/hooks/validAriaProperties.js new
+69
@@ -0,0 +1,69 @@
1 +/**
2 + * Copyright 2013-present, Facebook, Inc.
3 + * All rights reserved.
4 + *
5 + * This source code is licensed under the BSD-style license found in the
6 + * LICENSE file in the root directory of this source tree. An additional grant
7 + * of patent rights can be found in the PATENTS file in the same directory.
8 + *
9 + * @providesModule validAriaProperties
10 + */
11 +
12 +'use strict';
13 +
14 +var ariaProperties = {
15 + 'aria-current': 0, // state
16 + 'aria-details': 0,
17 + 'aria-disabled': 0, // state
18 + 'aria-hidden': 0, // state
19 + 'aria-invalid': 0, // state
20 + 'aria-keyshortcuts': 0,
21 + 'aria-label': 0,
22 + 'aria-roledescription': 0,
23 + // Widget Attributes
24 + 'aria-autocomplete': 0,
25 + 'aria-checked': 0,
26 + 'aria-expanded': 0,
27 + 'aria-haspopup': 0,
28 + 'aria-level': 0,
29 + 'aria-modal': 0,
30 + 'aria-multiline': 0,
31 + 'aria-multiselectable': 0,
32 + 'aria-orientation': 0,
33 + 'aria-placeholder': 0,
34 + 'aria-pressed': 0,
35 + 'aria-readonly': 0,
36 + 'aria-required': 0,
37 + 'aria-selected': 0,
38 + 'aria-sort': 0,
39 + 'aria-valuemax': 0,
40 + 'aria-valuemin': 0,
41 + 'aria-valuenow': 0,
42 + 'aria-valuetext': 0,
43 + // Live Region Attributes
44 + 'aria-atomic': 0,
45 + 'aria-busy': 0,
46 + 'aria-live': 0,
47 + 'aria-relevant': 0,
48 + // Drag-and-Drop Attributes
49 + 'aria-dropeffect': 0,
50 + 'aria-grabbed': 0,
51 + // Relationship Attributes
52 + 'aria-activedescendant': 0,
53 + 'aria-colcount': 0,
54 + 'aria-colindex': 0,
55 + 'aria-colspan': 0,
56 + 'aria-controls': 0,
57 + 'aria-describedby': 0,
58 + 'aria-errormessage': 0,
59 + 'aria-flowto': 0,
60 + 'aria-labelledby': 0,
61 + 'aria-owns': 0,
62 + 'aria-posinset': 0,
63 + 'aria-rowcount': 0,
64 + 'aria-rowindex': 0,
65 + 'aria-rowspan': 0,
66 + 'aria-setsize': 0,
67 +};
68 +
69 +module.exports = ariaProperties;
src/renderers/dom/shared/utils/assertValidProps.js
-11
@@ -63,11 +63,6 @@ function assertValidProps(
63 );
64 }
65 if (__DEV__) {
66 - warning(
67 - props.innerHTML == null,
68 - 'Directly setting property `innerHTML` is not permitted. ' +
69 - 'For more information, lookup documentation on `dangerouslySetInnerHTML`.',
70 - );
66 warning(
67 props.suppressContentEditableWarning ||
68 !props.contentEditable ||
@@ -77,12 +72,6 @@ function assertValidProps(
72 'those nodes are unexpectedly modified or duplicated. This is ' +
73 'probably not intentional.',
74 );
80 - warning(
81 - props.onFocusIn == null && props.onFocusOut == null,
82 - 'React uses onFocus and onBlur instead of onFocusIn and onFocusOut. ' +
83 - 'All React events are normalized to bubble, so onFocusIn and onFocusOut ' +
84 - 'are not needed/supported by React.',
85 - );
75 }
76 invariant(
77 props.style == null || typeof props.style === 'object',
src/renderers/dom/stack/client/ReactDOMComponent.js
+7 -29
@@ -60,11 +60,6 @@ var CONTENT_TYPES = {string: true, number: true};
60
61 var STYLE = 'style';
62 var HTML = '__html';
63 -var RESERVED_PROPS = {
64 - children: null,
65 - dangerouslySetInnerHTML: null,
66 - suppressContentEditableWarning: null,
67 -};
63
64 function getDeclarationErrorAddendum(internalInstance) {
65 if (internalInstance) {
@@ -111,11 +106,6 @@ function assertValidProps(component, props) {
106 );
107 }
108 if (__DEV__) {
114 - warning(
115 - props.innerHTML == null,
116 - 'Directly setting property `innerHTML` is not permitted. ' +
117 - 'For more information, lookup documentation on `dangerouslySetInnerHTML`.',
118 - );
109 warning(
110 props.suppressContentEditableWarning ||
111 !props.contentEditable ||
@@ -125,12 +115,6 @@ function assertValidProps(component, props) {
115 'those nodes are unexpectedly modified or duplicated. This is ' +
116 'probably not intentional.',
117 );
128 - warning(
129 - props.onFocusIn == null && props.onFocusOut == null,
130 - 'React uses onFocus and onBlur instead of onFocusIn and onFocusOut. ' +
131 - 'All React events are normalized to bubble, so onFocusIn and onFocusOut ' +
132 - 'are not needed/supported by React.',
133 - );
118 }
119 invariant(
120 props.style == null || typeof props.style === 'object',
@@ -713,7 +697,7 @@ ReactDOMComponent.Mixin = {
697 }
698 var markup = null;
699 if (this._tag != null && isCustomComponent(this._tag, props)) {
716 - if (!RESERVED_PROPS.hasOwnProperty(propKey)) {
700 + if (!DOMProperty.isReservedProp(propKey)) {
701 markup = DOMMarkupOperations.createMarkupForCustomAttribute(
702 propKey,
703 propValue,
@@ -968,15 +952,12 @@ ReactDOMComponent.Mixin = {
952 }
953 } else if (registrationNameModules.hasOwnProperty(propKey)) {
954 // Do nothing for event names.
971 - } else if (isCustomComponent(this._tag, lastProps)) {
972 - if (!RESERVED_PROPS.hasOwnProperty(propKey)) {
955 + } else if (!DOMProperty.isReservedProp(propKey)) {
956 + if (isCustomComponent(this._tag, lastProps)) {
957 DOMPropertyOperations.deleteValueForAttribute(getNode(this), propKey);
958 + } else {
959 + DOMPropertyOperations.deleteValueForProperty(getNode(this), propKey);
960 }
975 - } else if (
976 - DOMProperty.properties[propKey] ||
977 - DOMProperty.isCustomAttribute(propKey)
978 - ) {
979 - DOMPropertyOperations.deleteValueForProperty(getNode(this), propKey);
961 }
962 }
963 for (propKey in nextProps) {
@@ -1025,17 +1006,14 @@ ReactDOMComponent.Mixin = {
1006 ensureListeningTo(this, propKey, transaction);
1007 }
1008 } else if (isCustomComponentTag) {
1028 - if (!RESERVED_PROPS.hasOwnProperty(propKey)) {
1009 + if (!DOMProperty.isReservedProp(propKey)) {
1010 DOMPropertyOperations.setValueForAttribute(
1011 getNode(this),
1012 propKey,
1013 nextProp,
1014 );
1015 }
1035 - } else if (
1036 - DOMProperty.properties[propKey] ||
1037 - DOMProperty.isCustomAttribute(propKey)
1038 - ) {
1016 + } else if (!DOMProperty.isReservedProp(propKey)) {
1017 var node = getNode(this);
1018 // If we're updating to null or undefined, we should remove the property
1019 // from the DOM node instead of inadvertently setting to a string. This