diff --git a/fixtures/attribute-behavior/AttributeTableSnapshot.md b/fixtures/attribute-behavior/AttributeTableSnapshot.md index e752338898f22..ab011c72a8cca 100644 --- a/fixtures/attribute-behavior/AttributeTableSnapshot.md +++ b/fixtures/attribute-behavior/AttributeTableSnapshot.md @@ -701,23 +701,23 @@ ## `as` (on `
`) | Test Case | Flags | Result | | --- | --- | --- | -| `as=(string)`| (changed)| `"a string"` | +| `as=(string)`| (initial)| `` | | `as=(empty string)`| (initial)| `` | -| `as=(array with string)`| (changed)| `"string"` | +| `as=(array with string)`| (initial)| `` | | `as=(empty array)`| (initial)| `` | -| `as=(object)`| (changed)| `"result of toString()"` | -| `as=(numeric string)`| (changed)| `"42"` | -| `as=(-1)`| (changed)| `"-1"` | -| `as=(0)`| (changed)| `"0"` | -| `as=(integer)`| (changed)| `"1"` | -| `as=(NaN)`| (changed, warning)| `"NaN"` | -| `as=(float)`| (changed)| `"99.99"` | +| `as=(object)`| (initial)| `` | +| `as=(numeric string)`| (initial)| `` | +| `as=(-1)`| (initial)| `` | +| `as=(0)`| (initial)| `` | +| `as=(integer)`| (initial)| `` | +| `as=(NaN)`| (initial, warning)| `` | +| `as=(float)`| (initial)| `` | | `as=(true)`| (initial, warning)| `` | | `as=(false)`| (initial, warning)| `` | -| `as=(string 'true')`| (changed)| `"true"` | -| `as=(string 'false')`| (changed)| `"false"` | -| `as=(string 'on')`| (changed)| `"on"` | -| `as=(string 'off')`| (changed)| `"off"` | +| `as=(string 'true')`| (initial)| `` | +| `as=(string 'false')`| (initial)| `` | +| `as=(string 'on')`| (initial)| `` | +| `as=(string 'off')`| (initial)| `` | | `as=(symbol)`| (initial, warning)| `` | | `as=(function)`| (initial, warning)| `` | | `as=(null)`| (initial)| `` | @@ -5251,52 +5251,52 @@ ## `initialChecked` (on `
`) | Test Case | Flags | Result | | --- | --- | --- | -| `initialChecked=(string)`| (changed)| `"a string"` | -| `initialChecked=(empty string)`| (changed)| `` | -| `initialChecked=(array with string)`| (changed)| `"string"` | -| `initialChecked=(empty array)`| (changed)| `` | -| `initialChecked=(object)`| (changed)| `"result of toString()"` | -| `initialChecked=(numeric string)`| (changed)| `"42"` | -| `initialChecked=(-1)`| (changed)| `"-1"` | -| `initialChecked=(0)`| (changed)| `"0"` | -| `initialChecked=(integer)`| (changed)| `"1"` | +| `initialChecked=(string)`| (changed, warning)| `"a string"` | +| `initialChecked=(empty string)`| (changed, warning)| `` | +| `initialChecked=(array with string)`| (changed, warning)| `"string"` | +| `initialChecked=(empty array)`| (changed, warning)| `` | +| `initialChecked=(object)`| (changed, warning)| `"result of toString()"` | +| `initialChecked=(numeric string)`| (changed, warning)| `"42"` | +| `initialChecked=(-1)`| (changed, warning)| `"-1"` | +| `initialChecked=(0)`| (changed, warning)| `"0"` | +| `initialChecked=(integer)`| (changed, warning)| `"1"` | | `initialChecked=(NaN)`| (changed, warning)| `"NaN"` | -| `initialChecked=(float)`| (changed)| `"99.99"` | +| `initialChecked=(float)`| (changed, warning)| `"99.99"` | | `initialChecked=(true)`| (initial, warning)| `` | | `initialChecked=(false)`| (initial, warning)| `` | -| `initialChecked=(string 'true')`| (changed)| `"true"` | -| `initialChecked=(string 'false')`| (changed)| `"false"` | -| `initialChecked=(string 'on')`| (changed)| `"on"` | -| `initialChecked=(string 'off')`| (changed)| `"off"` | +| `initialChecked=(string 'true')`| (changed, warning)| `"true"` | +| `initialChecked=(string 'false')`| (changed, warning)| `"false"` | +| `initialChecked=(string 'on')`| (changed, warning)| `"on"` | +| `initialChecked=(string 'off')`| (changed, warning)| `"off"` | | `initialChecked=(symbol)`| (initial, warning)| `` | | `initialChecked=(function)`| (initial, warning)| `` | -| `initialChecked=(null)`| (initial)| `` | -| `initialChecked=(undefined)`| (initial)| `` | +| `initialChecked=(null)`| (initial, warning)| `` | +| `initialChecked=(undefined)`| (initial, warning)| `` | ## `initialValue` (on `
`) | Test Case | Flags | Result | | --- | --- | --- | -| `initialValue=(string)`| (changed)| `"a string"` | -| `initialValue=(empty string)`| (changed)| `` | -| `initialValue=(array with string)`| (changed)| `"string"` | -| `initialValue=(empty array)`| (changed)| `` | -| `initialValue=(object)`| (changed)| `"result of toString()"` | -| `initialValue=(numeric string)`| (changed)| `"42"` | -| `initialValue=(-1)`| (changed)| `"-1"` | -| `initialValue=(0)`| (changed)| `"0"` | -| `initialValue=(integer)`| (changed)| `"1"` | +| `initialValue=(string)`| (changed, warning)| `"a string"` | +| `initialValue=(empty string)`| (changed, warning)| `` | +| `initialValue=(array with string)`| (changed, warning)| `"string"` | +| `initialValue=(empty array)`| (changed, warning)| `` | +| `initialValue=(object)`| (changed, warning)| `"result of toString()"` | +| `initialValue=(numeric string)`| (changed, warning)| `"42"` | +| `initialValue=(-1)`| (changed, warning)| `"-1"` | +| `initialValue=(0)`| (changed, warning)| `"0"` | +| `initialValue=(integer)`| (changed, warning)| `"1"` | | `initialValue=(NaN)`| (changed, warning)| `"NaN"` | -| `initialValue=(float)`| (changed)| `"99.99"` | +| `initialValue=(float)`| (changed, warning)| `"99.99"` | | `initialValue=(true)`| (initial, warning)| `` | | `initialValue=(false)`| (initial, warning)| `` | -| `initialValue=(string 'true')`| (changed)| `"true"` | -| `initialValue=(string 'false')`| (changed)| `"false"` | -| `initialValue=(string 'on')`| (changed)| `"on"` | -| `initialValue=(string 'off')`| (changed)| `"off"` | +| `initialValue=(string 'true')`| (changed, warning)| `"true"` | +| `initialValue=(string 'false')`| (changed, warning)| `"false"` | +| `initialValue=(string 'on')`| (changed, warning)| `"on"` | +| `initialValue=(string 'off')`| (changed, warning)| `"off"` | | `initialValue=(symbol)`| (initial, warning)| `` | | `initialValue=(function)`| (initial, warning)| `` | -| `initialValue=(null)`| (initial)| `` | -| `initialValue=(undefined)`| (initial)| `` | +| `initialValue=(null)`| (initial, warning)| `` | +| `initialValue=(undefined)`| (initial, warning)| `` | ## `inlist` (on `
`) | Test Case | Flags | Result | @@ -9276,27 +9276,27 @@ ## `selectedIndex` (on `
`) | Test Case | Flags | Result | | --- | --- | --- | -| `selectedIndex=(string)`| (initial)| `` | -| `selectedIndex=(empty string)`| (initial)| `` | -| `selectedIndex=(array with string)`| (initial)| `` | -| `selectedIndex=(empty array)`| (initial)| `` | -| `selectedIndex=(object)`| (initial)| `` | -| `selectedIndex=(numeric string)`| (initial)| `` | -| `selectedIndex=(-1)`| (initial)| `` | -| `selectedIndex=(0)`| (initial)| `` | -| `selectedIndex=(integer)`| (initial)| `` | +| `selectedIndex=(string)`| (initial, warning)| `` | +| `selectedIndex=(empty string)`| (initial, warning)| `` | +| `selectedIndex=(array with string)`| (initial, warning)| `` | +| `selectedIndex=(empty array)`| (initial, warning)| `` | +| `selectedIndex=(object)`| (initial, warning)| `` | +| `selectedIndex=(numeric string)`| (initial, warning)| `` | +| `selectedIndex=(-1)`| (initial, warning)| `` | +| `selectedIndex=(0)`| (initial, warning)| `` | +| `selectedIndex=(integer)`| (initial, warning)| `` | | `selectedIndex=(NaN)`| (initial, warning)| `` | -| `selectedIndex=(float)`| (initial)| `` | +| `selectedIndex=(float)`| (initial, warning)| `` | | `selectedIndex=(true)`| (initial, warning)| `` | | `selectedIndex=(false)`| (initial, warning)| `` | -| `selectedIndex=(string 'true')`| (initial)| `` | -| `selectedIndex=(string 'false')`| (initial)| `` | -| `selectedIndex=(string 'on')`| (initial)| `` | -| `selectedIndex=(string 'off')`| (initial)| `` | +| `selectedIndex=(string 'true')`| (initial, warning)| `` | +| `selectedIndex=(string 'false')`| (initial, warning)| `` | +| `selectedIndex=(string 'on')`| (initial, warning)| `` | +| `selectedIndex=(string 'off')`| (initial, warning)| `` | | `selectedIndex=(symbol)`| (initial, warning)| `` | | `selectedIndex=(function)`| (initial, warning)| `` | -| `selectedIndex=(null)`| (initial)| `` | -| `selectedIndex=(undefined)`| (initial)| `` | +| `selectedIndex=(null)`| (initial, warning)| `` | +| `selectedIndex=(undefined)`| (initial, warning)| `` | ## `shape` (on `
`) | Test Case | Flags | Result | diff --git a/src/renderers/dom/shared/__tests__/ReactDOMAttribute-test.js b/src/renderers/dom/shared/__tests__/ReactDOMAttribute-test.js index 6ead95c78d416..04dedf7871beb 100644 --- a/src/renderers/dom/shared/__tests__/ReactDOMAttribute-test.js +++ b/src/renderers/dom/shared/__tests__/ReactDOMAttribute-test.js @@ -96,7 +96,7 @@ describe('ReactDOM unknown attribute', () => { expectDev(console.error.calls.count()).toBe(1); }); - it('coerces objects to strings **and warns**', () => { + it('coerces objects to strings and warns', () => { const lol = { toString() { return 'lol'; @@ -133,5 +133,25 @@ describe('ReactDOM unknown attribute', () => { ); expectDev(console.error.calls.count()).toBe(1); }); + + it('allows camelCase unknown attributes and warns', () => { + spyOn(console, 'error'); + + var el = document.createElement('div'); + ReactDOM.render(
, el); + expect(el.firstChild.getAttribute('helloworld')).toBe('something'); + + expectDev(console.error.calls.count()).toBe(1); + expectDev( + normalizeCodeLocInfo(console.error.calls.argsFor(0)[0]), + ).toMatch( + 'React does not recognize the `helloWorld` prop on a DOM element. ' + + 'If you intentionally want it to appear in the DOM as a custom ' + + 'attribute, spell it as lowercase `helloworld` instead. ' + + 'If you accidentally passed it from a parent component, remove ' + + 'it from the DOM element.\n' + + ' in div (at **)', + ); + }); }); }); diff --git a/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js b/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js index 206a74ba11719..ba21f482afbe7 100644 --- a/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js +++ b/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js @@ -2125,13 +2125,41 @@ describe('ReactDOMComponent', () => { }); it('allows cased data attributes', function() { + spyOn(console, 'error'); + var el = ReactTestUtils.renderIntoDocument(
); expect(el.getAttribute('data-foobar')).toBe('true'); + + expectDev(console.error.calls.count()).toBe(1); + expectDev( + normalizeCodeLocInfo(console.error.calls.argsFor(0)[0]), + ).toMatch( + 'React does not recognize the `data-fooBar` prop on a DOM element. ' + + 'If you intentionally want it to appear in the DOM as a custom ' + + 'attribute, spell it as lowercase `data-foobar` instead. ' + + 'If you accidentally passed it from a parent component, remove ' + + 'it from the DOM element.\n' + + ' in div (at **)', + ); }); it('allows cased custom attributes', function() { + spyOn(console, 'error'); + var el = ReactTestUtils.renderIntoDocument(
); expect(el.getAttribute('foobar')).toBe('true'); + + expectDev(console.error.calls.count()).toBe(1); + expectDev( + normalizeCodeLocInfo(console.error.calls.argsFor(0)[0]), + ).toMatch( + 'React does not recognize the `fooBar` prop on a DOM element. ' + + 'If you intentionally want it to appear in the DOM as a custom ' + + 'attribute, spell it as lowercase `foobar` instead. ' + + 'If you accidentally passed it from a parent component, remove ' + + 'it from the DOM element.\n' + + ' in div (at **)', + ); }); it('warns on NaN attributes', function() { @@ -2195,10 +2223,8 @@ describe('ReactDOMComponent', () => { ReactDOM.render(, container); expect(container.firstChild.getAttribute('arabic-form')).toBe('hello'); - ReactDOM.render(
, container); - expect(container.firstChild.getAttribute('customAttribute')).toBe( - 'hello', - ); + ReactDOM.render(
, container); + expect(container.firstChild.getAttribute('unknown')).toBe('hello'); }); it('passes objects on known SVG attributes if they do not define toString', () => { @@ -2215,8 +2241,8 @@ describe('ReactDOMComponent', () => { var obj = {}; var container = document.createElement('div'); - ReactDOM.render(
, container); - expect(container.firstChild.getAttribute('customAttribute')).toBe( + ReactDOM.render(
, container); + expect(container.firstChild.getAttribute('unknown')).toBe( '[object Object]', ); }); diff --git a/src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js b/src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js index 9016dad76cbdf..792360312be27 100644 --- a/src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js +++ b/src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js @@ -909,25 +909,25 @@ describe('ReactDOMServerIntegration', () => { }); itRenders('unknown data- attributes with casing', async render => { - const e = await render(
); - expect(e.getAttribute('data-fooBar')).toBe('true'); + const e = await render(
, 1); + expect(e.getAttribute('data-foobar')).toBe('true'); }); itRenders('unknown data- attributes with boolean true', async render => { - const e = await render(
); - expect(e.getAttribute('data-fooBar')).toBe('true'); + const e = await render(
); + expect(e.getAttribute('data-foobar')).toBe('true'); }); itRenders('unknown data- attributes with boolean false', async render => { - const e = await render(
); - expect(e.getAttribute('data-fooBar')).toBe('false'); + const e = await render(
); + expect(e.getAttribute('data-foobar')).toBe('false'); }); itRenders( 'no unknown data- attributes with casing and null value', async render => { - const e = await render(
); - expect(e.hasAttribute('data-fooBar')).toBe(false); + const e = await render(
, 1); + expect(e.hasAttribute('data-foobar')).toBe(false); }, ); @@ -972,8 +972,8 @@ describe('ReactDOMServerIntegration', () => { }); itRenders('cased custom attributes', async render => { - const e = await render(
); - expect(e.getAttribute('fooBar')).toBe('test'); + const e = await render(
, 1); + expect(e.getAttribute('foobar')).toBe('test'); }); }); diff --git a/src/renderers/dom/shared/hooks/ReactDOMUnknownPropertyHook.js b/src/renderers/dom/shared/hooks/ReactDOMUnknownPropertyHook.js index 1ce2a90fecb63..f49442ccf3d4e 100644 --- a/src/renderers/dom/shared/hooks/ReactDOMUnknownPropertyHook.js +++ b/src/renderers/dom/shared/hooks/ReactDOMUnknownPropertyHook.js @@ -39,7 +39,10 @@ if (__DEV__) { var warnedProperties = {}; var hasOwnProperty = Object.prototype.hasOwnProperty; var EVENT_NAME_REGEX = /^on[A-Z]/; - var ARIA_NAME_REGEX = /^aria-/i; + var rARIA = new RegExp('^(aria)-[' + DOMProperty.ATTRIBUTE_NAME_CHAR + ']*$'); + var rARIACamel = new RegExp( + '^(aria)[A-Z][' + DOMProperty.ATTRIBUTE_NAME_CHAR + ']*$', + ); var possibleStandardNames = require('possibleStandardNames'); var validateProperty = function(tagName, name, value, debugID) { @@ -91,7 +94,7 @@ if (__DEV__) { } // Let the ARIA attribute hook validate ARIA attributes - if (ARIA_NAME_REGEX.test(name)) { + if (rARIA.test(name) || rARIACamel.test(name)) { return true; } @@ -155,6 +158,8 @@ if (__DEV__) { return true; } + const isReserved = DOMProperty.isReservedProp(name); + // Known attributes should match the casing specified in the property config. if (possibleStandardNames.hasOwnProperty(lowerCasedName)) { var standardName = possibleStandardNames[lowerCasedName]; @@ -169,6 +174,22 @@ if (__DEV__) { warnedProperties[name] = true; return true; } + } else if (!isReserved && name !== lowerCasedName) { + // Unknown attributes should have lowercase casing since that's how they + // will be cased anyway with server rendering. + warning( + false, + 'React does not recognize the `%s` prop on a DOM element. If you ' + + 'intentionally want it to appear in the DOM as a custom ' + + 'attribute, spell it as lowercase `%s` instead. ' + + 'If you accidentally passed it from a parent component, remove ' + + 'it from the DOM element.%s', + name, + lowerCasedName, + getStackAddendum(debugID), + ); + warnedProperties[name] = true; + return true; } if (typeof value === 'boolean') { @@ -186,7 +207,7 @@ if (__DEV__) { // Now that we've validated casing, do not validate // data types for reserved props - if (DOMProperty.isReservedProp(name)) { + if (isReserved) { return true; }