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');
+ },
+ );
});
});