Warn against custom non-lowercase attributes (#10699)
* Warn against custom non-lowercase attributes * Update attribute table * Grammar tweaks
Dan Abramov committed
Sep 13, 2017 at 18:13 UTC
c99bad9483309b4cc89bad004450cd668d34b044
5 files changed
+148
-81
fixtures/attribute-behavior/AttributeTableSnapshot.md
+61
-61
@@ -701,23 +701,23 @@
701
## `as` (on `<div>`)
702
| Test Case | Flags | Result |
703
| --- | --- | --- |
704
-| `as=(string)`| (changed)| `"a string"` |
704
+| `as=(string)`| (initial)| `<empty string>` |
705
| `as=(empty string)`| (initial)| `<empty string>` |
706
-| `as=(array with string)`| (changed)| `"string"` |
706
+| `as=(array with string)`| (initial)| `<empty string>` |
707
| `as=(empty array)`| (initial)| `<empty string>` |
708
-| `as=(object)`| (changed)| `"result of toString()"` |
709
-| `as=(numeric string)`| (changed)| `"42"` |
710
-| `as=(-1)`| (changed)| `"-1"` |
711
-| `as=(0)`| (changed)| `"0"` |
712
-| `as=(integer)`| (changed)| `"1"` |
713
-| `as=(NaN)`| (changed, warning)| `"NaN"` |
714
-| `as=(float)`| (changed)| `"99.99"` |
708
+| `as=(object)`| (initial)| `<empty string>` |
709
+| `as=(numeric string)`| (initial)| `<empty string>` |
710
+| `as=(-1)`| (initial)| `<empty string>` |
711
+| `as=(0)`| (initial)| `<empty string>` |
712
+| `as=(integer)`| (initial)| `<empty string>` |
713
+| `as=(NaN)`| (initial, warning)| `<empty string>` |
714
+| `as=(float)`| (initial)| `<empty string>` |
715
| `as=(true)`| (initial, warning)| `<empty string>` |
716
| `as=(false)`| (initial, warning)| `<empty string>` |
717
-| `as=(string 'true')`| (changed)| `"true"` |
718
-| `as=(string 'false')`| (changed)| `"false"` |
719
-| `as=(string 'on')`| (changed)| `"on"` |
720
-| `as=(string 'off')`| (changed)| `"off"` |
717
+| `as=(string 'true')`| (initial)| `<empty string>` |
718
+| `as=(string 'false')`| (initial)| `<empty string>` |
719
+| `as=(string 'on')`| (initial)| `<empty string>` |
720
+| `as=(string 'off')`| (initial)| `<empty string>` |
721
| `as=(symbol)`| (initial, warning)| `<empty string>` |
722
| `as=(function)`| (initial, warning)| `<empty string>` |
723
| `as=(null)`| (initial)| `<empty string>` |
@@ -5251,52 +5251,52 @@
5251
## `initialChecked` (on `<div>`)
5252
| Test Case | Flags | Result |
5253
| --- | --- | --- |
5254
-| `initialChecked=(string)`| (changed)| `"a string"` |
5255
-| `initialChecked=(empty string)`| (changed)| `<empty string>` |
5256
-| `initialChecked=(array with string)`| (changed)| `"string"` |
5257
-| `initialChecked=(empty array)`| (changed)| `<empty string>` |
5258
-| `initialChecked=(object)`| (changed)| `"result of toString()"` |
5259
-| `initialChecked=(numeric string)`| (changed)| `"42"` |
5260
-| `initialChecked=(-1)`| (changed)| `"-1"` |
5261
-| `initialChecked=(0)`| (changed)| `"0"` |
5262
-| `initialChecked=(integer)`| (changed)| `"1"` |
5254
+| `initialChecked=(string)`| (changed, warning)| `"a string"` |
5255
+| `initialChecked=(empty string)`| (changed, warning)| `<empty string>` |
5256
+| `initialChecked=(array with string)`| (changed, warning)| `"string"` |
5257
+| `initialChecked=(empty array)`| (changed, warning)| `<empty string>` |
5258
+| `initialChecked=(object)`| (changed, warning)| `"result of toString()"` |
5259
+| `initialChecked=(numeric string)`| (changed, warning)| `"42"` |
5260
+| `initialChecked=(-1)`| (changed, warning)| `"-1"` |
5261
+| `initialChecked=(0)`| (changed, warning)| `"0"` |
5262
+| `initialChecked=(integer)`| (changed, warning)| `"1"` |
5263
| `initialChecked=(NaN)`| (changed, warning)| `"NaN"` |
5264
-| `initialChecked=(float)`| (changed)| `"99.99"` |
5264
+| `initialChecked=(float)`| (changed, warning)| `"99.99"` |
5265
| `initialChecked=(true)`| (initial, warning)| `<null>` |
5266
| `initialChecked=(false)`| (initial, warning)| `<null>` |
5267
-| `initialChecked=(string 'true')`| (changed)| `"true"` |
5268
-| `initialChecked=(string 'false')`| (changed)| `"false"` |
5269
-| `initialChecked=(string 'on')`| (changed)| `"on"` |
5270
-| `initialChecked=(string 'off')`| (changed)| `"off"` |
5267
+| `initialChecked=(string 'true')`| (changed, warning)| `"true"` |
5268
+| `initialChecked=(string 'false')`| (changed, warning)| `"false"` |
5269
+| `initialChecked=(string 'on')`| (changed, warning)| `"on"` |
5270
+| `initialChecked=(string 'off')`| (changed, warning)| `"off"` |
5271
| `initialChecked=(symbol)`| (initial, warning)| `<null>` |
5272
| `initialChecked=(function)`| (initial, warning)| `<null>` |
5273
-| `initialChecked=(null)`| (initial)| `<null>` |
5274
-| `initialChecked=(undefined)`| (initial)| `<null>` |
5273
+| `initialChecked=(null)`| (initial, warning)| `<null>` |
5274
+| `initialChecked=(undefined)`| (initial, warning)| `<null>` |
5275
5276
## `initialValue` (on `<div>`)
5277
| Test Case | Flags | Result |
5278
| --- | --- | --- |
5279
-| `initialValue=(string)`| (changed)| `"a string"` |
5280
-| `initialValue=(empty string)`| (changed)| `<empty string>` |
5281
-| `initialValue=(array with string)`| (changed)| `"string"` |
5282
-| `initialValue=(empty array)`| (changed)| `<empty string>` |
5283
-| `initialValue=(object)`| (changed)| `"result of toString()"` |
5284
-| `initialValue=(numeric string)`| (changed)| `"42"` |
5285
-| `initialValue=(-1)`| (changed)| `"-1"` |
5286
-| `initialValue=(0)`| (changed)| `"0"` |
5287
-| `initialValue=(integer)`| (changed)| `"1"` |
5279
+| `initialValue=(string)`| (changed, warning)| `"a string"` |
5280
+| `initialValue=(empty string)`| (changed, warning)| `<empty string>` |
5281
+| `initialValue=(array with string)`| (changed, warning)| `"string"` |
5282
+| `initialValue=(empty array)`| (changed, warning)| `<empty string>` |
5283
+| `initialValue=(object)`| (changed, warning)| `"result of toString()"` |
5284
+| `initialValue=(numeric string)`| (changed, warning)| `"42"` |
5285
+| `initialValue=(-1)`| (changed, warning)| `"-1"` |
5286
+| `initialValue=(0)`| (changed, warning)| `"0"` |
5287
+| `initialValue=(integer)`| (changed, warning)| `"1"` |
5288
| `initialValue=(NaN)`| (changed, warning)| `"NaN"` |
5289
-| `initialValue=(float)`| (changed)| `"99.99"` |
5289
+| `initialValue=(float)`| (changed, warning)| `"99.99"` |
5290
| `initialValue=(true)`| (initial, warning)| `<null>` |
5291
| `initialValue=(false)`| (initial, warning)| `<null>` |
5292
-| `initialValue=(string 'true')`| (changed)| `"true"` |
5293
-| `initialValue=(string 'false')`| (changed)| `"false"` |
5294
-| `initialValue=(string 'on')`| (changed)| `"on"` |
5295
-| `initialValue=(string 'off')`| (changed)| `"off"` |
5292
+| `initialValue=(string 'true')`| (changed, warning)| `"true"` |
5293
+| `initialValue=(string 'false')`| (changed, warning)| `"false"` |
5294
+| `initialValue=(string 'on')`| (changed, warning)| `"on"` |
5295
+| `initialValue=(string 'off')`| (changed, warning)| `"off"` |
5296
| `initialValue=(symbol)`| (initial, warning)| `<null>` |
5297
| `initialValue=(function)`| (initial, warning)| `<null>` |
5298
-| `initialValue=(null)`| (initial)| `<null>` |
5299
-| `initialValue=(undefined)`| (initial)| `<null>` |
5298
+| `initialValue=(null)`| (initial, warning)| `<null>` |
5299
+| `initialValue=(undefined)`| (initial, warning)| `<null>` |
5300
5301
## `inlist` (on `<div>`)
5302
| Test Case | Flags | Result |
@@ -9276,27 +9276,27 @@
9276
## `selectedIndex` (on `<div>`)
9277
| Test Case | Flags | Result |
9278
| --- | --- | --- |
9279
-| `selectedIndex=(string)`| (initial)| `<number: -1>` |
9280
-| `selectedIndex=(empty string)`| (initial)| `<number: -1>` |
9281
-| `selectedIndex=(array with string)`| (initial)| `<number: -1>` |
9282
-| `selectedIndex=(empty array)`| (initial)| `<number: -1>` |
9283
-| `selectedIndex=(object)`| (initial)| `<number: -1>` |
9284
-| `selectedIndex=(numeric string)`| (initial)| `<number: -1>` |
9285
-| `selectedIndex=(-1)`| (initial)| `<number: -1>` |
9286
-| `selectedIndex=(0)`| (initial)| `<number: -1>` |
9287
-| `selectedIndex=(integer)`| (initial)| `<number: -1>` |
9279
+| `selectedIndex=(string)`| (initial, warning)| `<number: -1>` |
9280
+| `selectedIndex=(empty string)`| (initial, warning)| `<number: -1>` |
9281
+| `selectedIndex=(array with string)`| (initial, warning)| `<number: -1>` |
9282
+| `selectedIndex=(empty array)`| (initial, warning)| `<number: -1>` |
9283
+| `selectedIndex=(object)`| (initial, warning)| `<number: -1>` |
9284
+| `selectedIndex=(numeric string)`| (initial, warning)| `<number: -1>` |
9285
+| `selectedIndex=(-1)`| (initial, warning)| `<number: -1>` |
9286
+| `selectedIndex=(0)`| (initial, warning)| `<number: -1>` |
9287
+| `selectedIndex=(integer)`| (initial, warning)| `<number: -1>` |
9288
| `selectedIndex=(NaN)`| (initial, warning)| `<number: -1>` |
9289
-| `selectedIndex=(float)`| (initial)| `<number: -1>` |
9289
+| `selectedIndex=(float)`| (initial, warning)| `<number: -1>` |
9290
| `selectedIndex=(true)`| (initial, warning)| `<number: -1>` |
9291
| `selectedIndex=(false)`| (initial, warning)| `<number: -1>` |
9292
-| `selectedIndex=(string 'true')`| (initial)| `<number: -1>` |
9293
-| `selectedIndex=(string 'false')`| (initial)| `<number: -1>` |
9294
-| `selectedIndex=(string 'on')`| (initial)| `<number: -1>` |
9295
-| `selectedIndex=(string 'off')`| (initial)| `<number: -1>` |
9292
+| `selectedIndex=(string 'true')`| (initial, warning)| `<number: -1>` |
9293
+| `selectedIndex=(string 'false')`| (initial, warning)| `<number: -1>` |
9294
+| `selectedIndex=(string 'on')`| (initial, warning)| `<number: -1>` |
9295
+| `selectedIndex=(string 'off')`| (initial, warning)| `<number: -1>` |
9296
| `selectedIndex=(symbol)`| (initial, warning)| `<number: -1>` |
9297
| `selectedIndex=(function)`| (initial, warning)| `<number: -1>` |
9298
-| `selectedIndex=(null)`| (initial)| `<number: -1>` |
9299
-| `selectedIndex=(undefined)`| (initial)| `<number: -1>` |
9298
+| `selectedIndex=(null)`| (initial, warning)| `<number: -1>` |
9299
+| `selectedIndex=(undefined)`| (initial, warning)| `<number: -1>` |
9300
9301
## `shape` (on `<div>`)
9302
| Test Case | Flags | Result |
src/renderers/dom/shared/__tests__/ReactDOMAttribute-test.js
+21
-1
@@ -96,7 +96,7 @@ describe('ReactDOM unknown attribute', () => {
96
expectDev(console.error.calls.count()).toBe(1);
97
});
98
99
- it('coerces objects to strings **and warns**', () => {
99
+ it('coerces objects to strings and warns', () => {
100
const lol = {
101
toString() {
102
return 'lol';
@@ -133,5 +133,25 @@ describe('ReactDOM unknown attribute', () => {
133
);
134
expectDev(console.error.calls.count()).toBe(1);
135
});
136
+
137
+ it('allows camelCase unknown attributes and warns', () => {
138
+ spyOn(console, 'error');
139
+
140
+ var el = document.createElement('div');
141
+ ReactDOM.render(<div helloWorld="something" />, el);
142
+ expect(el.firstChild.getAttribute('helloworld')).toBe('something');
143
+
144
+ expectDev(console.error.calls.count()).toBe(1);
145
+ expectDev(
146
+ normalizeCodeLocInfo(console.error.calls.argsFor(0)[0]),
147
+ ).toMatch(
148
+ 'React does not recognize the `helloWorld` prop on a DOM element. ' +
149
+ 'If you intentionally want it to appear in the DOM as a custom ' +
150
+ 'attribute, spell it as lowercase `helloworld` instead. ' +
151
+ 'If you accidentally passed it from a parent component, remove ' +
152
+ 'it from the DOM element.\n' +
153
+ ' in div (at **)',
154
+ );
155
+ });
156
});
157
});
src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js
+32
-6
@@ -2125,13 +2125,41 @@ describe('ReactDOMComponent', () => {
2125
});
2126
2127
it('allows cased data attributes', function() {
2128
+ spyOn(console, 'error');
2129
+
2130
var el = ReactTestUtils.renderIntoDocument(<div data-fooBar="true" />);
2131
expect(el.getAttribute('data-foobar')).toBe('true');
2132
+
2133
+ expectDev(console.error.calls.count()).toBe(1);
2134
+ expectDev(
2135
+ normalizeCodeLocInfo(console.error.calls.argsFor(0)[0]),
2136
+ ).toMatch(
2137
+ 'React does not recognize the `data-fooBar` prop on a DOM element. ' +
2138
+ 'If you intentionally want it to appear in the DOM as a custom ' +
2139
+ 'attribute, spell it as lowercase `data-foobar` instead. ' +
2140
+ 'If you accidentally passed it from a parent component, remove ' +
2141
+ 'it from the DOM element.\n' +
2142
+ ' in div (at **)',
2143
+ );
2144
});
2145
2146
it('allows cased custom attributes', function() {
2147
+ spyOn(console, 'error');
2148
+
2149
var el = ReactTestUtils.renderIntoDocument(<div fooBar="true" />);
2150
expect(el.getAttribute('foobar')).toBe('true');
2151
+
2152
+ expectDev(console.error.calls.count()).toBe(1);
2153
+ expectDev(
2154
+ normalizeCodeLocInfo(console.error.calls.argsFor(0)[0]),
2155
+ ).toMatch(
2156
+ 'React does not recognize the `fooBar` prop on a DOM element. ' +
2157
+ 'If you intentionally want it to appear in the DOM as a custom ' +
2158
+ 'attribute, spell it as lowercase `foobar` instead. ' +
2159
+ 'If you accidentally passed it from a parent component, remove ' +
2160
+ 'it from the DOM element.\n' +
2161
+ ' in div (at **)',
2162
+ );
2163
});
2164
2165
it('warns on NaN attributes', function() {
@@ -2195,10 +2223,8 @@ describe('ReactDOMComponent', () => {
2223
ReactDOM.render(<svg arabicForm={obj} />, container);
2224
expect(container.firstChild.getAttribute('arabic-form')).toBe('hello');
2225
2198
- ReactDOM.render(<div customAttribute={obj} />, container);
2199
- expect(container.firstChild.getAttribute('customAttribute')).toBe(
2200
- 'hello',
2201
- );
2226
+ ReactDOM.render(<div unknown={obj} />, container);
2227
+ expect(container.firstChild.getAttribute('unknown')).toBe('hello');
2228
});
2229
2230
it('passes objects on known SVG attributes if they do not define toString', () => {
@@ -2215,8 +2241,8 @@ describe('ReactDOMComponent', () => {
2241
var obj = {};
2242
var container = document.createElement('div');
2243
2218
- ReactDOM.render(<div customAttribute={obj} />, container);
2219
- expect(container.firstChild.getAttribute('customAttribute')).toBe(
2244
+ ReactDOM.render(<div unknown={obj} />, container);
2245
+ expect(container.firstChild.getAttribute('unknown')).toBe(
2246
'[object Object]',
2247
);
2248
});
src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js
+10
-10
@@ -909,25 +909,25 @@ describe('ReactDOMServerIntegration', () => {
909
});
910
911
itRenders('unknown data- attributes with casing', async render => {
912
- const e = await render(<div data-fooBar="true" />);
913
- expect(e.getAttribute('data-fooBar')).toBe('true');
912
+ const e = await render(<div data-fooBar="true" />, 1);
913
+ expect(e.getAttribute('data-foobar')).toBe('true');
914
});
915
916
itRenders('unknown data- attributes with boolean true', async render => {
917
- const e = await render(<div data-fooBar={true} />);
918
- expect(e.getAttribute('data-fooBar')).toBe('true');
917
+ const e = await render(<div data-foobar={true} />);
918
+ expect(e.getAttribute('data-foobar')).toBe('true');
919
});
920
921
itRenders('unknown data- attributes with boolean false', async render => {
922
- const e = await render(<div data-fooBar={false} />);
923
- expect(e.getAttribute('data-fooBar')).toBe('false');
922
+ const e = await render(<div data-foobar={false} />);
923
+ expect(e.getAttribute('data-foobar')).toBe('false');
924
});
925
926
itRenders(
927
'no unknown data- attributes with casing and null value',
928
async render => {
929
- const e = await render(<div data-fooBar={null} />);
930
- expect(e.hasAttribute('data-fooBar')).toBe(false);
929
+ const e = await render(<div data-fooBar={null} />, 1);
930
+ expect(e.hasAttribute('data-foobar')).toBe(false);
931
},
932
);
933
@@ -972,8 +972,8 @@ describe('ReactDOMServerIntegration', () => {
972
});
973
974
itRenders('cased custom attributes', async render => {
975
- const e = await render(<div fooBar="test" />);
976
- expect(e.getAttribute('fooBar')).toBe('test');
975
+ const e = await render(<div fooBar="test" />, 1);
976
+ expect(e.getAttribute('foobar')).toBe('test');
977
});
978
});
979
src/renderers/dom/shared/hooks/ReactDOMUnknownPropertyHook.js
+24
-3
@@ -39,7 +39,10 @@ if (__DEV__) {
39
var warnedProperties = {};
40
var hasOwnProperty = Object.prototype.hasOwnProperty;
41
var EVENT_NAME_REGEX = /^on[A-Z]/;
42
- var ARIA_NAME_REGEX = /^aria-/i;
42
+ var rARIA = new RegExp('^(aria)-[' + DOMProperty.ATTRIBUTE_NAME_CHAR + ']*$');
43
+ var rARIACamel = new RegExp(
44
+ '^(aria)[A-Z][' + DOMProperty.ATTRIBUTE_NAME_CHAR + ']*$',
45
+ );
46
var possibleStandardNames = require('possibleStandardNames');
47
48
var validateProperty = function(tagName, name, value, debugID) {
@@ -91,7 +94,7 @@ if (__DEV__) {
94
}
95
96
// Let the ARIA attribute hook validate ARIA attributes
94
- if (ARIA_NAME_REGEX.test(name)) {
97
+ if (rARIA.test(name) || rARIACamel.test(name)) {
98
return true;
99
}
100
@@ -155,6 +158,8 @@ if (__DEV__) {
158
return true;
159
}
160
161
+ const isReserved = DOMProperty.isReservedProp(name);
162
+
163
// Known attributes should match the casing specified in the property config.
164
if (possibleStandardNames.hasOwnProperty(lowerCasedName)) {
165
var standardName = possibleStandardNames[lowerCasedName];
@@ -169,6 +174,22 @@ if (__DEV__) {
174
warnedProperties[name] = true;
175
return true;
176
}
177
+ } else if (!isReserved && name !== lowerCasedName) {
178
+ // Unknown attributes should have lowercase casing since that's how they
179
+ // will be cased anyway with server rendering.
180
+ warning(
181
+ false,
182
+ 'React does not recognize the `%s` prop on a DOM element. If you ' +
183
+ 'intentionally want it to appear in the DOM as a custom ' +
184
+ 'attribute, spell it as lowercase `%s` instead. ' +
185
+ 'If you accidentally passed it from a parent component, remove ' +
186
+ 'it from the DOM element.%s',
187
+ name,
188
+ lowerCasedName,
189
+ getStackAddendum(debugID),
190
+ );
191
+ warnedProperties[name] = true;
192
+ return true;
193
}
194
195
if (typeof value === 'boolean') {
@@ -186,7 +207,7 @@ if (__DEV__) {
207
208
// Now that we've validated casing, do not validate
209
// data types for reserved props
189
- if (DOMProperty.isReservedProp(name)) {
210
+ if (isReserved) {
211
return true;
212
}
213