Skip to content

Commit bbc571a

Browse files
authored
React DOM: Support boolean values for inert prop (#24730)
1 parent bb0944f commit bbc571a

14 files changed

+207
-7
lines changed

fixtures/attribute-behavior/AttributeTableSnapshot.md

+7-7
Original file line numberDiff line numberDiff line change
@@ -5427,20 +5427,20 @@
54275427
| Test Case | Flags | Result |
54285428
| --- | --- | --- |
54295429
| `inert=(string)`| (changed)| `<boolean: true>` |
5430-
| `inert=(empty string)`| (changed)| `<boolean: true>` |
5430+
| `inert=(empty string)`| (initial, warning)| `<boolean: false>` |
54315431
| `inert=(array with string)`| (changed)| `<boolean: true>` |
54325432
| `inert=(empty array)`| (changed)| `<boolean: true>` |
54335433
| `inert=(object)`| (changed)| `<boolean: true>` |
54345434
| `inert=(numeric string)`| (changed)| `<boolean: true>` |
54355435
| `inert=(-1)`| (changed)| `<boolean: true>` |
5436-
| `inert=(0)`| (changed)| `<boolean: true>` |
5436+
| `inert=(0)`| (initial)| `<boolean: false>` |
54375437
| `inert=(integer)`| (changed)| `<boolean: true>` |
5438-
| `inert=(NaN)`| (changed, warning)| `<boolean: true>` |
5438+
| `inert=(NaN)`| (initial, warning)| `<boolean: false>` |
54395439
| `inert=(float)`| (changed)| `<boolean: true>` |
5440-
| `inert=(true)`| (initial, warning)| `<boolean: false>` |
5441-
| `inert=(false)`| (initial, warning)| `<boolean: false>` |
5442-
| `inert=(string 'true')`| (changed)| `<boolean: true>` |
5443-
| `inert=(string 'false')`| (changed)| `<boolean: true>` |
5440+
| `inert=(true)`| (changed)| `<boolean: true>` |
5441+
| `inert=(false)`| (initial)| `<boolean: false>` |
5442+
| `inert=(string 'true')`| (changed, warning)| `<boolean: true>` |
5443+
| `inert=(string 'false')`| (changed, warning)| `<boolean: true>` |
54445444
| `inert=(string 'on')`| (changed)| `<boolean: true>` |
54455445
| `inert=(string 'off')`| (changed)| `<boolean: true>` |
54465446
| `inert=(symbol)`| (initial, warning)| `<boolean: false>` |

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

+49
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ import {
7373
disableIEWorkarounds,
7474
enableTrustedTypesIntegration,
7575
enableFilterEmptyStringAttributesDOM,
76+
enableNewBooleanProps,
7677
} from 'shared/ReactFeatureFlags';
7778
import {
7879
mediaEventTypes,
@@ -86,8 +87,10 @@ let didWarnFormActionType = false;
8687
let didWarnFormActionName = false;
8788
let didWarnFormActionTarget = false;
8889
let didWarnFormActionMethod = false;
90+
let didWarnForNewBooleanPropsWithEmptyValue: {[string]: boolean};
8991
let canDiffStyleForHydrationWarning;
9092
if (__DEV__) {
93+
didWarnForNewBooleanPropsWithEmptyValue = {};
9194
// IE 11 parses & normalizes the style attribute as opposed to other
9295
// browsers. It adds spaces and sorts the properties in some
9396
// non-alphabetical order. Handling that would require sorting CSS
@@ -712,6 +715,25 @@ function setProp(
712715
break;
713716
}
714717
// Boolean
718+
case 'inert':
719+
if (!enableNewBooleanProps) {
720+
setValueForAttribute(domElement, key, value);
721+
break;
722+
} else {
723+
if (__DEV__) {
724+
if (value === '' && !didWarnForNewBooleanPropsWithEmptyValue[key]) {
725+
didWarnForNewBooleanPropsWithEmptyValue[key] = true;
726+
console.error(
727+
'Received an empty string for a boolean attribute `%s`. ' +
728+
'This will treat the attribute as if it were false. ' +
729+
'Either pass `false` to silence this warning, or ' +
730+
'pass `true` if you used an empty string in earlier versions of React to indicate this attribute is true.',
731+
key,
732+
);
733+
}
734+
}
735+
}
736+
// fallthrough for new boolean props without the flag on
715737
case 'allowFullScreen':
716738
case 'async':
717739
case 'autoPlay':
@@ -2663,6 +2685,33 @@ function diffHydratedGenericElement(
26632685
extraAttributes,
26642686
);
26652687
continue;
2688+
case 'inert':
2689+
if (enableNewBooleanProps) {
2690+
if (__DEV__) {
2691+
if (
2692+
value === '' &&
2693+
!didWarnForNewBooleanPropsWithEmptyValue[propKey]
2694+
) {
2695+
didWarnForNewBooleanPropsWithEmptyValue[propKey] = true;
2696+
console.error(
2697+
'Received an empty string for a boolean attribute `%s`. ' +
2698+
'This will treat the attribute as if it were false. ' +
2699+
'Either pass `false` to silence this warning, or ' +
2700+
'pass `true` if you used an empty string in earlier versions of React to indicate this attribute is true.',
2701+
propKey,
2702+
);
2703+
}
2704+
}
2705+
hydrateBooleanAttribute(
2706+
domElement,
2707+
propKey,
2708+
propKey,
2709+
value,
2710+
extraAttributes,
2711+
);
2712+
continue;
2713+
}
2714+
// fallthrough for new boolean props without the flag on
26662715
default: {
26672716
if (
26682717
// shouldIgnoreAttribute

packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js

+32
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
enableFloat,
3535
enableFormActions,
3636
enableFizzExternalRuntime,
37+
enableNewBooleanProps,
3738
} from 'shared/ReactFeatureFlags';
3839

3940
import type {
@@ -345,6 +346,11 @@ const importMapScriptEnd = stringToPrecomputedChunk('</script>');
345346
// allow one more header to be captured which means in practice if the limit is approached it will be exceeded
346347
const DEFAULT_HEADERS_CAPACITY_IN_UTF16_CODE_UNITS = 2000;
347348

349+
let didWarnForNewBooleanPropsWithEmptyValue: {[string]: boolean};
350+
if (__DEV__) {
351+
didWarnForNewBooleanPropsWithEmptyValue = {};
352+
}
353+
348354
// Allows us to keep track of what we've already written so we can refer back to it.
349355
// if passed externalRuntimeConfig and the enableFizzExternalRuntime feature flag
350356
// is set, the server will send instructions via data attributes (instead of inline scripts)
@@ -1398,6 +1404,32 @@ function pushAttribute(
13981404
case 'xmlSpace':
13991405
pushStringAttribute(target, 'xml:space', value);
14001406
return;
1407+
case 'inert': {
1408+
if (enableNewBooleanProps) {
1409+
if (__DEV__) {
1410+
if (value === '' && !didWarnForNewBooleanPropsWithEmptyValue[name]) {
1411+
didWarnForNewBooleanPropsWithEmptyValue[name] = true;
1412+
console.error(
1413+
'Received an empty string for a boolean attribute `%s`. ' +
1414+
'This will treat the attribute as if it were false. ' +
1415+
'Either pass `false` to silence this warning, or ' +
1416+
'pass `true` if you used an empty string in earlier versions of React to indicate this attribute is true.',
1417+
name,
1418+
);
1419+
}
1420+
}
1421+
// Boolean
1422+
if (value && typeof value !== 'function' && typeof value !== 'symbol') {
1423+
target.push(
1424+
attributeSeparator,
1425+
stringToChunk(name),
1426+
attributeEmptyString,
1427+
);
1428+
}
1429+
return;
1430+
}
1431+
}
1432+
// fallthrough for new boolean props without the flag on
14011433
default:
14021434
if (
14031435
// shouldIgnoreAttribute

packages/react-dom-bindings/src/shared/ReactDOMUnknownPropertyHook.js

+15
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import hasOwnProperty from 'shared/hasOwnProperty';
1212
import {
1313
enableCustomElementPropertySupport,
1414
enableFormActions,
15+
enableNewBooleanProps,
1516
} from 'shared/ReactFeatureFlags';
1617

1718
const warnedProperties = {};
@@ -240,6 +241,14 @@ function validateProperty(tagName, name, value, eventRegistry) {
240241
// Boolean properties can accept boolean values
241242
return true;
242243
}
244+
// fallthrough
245+
case 'inert': {
246+
if (enableNewBooleanProps) {
247+
// Boolean properties can accept boolean values
248+
return true;
249+
}
250+
}
251+
// fallthrough for new boolean props without the flag on
243252
default: {
244253
const prefix = name.toLowerCase().slice(0, 5);
245254
if (prefix === 'data-' || prefix === 'aria-') {
@@ -314,6 +323,12 @@ function validateProperty(tagName, name, value, eventRegistry) {
314323
case 'itemScope': {
315324
break;
316325
}
326+
case 'inert': {
327+
if (enableNewBooleanProps) {
328+
break;
329+
}
330+
}
331+
// fallthrough for new boolean props without the flag on
317332
default: {
318333
return true;
319334
}

packages/react-dom-bindings/src/shared/possibleStandardNames.js

+5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*/
7+
import {enableNewBooleanProps} from 'shared/ReactFeatureFlags';
78

89
// When adding attributes to the HTML or SVG allowed attribute list, be sure to
910
// also add them to this module to ensure casing and incorrect name
@@ -502,4 +503,8 @@ const possibleStandardNames = {
502503
zoomandpan: 'zoomAndPan',
503504
};
504505

506+
if (enableNewBooleanProps) {
507+
possibleStandardNames.inert = 'inert';
508+
}
509+
505510
export default possibleStandardNames;

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

+50
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212
describe('ReactDOM unknown attribute', () => {
1313
let React;
1414
let ReactDOMClient;
15+
let ReactFeatureFlags;
1516
let act;
1617

1718
beforeEach(() => {
1819
jest.resetModules();
1920
React = require('react');
2021
ReactDOMClient = require('react-dom/client');
22+
ReactFeatureFlags = require('shared/ReactFeatureFlags');
2123
act = require('internal-test-utils').act;
2224
});
2325

@@ -88,6 +90,54 @@ describe('ReactDOM unknown attribute', () => {
8890
expect(el.firstChild.hasAttribute('unknown')).toBe(false);
8991
});
9092

93+
it('removes new boolean props', async () => {
94+
const el = document.createElement('div');
95+
const root = ReactDOMClient.createRoot(el);
96+
97+
await expect(async () => {
98+
await act(() => {
99+
root.render(<div inert={true} />);
100+
});
101+
}).toErrorDev(
102+
ReactFeatureFlags.enableNewBooleanProps
103+
? []
104+
: ['Warning: Received `true` for a non-boolean attribute `inert`.'],
105+
);
106+
107+
expect(el.firstChild.getAttribute('inert')).toBe(
108+
ReactFeatureFlags.enableNewBooleanProps ? '' : null,
109+
);
110+
});
111+
112+
it('warns once for empty strings in new boolean props', async () => {
113+
const el = document.createElement('div');
114+
const root = ReactDOMClient.createRoot(el);
115+
116+
await expect(async () => {
117+
await act(() => {
118+
root.render(<div inert="" />);
119+
});
120+
}).toErrorDev(
121+
ReactFeatureFlags.enableNewBooleanProps
122+
? [
123+
'Warning: Received an empty string for a boolean attribute `inert`. ' +
124+
'This will treat the attribute as if it were false. ' +
125+
'Either pass `false` to silence this warning, or ' +
126+
'pass `true` if you used an empty string in earlier versions of React to indicate this attribute is true.',
127+
]
128+
: [],
129+
);
130+
131+
expect(el.firstChild.getAttribute('inert')).toBe(
132+
ReactFeatureFlags.enableNewBooleanProps ? null : '',
133+
);
134+
135+
// The warning is only printed once.
136+
await act(() => {
137+
root.render(<div inert="" />);
138+
});
139+
});
140+
91141
it('passes through strings', async () => {
92142
await testUnknownAttributeAssignment('a string', 'a string');
93143
});

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

+35
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,41 @@ describe('ReactDOMServerIntegration', () => {
754754
}
755755
});
756756

757+
itRenders('new boolean `true` attributes', async render => {
758+
const element = await render(
759+
<div inert={true} />,
760+
ReactFeatureFlags.enableNewBooleanProps ? 0 : 1,
761+
);
762+
763+
expect(element.getAttribute('inert')).toBe(
764+
ReactFeatureFlags.enableNewBooleanProps ? '' : null,
765+
);
766+
});
767+
768+
itRenders('new boolean `""` attributes', async render => {
769+
const element = await render(
770+
<div inert="" />,
771+
ReactFeatureFlags.enableNewBooleanProps
772+
? // Warns since this used to render `inert=""` like `inert={true}`
773+
// but now renders it like `inert={false}`.
774+
1
775+
: 0,
776+
);
777+
778+
expect(element.getAttribute('inert')).toBe(
779+
ReactFeatureFlags.enableNewBooleanProps ? null : '',
780+
);
781+
});
782+
783+
itRenders('new boolean `false` attributes', async render => {
784+
const element = await render(
785+
<div inert={false} />,
786+
ReactFeatureFlags.enableNewBooleanProps ? 0 : 1,
787+
);
788+
789+
expect(element.getAttribute('inert')).toBe(null);
790+
});
791+
757792
itRenders(
758793
'no unknown attributes for custom elements with null value',
759794
async render => {

packages/shared/ReactFeatureFlags.js

+7
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,13 @@ export const enableReactTestRendererWarning = false;
198198
// before removing them in stable in the next Major
199199
export const disableLegacyMode = __NEXT_MAJOR__;
200200

201+
// HTML boolean attributes need a special PropertyInfoRecord.
202+
// Between support of these attributes in browsers and React supporting them as
203+
// boolean props library users can use them as `<div someBooleanAttribute="" />`.
204+
// However, once React considers them as boolean props an empty string will
205+
// result in false property i.e. break existing usage.
206+
export const enableNewBooleanProps = __NEXT_MAJOR__;
207+
201208
// -----------------------------------------------------------------------------
202209
// Chopping Block
203210
//

packages/shared/forks/ReactFeatureFlags.native-fb.js

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export const enableLegacyHidden = false;
8080
export const forceConcurrentByDefaultForTesting = false;
8181
export const allowConcurrentByDefault = false;
8282
export const enableCustomElementPropertySupport = true;
83+
export const enableNewBooleanProps = true;
8384

8485
export const enableTransitionTracing = false;
8586

packages/shared/forks/ReactFeatureFlags.native-oss.js

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export const forceConcurrentByDefaultForTesting = false;
6363
export const enableUnifiedSyncLane = true;
6464
export const allowConcurrentByDefault = false;
6565
export const enableCustomElementPropertySupport = true;
66+
export const enableNewBooleanProps = true;
6667

6768
export const consoleManagedByDevToolsDuringStrictMode = false;
6869

packages/shared/forks/ReactFeatureFlags.test-renderer.js

+1
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ export const enableReactTestRendererWarning = false;
9999
export const enableBigIntSupport = __NEXT_MAJOR__;
100100
export const disableLegacyMode = __NEXT_MAJOR__;
101101
export const disableLegacyContext = __NEXT_MAJOR__;
102+
export const enableNewBooleanProps = __NEXT_MAJOR__;
102103

103104
// Flow magic to verify the exports of this file match the original version.
104105
((((null: any): ExportsType): FeatureFlagsType): ExportsType);

packages/shared/forks/ReactFeatureFlags.test-renderer.native.js

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export const forceConcurrentByDefaultForTesting = false;
6363
export const enableUnifiedSyncLane = true;
6464
export const allowConcurrentByDefault = true;
6565
export const enableCustomElementPropertySupport = true;
66+
export const enableNewBooleanProps = true;
6667

6768
export const consoleManagedByDevToolsDuringStrictMode = false;
6869

packages/shared/forks/ReactFeatureFlags.test-renderer.www.js

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export const forceConcurrentByDefaultForTesting = false;
6363
export const enableUnifiedSyncLane = true;
6464
export const allowConcurrentByDefault = true;
6565
export const enableCustomElementPropertySupport = false;
66+
export const enableNewBooleanProps = false;
6667

6768
export const consoleManagedByDevToolsDuringStrictMode = false;
6869

packages/shared/forks/ReactFeatureFlags.www.js

+2
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@ export const allowConcurrentByDefault = true;
102102

103103
export const consoleManagedByDevToolsDuringStrictMode = true;
104104

105+
export const enableNewBooleanProps = false;
106+
105107
export const enableFizzExternalRuntime = true;
106108

107109
export const forceConcurrentByDefaultForTesting = false;

0 commit comments

Comments
 (0)