diff --git a/packages/react-dom-bindings/src/client/DOMPropertyOperations.js b/packages/react-dom-bindings/src/client/DOMPropertyOperations.js index df09445acc55..cd915b8d26cf 100644 --- a/packages/react-dom-bindings/src/client/DOMPropertyOperations.js +++ b/packages/react-dom-bindings/src/client/DOMPropertyOperations.js @@ -226,10 +226,13 @@ export function setValueForPropertyOnCustomComponent( } if (value === true) { - node.setAttribute(name, ''); - return; + const prefix = name.toLowerCase().slice(0, 5); + if (prefix !== 'data-' && prefix !== 'aria-') { + // for non aria and data attributes, set the attribute to an empty string (just the value on the DOM) + node.setAttribute(name, ''); + return; + } } - // From here, it's the same as any attribute setValueForAttribute(node, name, value); } diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js index 691e49e563fd..e9c6f18a672d 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -4019,7 +4019,17 @@ function pushStartCustomElement( typeof propValue !== 'function' && typeof propValue !== 'symbol' ) { - if (propValue === false) { + const prefix = attributeName.toLowerCase().slice(0, 5); + const propValueNeedsStringification = + prefix === 'data-' || prefix === 'aria-'; + if ( + propValueNeedsStringification && + typeof propValue === 'boolean' + ) { + // aria and data attributes are stringified as 'true' if they have the boolean value `true` + // this is because data-foo="true" is the same as data-foo on the DOM + propValue = propValue ? 'true' : 'false'; + } else if (propValue === false) { continue; } else if (propValue === true) { propValue = ''; diff --git a/packages/react-dom/src/__tests__/DOMPropertyOperations-test.js b/packages/react-dom/src/__tests__/DOMPropertyOperations-test.js index 5bbc2c8dbac8..a40ba6c0a3fe 100644 --- a/packages/react-dom/src/__tests__/DOMPropertyOperations-test.js +++ b/packages/react-dom/src/__tests__/DOMPropertyOperations-test.js @@ -1151,6 +1151,59 @@ describe('DOMPropertyOperations', () => { expect(customElement.getAttribute('foo')).toBe(null); }); + it('aria attributes should have proper representation', async () => { + const container = document.createElement('div'); + document.body.appendChild(container); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + , + ); + }); + const customElement = container.querySelector('my-custom-element'); + + expect(customElement.getAttribute('aria-disabled')).toBe('true'); + expect(customElement.getAttribute('aria-label')).toBe('label test'); + expect(customElement.getAttribute('aria-hidden')).toBe('false'); + expect(customElement.getAttribute('aria-required')).toBe(null); + expect(customElement.getAttribute('aria-colindex')).toBe('1'); + expect(customElement.getAttribute('aria-rowindex')).toBe('0'); + }); + + it('data attributes should have proper representation', async () => { + const container = document.createElement('div'); + document.body.appendChild(container); + const root = ReactDOMClient.createRoot(container); + await act(() => { + root.render( + , + ); + }); + const customElement = container.querySelector('my-custom-element'); + expect(customElement.getAttribute('data-false')).toBe('false'); + expect(customElement.getAttribute('data-true')).toBe('true'); + expect(customElement.getAttribute('data-undefined')).toBe(null); + expect(customElement.getAttribute('data-null')).toBe(null); + expect(customElement.getAttribute('data-label')).toBe('label test'); + expect(customElement.getAttribute('data-index')).toBe('1'); + expect(customElement.getAttribute('data-index-0')).toBe('0'); + }); + it('custom element custom event handlers assign multiple types', async () => { const container = document.createElement('div'); document.body.appendChild(container); diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationAttributes-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationAttributes-test.js index 9857718c35b7..18224136d5e7 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationAttributes-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationAttributes-test.js @@ -806,5 +806,123 @@ describe('ReactDOMServerIntegration', () => { expect(e.hasAttribute('foo')).toBe(false); }, ); + + describe('aria attributes', function () { + itRenders('simple strings on custom elements', async render => { + const e = await render(); + expect(e.getAttribute('aria-label')).toBe('hello'); + }); + + itRenders( + 'aria string prop with false value on custom elements', + async render => { + const e = await render(); + expect(e.getAttribute('aria-hidden')).toBe('false'); + }, + ); + + itRenders( + 'aria string prop with true value on custom elements', + async render => { + const e = await render(); + expect(e.getAttribute('aria-hidden')).toBe('true'); + }, + ); + + itRenders( + 'aria string prop with number value on custom elements', + async render => { + const e = await render(); + expect(e.getAttribute('aria-label')).toBe('1'); + }, + ); + + itRenders( + 'aria string prop with string value on custom elements', + async render => { + const e = await render(); + expect(e.getAttribute('aria-label')).toBe('hello'); + }, + ); + + itRenders( + 'no aria prop with null value on custom elements', + async render => { + const e = await render(); + expect(e.hasAttribute('aria-label')).toBe(false); + }, + ); + + itRenders( + 'no aria prop with undefined value on custom elements', + async render => { + const e = await render(); + expect(e.hasAttribute('aria-label')).toBe(false); + }, + ); + }); + }); + + describe('data attributes', function () { + itRenders('simple strings on custom elements', async render => { + const e = await render(); + expect(e.getAttribute('data-foo')).toBe('hello'); + }); + + itRenders( + 'data string prop with false value on custom elements', + async render => { + const e = await render(); + expect(e.getAttribute('data-foo')).toBe('false'); + }, + ); + + itRenders( + 'data string prop with true value on custom elements', + async render => { + const e = await render(); + expect(e.getAttribute('data-foo')).toBe('true'); + }, + ); + + itRenders( + 'no data prop with null value on custom elements', + async render => { + const e = await render(); + expect(e.hasAttribute('data-foo')).toBe(false); + }, + ); + + itRenders( + 'no data prop with undefined value on custom elements', + async render => { + const e = await render(); + expect(e.hasAttribute('data-foo')).toBe(false); + }, + ); + + itRenders( + 'no data prop with function value on custom elements', + async render => { + const e = await render(); + expect(e.hasAttribute('data-foo')).toBe(false); + }, + ); + + itRenders( + 'data string prop for number value on custom elements', + async render => { + const e = await render(); + expect(e.getAttribute('data-foo')).toBe('1'); + }, + ); + + itRenders( + 'data string prop for string value on custom elements', + async render => { + const e = await render(); + expect(e.getAttribute('data-foo')).toBe('hello'); + }, + ); }); });