`)
| 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;
}