diff --git a/packages/react-dom-bindings/src/shared/ReactDOMInvalidARIAHook.js b/packages/react-dom-bindings/src/shared/ReactDOMInvalidARIAHook.js index 9cce3b5807aa..139bfd7dea59 100644 --- a/packages/react-dom-bindings/src/shared/ReactDOMInvalidARIAHook.js +++ b/packages/react-dom-bindings/src/shared/ReactDOMInvalidARIAHook.js @@ -13,7 +13,7 @@ const warnedProperties = {}; const rARIA = new RegExp('^(aria)-[' + ATTRIBUTE_NAME_CHAR + ']*$'); const rARIACamel = new RegExp('^(aria)[A-Z][' + ATTRIBUTE_NAME_CHAR + ']*$'); -function validateProperty(tagName, name) { +function validateProperty(tagName, name, value) { if (__DEV__) { if (hasOwnProperty.call(warnedProperties, name) && warnedProperties[name]) { return true; @@ -69,6 +69,19 @@ function validateProperty(tagName, name) { warnedProperties[name] = true; return true; } + // NaN and Infinity are never valid aria attribute values. The DOM will + // stringify them (e.g. "NaN", "Infinity") which violates the ARIA spec + // for every attribute type. + if (typeof value === 'number' && !isFinite(value)) { + console.error( + 'Received `%s` for the `%s` attribute. If this is expected, cast ' + + 'the value to a string.', + value, + name, + ); + warnedProperties[name] = true; + return true; + } } } @@ -80,7 +93,7 @@ export function validateProperties(type, props) { const invalidProps = []; for (const key in props) { - const isValid = validateProperty(type, key); + const isValid = validateProperty(type, key, props[key]); if (!isValid) { invalidProps.push(key); } diff --git a/packages/react-dom/src/__tests__/ReactDOMInvalidARIAHook-test.js b/packages/react-dom/src/__tests__/ReactDOMInvalidARIAHook-test.js index a5fd14e95c96..538a98c59dea 100644 --- a/packages/react-dom/src/__tests__/ReactDOMInvalidARIAHook-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMInvalidARIAHook-test.js @@ -106,5 +106,47 @@ describe('ReactDOMInvalidARIAHook', () => { ' in div (at **)', ]); }); + + it('should warn when a valid aria-* attribute receives a NaN value', async () => { + await mountComponent({'aria-valuenow': NaN}); + assertConsoleErrorDev([ + 'Received `NaN` for the `aria-valuenow` attribute. If this is expected, cast ' + + 'the value to a string.\n' + + ' in div (at **)', + ]); + }); + + it('should warn when a string-type aria-* attribute receives a NaN value', async () => { + await mountComponent({'aria-label': NaN}); + assertConsoleErrorDev([ + 'Received `NaN` for the `aria-label` attribute. If this is expected, cast ' + + 'the value to a string.\n' + + ' in div (at **)', + ]); + }); + + it('should warn when a valid aria-* attribute receives an Infinity value', async () => { + await mountComponent({'aria-valuenow': Infinity}); + assertConsoleErrorDev([ + 'Received `Infinity` for the `aria-valuenow` attribute. If this is expected, cast ' + + 'the value to a string.\n' + + ' in div (at **)', + ]); + }); + + it('should warn when a valid aria-* attribute receives a -Infinity value', async () => { + await mountComponent({'aria-valuemin': -Infinity}); + assertConsoleErrorDev([ + 'Received `-Infinity` for the `aria-valuemin` attribute. If this is expected, cast ' + + 'the value to a string.\n' + + ' in div (at **)', + ]); + }); + + it('should not warn for valid numeric values in aria-* attributes', async () => { + await mountComponent({'aria-valuenow': 42}); + await mountComponent({'aria-level': 3}); + await mountComponent({'aria-colcount': -1}); + }); }); });