Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
12 changes: 11 additions & 1 deletion packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '';
Expand Down
53 changes: 53 additions & 0 deletions packages/react-dom/src/__tests__/DOMPropertyOperations-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<my-custom-element
aria-disabled={true}
aria-label={'label test'}
aria-hidden={false}
aria-required={undefined}
aria-colindex={1}
aria-rowindex={0}
/>,
);
});
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(
<my-custom-element
data-false={false}
data-true={true}
data-undefined={undefined}
data-null={null}
data-label={'label test'}
data-index={1}
data-index-0={0}
/>,
);
});
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(<custom-element aria-label="hello" />);
expect(e.getAttribute('aria-label')).toBe('hello');
});

itRenders(
'aria string prop with false value on custom elements',
async render => {
const e = await render(<custom-element aria-hidden={false} />);
expect(e.getAttribute('aria-hidden')).toBe('false');
},
);

itRenders(
'aria string prop with true value on custom elements',
async render => {
const e = await render(<custom-element aria-hidden={true} />);
expect(e.getAttribute('aria-hidden')).toBe('true');
},
);

itRenders(
'aria string prop with number value on custom elements',
async render => {
const e = await render(<custom-element aria-label={1} />);
expect(e.getAttribute('aria-label')).toBe('1');
},
);

itRenders(
'aria string prop with string value on custom elements',
async render => {
const e = await render(<custom-element aria-label="hello" />);
expect(e.getAttribute('aria-label')).toBe('hello');
},
);

itRenders(
'no aria prop with null value on custom elements',
async render => {
const e = await render(<custom-element aria-label={null} />);
expect(e.hasAttribute('aria-label')).toBe(false);
},
);

itRenders(
'no aria prop with undefined value on custom elements',
async render => {
const e = await render(<custom-element aria-label={undefined} />);
expect(e.hasAttribute('aria-label')).toBe(false);
},
);
});
});

describe('data attributes', function () {
itRenders('simple strings on custom elements', async render => {
const e = await render(<custom-element data-foo="hello" />);
expect(e.getAttribute('data-foo')).toBe('hello');
});

itRenders(
'data string prop with false value on custom elements',
async render => {
const e = await render(<custom-element data-foo={false} />);
expect(e.getAttribute('data-foo')).toBe('false');
},
);

itRenders(
'data string prop with true value on custom elements',
async render => {
const e = await render(<custom-element data-foo={true} />);
expect(e.getAttribute('data-foo')).toBe('true');
},
);

itRenders(
'no data prop with null value on custom elements',
async render => {
const e = await render(<custom-element data-foo={null} />);
expect(e.hasAttribute('data-foo')).toBe(false);
},
);

itRenders(
'no data prop with undefined value on custom elements',
async render => {
const e = await render(<custom-element data-foo={undefined} />);
expect(e.hasAttribute('data-foo')).toBe(false);
},
);

itRenders(
'no data prop with function value on custom elements',
async render => {
const e = await render(<custom-element data-foo={function () {}} />);
expect(e.hasAttribute('data-foo')).toBe(false);
},
);

itRenders(
'data string prop for number value on custom elements',
async render => {
const e = await render(<custom-element data-foo={1} />);
expect(e.getAttribute('data-foo')).toBe('1');
},
);

itRenders(
'data string prop for string value on custom elements',
async render => {
const e = await render(<custom-element data-foo="hello" />);
expect(e.getAttribute('data-foo')).toBe('hello');
},
);
});
});