@samitouri / QOS-React-2 / commits / c99bad9483

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