Skip to content

Commit 82c64e1

Browse files
authored
Match Preact behavior for boolean props on custom elements (#24541)
* Log unexpected warnings when testing with ReactDOMServerIntegrationTestUtils * Add test Following #9230 (comment) except that `foo={true}` renders an empty string. See #9230 (comment) for rationale. * Match Preact behavior for boolean props on custom elements * Poke CircleCI
1 parent 6e2f38f commit 82c64e1

8 files changed

+79
-11
lines changed

packages/react-dom/src/__tests__/DOMPropertyOperations-test.js

+25-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
'use strict';
1111

1212
// Set by `yarn test-fire`.
13-
const {disableInputAttributeSyncing} = require('shared/ReactFeatureFlags');
13+
const {
14+
enableCustomElementPropertySupport,
15+
disableInputAttributeSyncing,
16+
} = require('shared/ReactFeatureFlags');
1417

1518
describe('DOMPropertyOperations', () => {
1619
let React;
@@ -256,8 +259,12 @@ describe('DOMPropertyOperations', () => {
256259
expect(customElement.getAttribute('onstring')).toBe('hello');
257260
expect(customElement.getAttribute('onobj')).toBe('[object Object]');
258261
expect(customElement.getAttribute('onarray')).toBe('one,two');
259-
expect(customElement.getAttribute('ontrue')).toBe('true');
260-
expect(customElement.getAttribute('onfalse')).toBe('false');
262+
expect(customElement.getAttribute('ontrue')).toBe(
263+
enableCustomElementPropertySupport ? '' : 'true',
264+
);
265+
expect(customElement.getAttribute('onfalse')).toBe(
266+
enableCustomElementPropertySupport ? null : 'false',
267+
);
261268

262269
// Dispatch the corresponding event names to make sure that nothing crashes.
263270
customElement.dispatchEvent(new Event('string'));
@@ -959,6 +966,21 @@ describe('DOMPropertyOperations', () => {
959966
expect(customElement.foo).toBe(null);
960967
});
961968

969+
// @gate enableCustomElementPropertySupport
970+
it('boolean props should not be stringified in attributes', () => {
971+
const container = document.createElement('div');
972+
document.body.appendChild(container);
973+
ReactDOM.render(<my-custom-element foo={true} />, container);
974+
const customElement = container.querySelector('my-custom-element');
975+
976+
expect(customElement.getAttribute('foo')).toBe('');
977+
978+
// true => false
979+
ReactDOM.render(<my-custom-element foo={false} />, container);
980+
981+
expect(customElement.getAttribute('foo')).toBe(null);
982+
});
983+
962984
// @gate enableCustomElementPropertySupport
963985
it('custom element custom event handlers assign multiple types', () => {
964986
const container = document.createElement('div');

packages/react-dom/src/__tests__/ReactDOMComponent-test.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -2689,9 +2689,13 @@ describe('ReactDOMComponent', () => {
26892689
const container = document.createElement('div');
26902690
ReactDOM.render(<some-custom-element foo={true} />, container);
26912691
const node = container.firstChild;
2692-
expect(node.getAttribute('foo')).toBe('true');
2692+
expect(node.getAttribute('foo')).toBe(
2693+
ReactFeatureFlags.enableCustomElementPropertySupport ? '' : 'true',
2694+
);
26932695
ReactDOM.render(<some-custom-element foo={false} />, container);
2694-
expect(node.getAttribute('foo')).toBe('false');
2696+
expect(node.getAttribute('foo')).toBe(
2697+
ReactFeatureFlags.enableCustomElementPropertySupport ? null : 'false',
2698+
);
26952699
ReactDOM.render(<some-custom-element />, container);
26962700
expect(node.hasAttribute('foo')).toBe(false);
26972701
ReactDOM.render(<some-custom-element foo={true} />, container);

packages/react-dom/src/__tests__/ReactDOMServerIntegrationAttributes-test.js

+10-2
Original file line numberDiff line numberDiff line change
@@ -696,12 +696,20 @@ describe('ReactDOMServerIntegration', () => {
696696

697697
itRenders('unknown boolean `true` attributes as strings', async render => {
698698
const e = await render(<custom-element foo={true} />);
699-
expect(e.getAttribute('foo')).toBe('true');
699+
if (ReactFeatureFlags.enableCustomElementPropertySupport) {
700+
expect(e.getAttribute('foo')).toBe('');
701+
} else {
702+
expect(e.getAttribute('foo')).toBe('true');
703+
}
700704
});
701705

702706
itRenders('unknown boolean `false` attributes as strings', async render => {
703707
const e = await render(<custom-element foo={false} />);
704-
expect(e.getAttribute('foo')).toBe('false');
708+
if (ReactFeatureFlags.enableCustomElementPropertySupport) {
709+
expect(e.getAttribute('foo')).toBe(null);
710+
} else {
711+
expect(e.getAttribute('foo')).toBe('false');
712+
}
705713
});
706714

707715
itRenders(

packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ module.exports = function(initModules) {
8989
console.log(
9090
`We expected ${count} warning(s), but saw ${filteredWarnings.length} warning(s).`,
9191
);
92-
if (filteredWarnings.count > 0) {
92+
if (filteredWarnings.length > 0) {
9393
console.log(`We saw these warnings:`);
9494
for (let i = 0; i < filteredWarnings.length; i++) {
9595
console.log(...filteredWarnings[i]);

packages/react-dom/src/client/DOMPropertyOperations.js

+13
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ export function getValueForAttribute(
115115
node: Element,
116116
name: string,
117117
expected: mixed,
118+
isCustomComponentTag: boolean,
118119
): mixed {
119120
if (__DEV__) {
120121
if (!isAttributeNameSafe(name)) {
@@ -124,6 +125,13 @@ export function getValueForAttribute(
124125
return expected === undefined ? undefined : null;
125126
}
126127
const value = node.getAttribute(name);
128+
129+
if (enableCustomElementPropertySupport) {
130+
if (isCustomComponentTag && value === '') {
131+
return true;
132+
}
133+
}
134+
127135
if (__DEV__) {
128136
checkAttributeStringCoercion(expected, name);
129137
}
@@ -196,6 +204,11 @@ export function setValueForProperty(
196204
if (shouldRemoveAttribute(name, value, propertyInfo, isCustomComponentTag)) {
197205
value = null;
198206
}
207+
if (enableCustomElementPropertySupport) {
208+
if (isCustomComponentTag && value === true) {
209+
value = '';
210+
}
211+
}
199212

200213
// If the prop isn't in the special list, treat it as a simple attribute.
201214
if (isCustomComponentTag || propertyInfo === null) {

packages/react-dom/src/client/ReactDOMComponent.js

+12-2
Original file line numberDiff line numberDiff line change
@@ -1081,7 +1081,12 @@ export function diffHydratedProperties(
10811081
} else if (isCustomComponentTag && !enableCustomElementPropertySupport) {
10821082
// $FlowFixMe - Should be inferred as not undefined.
10831083
extraAttributeNames.delete(propKey.toLowerCase());
1084-
serverValue = getValueForAttribute(domElement, propKey, nextProp);
1084+
serverValue = getValueForAttribute(
1085+
domElement,
1086+
propKey,
1087+
nextProp,
1088+
isCustomComponentTag,
1089+
);
10851090

10861091
if (nextProp !== serverValue) {
10871092
warnForPropDifference(propKey, serverValue, nextProp);
@@ -1128,7 +1133,12 @@ export function diffHydratedProperties(
11281133
// $FlowFixMe - Should be inferred as not undefined.
11291134
extraAttributeNames.delete(propKey);
11301135
}
1131-
serverValue = getValueForAttribute(domElement, propKey, nextProp);
1136+
serverValue = getValueForAttribute(
1137+
domElement,
1138+
propKey,
1139+
nextProp,
1140+
isCustomComponentTag,
1141+
);
11321142
}
11331143

11341144
const dontWarnCustomElement =

packages/react-dom/src/server/ReactDOMServerFormatConfig.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -1156,7 +1156,7 @@ function pushStartCustomElement(
11561156
let innerHTML = null;
11571157
for (let propKey in props) {
11581158
if (hasOwnProperty.call(props, propKey)) {
1159-
const propValue = props[propKey];
1159+
let propValue = props[propKey];
11601160
if (propValue == null) {
11611161
continue;
11621162
}
@@ -1169,6 +1169,12 @@ function pushStartCustomElement(
11691169
// so skip it.
11701170
continue;
11711171
}
1172+
if (enableCustomElementPropertySupport && propValue === false) {
1173+
continue;
1174+
}
1175+
if (enableCustomElementPropertySupport && propValue === true) {
1176+
propValue = '';
1177+
}
11721178
if (enableCustomElementPropertySupport && propKey === 'className') {
11731179
// className gets rendered as class on the client, so it should be
11741180
// rendered as class on the server.

packages/react-dom/src/shared/DOMProperty.js

+5
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,11 @@ export function shouldRemoveAttribute(
162162
return true;
163163
}
164164
if (isCustomComponentTag) {
165+
if (enableCustomElementPropertySupport) {
166+
if (value === false) {
167+
return true;
168+
}
169+
}
165170
return false;
166171
}
167172
if (propertyInfo !== null) {

0 commit comments

Comments
 (0)