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
4 changes: 2 additions & 2 deletions packages/react-dom-bindings/src/client/ReactDOMComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -762,7 +762,6 @@ function setProp(
case 'disablePictureInPicture':
case 'disableRemotePlayback':
case 'formNoValidate':
case 'hidden':
case 'loop':
case 'noModule':
case 'noValidate':
Expand All @@ -782,6 +781,7 @@ function setProp(
break;
}
// Overloaded Boolean
case 'hidden':
case 'capture':
case 'download': {
// An attribute that can be used as a flag as well as with a value.
Expand Down Expand Up @@ -2855,7 +2855,6 @@ function diffHydratedGenericElement(
case 'disablePictureInPicture':
case 'disableRemotePlayback':
case 'formNoValidate':
case 'hidden':
case 'loop':
case 'noModule':
case 'noValidate':
Expand All @@ -2878,6 +2877,7 @@ function diffHydratedGenericElement(
);
continue;
}
case 'hidden':
case 'capture':
case 'download': {
hydrateOverloadedBooleanAttribute(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export type Props = {
autoFocus?: boolean,
children?: mixed,
disabled?: boolean,
hidden?: boolean,
hidden?: boolean | string,
suppressHydrationWarning?: boolean,
dangerouslySetInnerHTML?: mixed,
style?: {
Expand Down
24 changes: 19 additions & 5 deletions packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -1701,7 +1701,6 @@ function pushAttribute(
case 'disablePictureInPicture':
case 'disableRemotePlayback':
case 'formNoValidate':
case 'hidden':
case 'loop':
case 'noModule':
case 'noValidate':
Expand All @@ -1723,6 +1722,7 @@ function pushAttribute(
}
return;
}
case 'hidden':
case 'capture':
case 'download': {
// Overloaded Boolean
Expand Down Expand Up @@ -5848,12 +5848,19 @@ function writeStyleResourceAttributeInJS(
attributeValue = '' + (value: any);
break;
}
// Booleans
// Overloaded Booleans
case 'hidden': {
if (value === false) {
return;
}
attributeValue = '';
if (value === true) {
attributeValue = '';
} else {
if (__DEV__) {
checkAttributeStringCoercion(value, attributeName);
}
attributeValue = '' + (value: any);
}
break;
}
// Santized URLs
Expand Down Expand Up @@ -6043,12 +6050,19 @@ function writeStyleResourceAttributeInAttr(
break;
}

// Booleans
// Overloaded Booleans
case 'hidden': {
if (value === false) {
return;
}
attributeValue = '';
if (value === true) {
attributeValue = '';
} else {
if (__DEV__) {
checkAttributeStringCoercion(value, attributeName);
}
attributeValue = '' + (value: any);
}
break;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,6 @@ function validateProperty(tagName, name, value, eventRegistry) {
case 'disablePictureInPicture':
case 'disableRemotePlayback':
case 'formNoValidate':
case 'hidden':
case 'loop':
case 'noModule':
case 'noValidate':
Expand Down
78 changes: 71 additions & 7 deletions packages/react-dom/src/__tests__/ReactDOMComponent-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3614,16 +3614,18 @@ describe('ReactDOMComponent', () => {
const root = ReactDOMClient.createRoot(container);

await act(() => {
root.render(<div hidden="false" ref={current => (el = current)} />);
root.render(
<div disabled="false" ref={current => (el = current)} />,
);
});
assertConsoleErrorDev([
'Received the string `false` for the boolean attribute `hidden`. ' +
'Received the string `false` for the boolean attribute `disabled`. ' +
'The browser will interpret it as a truthy value. ' +
'Did you mean hidden={false}?\n' +
'Did you mean disabled={false}?\n' +
' in div (at **)',
]);

expect(el.getAttribute('hidden')).toBe('');
expect(el.getAttribute('disabled')).toBe('');
});

it('warns on the potentially-ambiguous string value "true"', async function () {
Expand All @@ -3632,17 +3634,79 @@ describe('ReactDOMComponent', () => {
const root = ReactDOMClient.createRoot(container);

await act(() => {
root.render(<div hidden="true" ref={current => (el = current)} />);
root.render(
<div disabled="true" ref={current => (el = current)} />,
);
});
assertConsoleErrorDev([
'Received the string `true` for the boolean attribute `hidden`. ' +
'Received the string `true` for the boolean attribute `disabled`. ' +
'Although this works, it will not work as expected if you pass the string "false". ' +
'Did you mean hidden={true}?\n' +
'Did you mean disabled={true}?\n' +
' in div (at **)',
]);

expect(el.getAttribute('disabled')).toBe('');
});
});

describe('Overloaded Boolean: hidden attribute', function () {
it('accepts boolean true', async function () {
let el;
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);

await act(() => {
root.render(<div hidden={true} ref={current => (el = current)} />);
});

expect(el.getAttribute('hidden')).toBe('');
});

it('accepts boolean false and removes the attribute', async function () {
let el;
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);

await act(() => {
root.render(<div hidden={false} ref={current => (el = current)} />);
});

expect(el.getAttribute('hidden')).toBe(null);
});

it('accepts the string value "until-found"', async function () {
let el;
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);

await act(() => {
root.render(
<div hidden="until-found" ref={current => (el = current)} />,
);
});

expect(el.getAttribute('hidden')).toBe('until-found');
});

it('accepts arbitrary string values', async function () {
let el;
const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);

await act(() => {
root.render(
<div hidden="until-found" ref={current => (el = current)} />,
);
});

expect(el.getAttribute('hidden')).toBe('until-found');

await act(() => {
root.render(<div hidden="true" ref={current => (el = current)} />);
});

expect(el.getAttribute('hidden')).toBe('true');
});
});

describe('Hyphenated SVG elements', function () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,69 +119,78 @@ describe('ReactDOMServerIntegration', () => {

describe('boolean properties', function () {
itRenders('boolean prop with true value', async render => {
const e = await render(<div disabled={true} />);
expect(e.getAttribute('disabled')).toBe('');
});

itRenders('boolean prop with false value', async render => {
const e = await render(<div disabled={false} />);
expect(e.getAttribute('disabled')).toBe(null);
});
});

describe('overloaded boolean properties (hidden)', function () {
itRenders('hidden prop with true value', async render => {
const e = await render(<div hidden={true} />);
expect(e.getAttribute('hidden')).toBe('');
});

itRenders('boolean prop with false value', async render => {
itRenders('hidden prop with false value', async render => {
const e = await render(<div hidden={false} />);
expect(e.getAttribute('hidden')).toBe(null);
});

itRenders('boolean prop with self value', async render => {
itRenders('hidden prop with "until-found" value', async render => {
const e = await render(<div hidden="until-found" />);
expect(e.getAttribute('hidden')).toBe('until-found');
});

itRenders('hidden prop with self value', async render => {
const e = await render(<div hidden="hidden" />);
expect(e.getAttribute('hidden')).toBe('');
expect(e.getAttribute('hidden')).toBe('hidden');
});

// this does not seem like correct behavior, since hidden="" in HTML indicates
// that the boolean property is present. however, it is how the current code
// behaves, so the test is included here.
itRenders('boolean prop with "" value', async render => {
itRenders('hidden prop with "" value', async render => {
const e = await render(<div hidden="" />);
expect(e.getAttribute('hidden')).toBe(null);
expect(e.getAttribute('hidden')).toBe('');
});

// this seems like it might mask programmer error, but it's existing behavior.
itRenders('boolean prop with string value', async render => {
itRenders('hidden prop with string value', async render => {
const e = await render(<div hidden="foo" />);
expect(e.getAttribute('hidden')).toBe('');
expect(e.getAttribute('hidden')).toBe('foo');
});

// this seems like it might mask programmer error, but it's existing behavior.
itRenders('boolean prop with array value', async render => {
itRenders('hidden prop with array value', async render => {
const e = await render(<div hidden={['foo', 'bar']} />);
expect(e.getAttribute('hidden')).toBe('');
expect(e.getAttribute('hidden')).toBe('foo,bar');
});

// this seems like it might mask programmer error, but it's existing behavior.
itRenders('boolean prop with object value', async render => {
itRenders('hidden prop with object value', async render => {
const e = await render(<div hidden={{foo: 'bar'}} />);
expect(e.getAttribute('hidden')).toBe('');
expect(e.getAttribute('hidden')).toBe('[object Object]');
});

// this seems like it might mask programmer error, but it's existing behavior.
itRenders('boolean prop with non-zero number value', async render => {
itRenders('hidden prop with non-zero number value', async render => {
const e = await render(<div hidden={10} />);
expect(e.getAttribute('hidden')).toBe('');
expect(e.getAttribute('hidden')).toBe('10');
});

// this seems like it might mask programmer error, but it's existing behavior.
itRenders('boolean prop with zero value', async render => {
itRenders('hidden prop with zero value', async render => {
const e = await render(<div hidden={0} />);
expect(e.getAttribute('hidden')).toBe(null);
expect(e.getAttribute('hidden')).toBe('0');
});

itRenders('no boolean prop with null value', async render => {
itRenders('no hidden prop with null value', async render => {
const e = await render(<div hidden={null} />);
expect(e.hasAttribute('hidden')).toBe(false);
});

itRenders('no boolean prop with function value', async render => {
itRenders('no hidden prop with function value', async render => {
const e = await render(<div hidden={function () {}} />, 1);
expect(e.hasAttribute('hidden')).toBe(false);
});

itRenders('no boolean prop with symbol value', async render => {
itRenders('no hidden prop with symbol value', async render => {
const e = await render(<div hidden={Symbol('foo')} />, 1);
expect(e.hasAttribute('hidden')).toBe(false);
});
Expand Down