React DOM: Support boolean values for `inert` prop (#24730)
Sebastian Silbermann committed
Mar 13, 2024 at 23:10 UTC
bbc571aee431d44799ae6a70832ea834325a5af9
14 files changed
+207
-7
fixtures/attribute-behavior/AttributeTableSnapshot.md
+7
-7
@@ -5427,20 +5427,20 @@
5427
| Test Case | Flags | Result |
5428
| --- | --- | --- |
5429
| `inert=(string)`| (changed)| `<boolean: true>` |
5430
-| `inert=(empty string)`| (changed)| `<boolean: true>` |
5430
+| `inert=(empty string)`| (initial, warning)| `<boolean: false>` |
5431
| `inert=(array with string)`| (changed)| `<boolean: true>` |
5432
| `inert=(empty array)`| (changed)| `<boolean: true>` |
5433
| `inert=(object)`| (changed)| `<boolean: true>` |
5434
| `inert=(numeric string)`| (changed)| `<boolean: true>` |
5435
| `inert=(-1)`| (changed)| `<boolean: true>` |
5436
-| `inert=(0)`| (changed)| `<boolean: true>` |
5436
+| `inert=(0)`| (initial)| `<boolean: false>` |
5437
| `inert=(integer)`| (changed)| `<boolean: true>` |
5438
-| `inert=(NaN)`| (changed, warning)| `<boolean: true>` |
5438
+| `inert=(NaN)`| (initial, warning)| `<boolean: false>` |
5439
| `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>` |
5444
| `inert=(string 'on')`| (changed)| `<boolean: true>` |
5445
| `inert=(string 'off')`| (changed)| `<boolean: true>` |
5446
| `inert=(symbol)`| (initial, warning)| `<boolean: false>` |
packages/react-dom-bindings/src/client/ReactDOMComponent.js
+49
@@ -73,6 +73,7 @@ import {
73
disableIEWorkarounds,
74
enableTrustedTypesIntegration,
75
enableFilterEmptyStringAttributesDOM,
76
+ enableNewBooleanProps,
77
} from 'shared/ReactFeatureFlags';
78
import {
79
mediaEventTypes,
@@ -86,8 +87,10 @@ let didWarnFormActionType = false;
87
let didWarnFormActionName = false;
88
let didWarnFormActionTarget = false;
89
let didWarnFormActionMethod = false;
90
+let didWarnForNewBooleanPropsWithEmptyValue: {[string]: boolean};
91
let canDiffStyleForHydrationWarning;
92
if (__DEV__) {
93
+ didWarnForNewBooleanPropsWithEmptyValue = {};
94
// IE 11 parses & normalizes the style attribute as opposed to other
95
// browsers. It adds spaces and sorts the properties in some
96
// non-alphabetical order. Handling that would require sorting CSS
@@ -712,6 +715,25 @@ function setProp(
715
break;
716
}
717
// 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
737
case 'allowFullScreen':
738
case 'async':
739
case 'autoPlay':
@@ -2663,6 +2685,33 @@ function diffHydratedGenericElement(
2685
extraAttributes,
2686
);
2687
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
2715
default: {
2716
if (
2717
// shouldIgnoreAttribute
packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
+32
@@ -34,6 +34,7 @@ import {
34
enableFloat,
35
enableFormActions,
36
enableFizzExternalRuntime,
37
+ enableNewBooleanProps,
38
} from 'shared/ReactFeatureFlags';
39
40
import type {
@@ -345,6 +346,11 @@ const importMapScriptEnd = stringToPrecomputedChunk('</script>');
346
// allow one more header to be captured which means in practice if the limit is approached it will be exceeded
347
const DEFAULT_HEADERS_CAPACITY_IN_UTF16_CODE_UNITS = 2000;
348
349
+let didWarnForNewBooleanPropsWithEmptyValue: {[string]: boolean};
350
+if (__DEV__) {
351
+ didWarnForNewBooleanPropsWithEmptyValue = {};
352
+}
353
+
354
// Allows us to keep track of what we've already written so we can refer back to it.
355
// if passed externalRuntimeConfig and the enableFizzExternalRuntime feature flag
356
// is set, the server will send instructions via data attributes (instead of inline scripts)
@@ -1398,6 +1404,32 @@ function pushAttribute(
1404
case 'xmlSpace':
1405
pushStringAttribute(target, 'xml:space', value);
1406
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
1433
default:
1434
if (
1435
// shouldIgnoreAttribute
packages/react-dom-bindings/src/shared/ReactDOMUnknownPropertyHook.js
+15
@@ -12,6 +12,7 @@ import hasOwnProperty from 'shared/hasOwnProperty';
12
import {
13
enableCustomElementPropertySupport,
14
enableFormActions,
15
+ enableNewBooleanProps,
16
} from 'shared/ReactFeatureFlags';
17
18
const warnedProperties = {};
@@ -240,6 +241,14 @@ function validateProperty(tagName, name, value, eventRegistry) {
241
// Boolean properties can accept boolean values
242
return true;
243
}
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
252
default: {
253
const prefix = name.toLowerCase().slice(0, 5);
254
if (prefix === 'data-' || prefix === 'aria-') {
@@ -314,6 +323,12 @@ function validateProperty(tagName, name, value, eventRegistry) {
323
case 'itemScope': {
324
break;
325
}
326
+ case 'inert': {
327
+ if (enableNewBooleanProps) {
328
+ break;
329
+ }
330
+ }
331
+ // fallthrough for new boolean props without the flag on
332
default: {
333
return true;
334
}
packages/react-dom-bindings/src/shared/possibleStandardNames.js
+5
@@ -4,6 +4,7 @@
4
* This source code is licensed under the MIT license found in the
5
* LICENSE file in the root directory of this source tree.
6
*/
7
+import {enableNewBooleanProps} from 'shared/ReactFeatureFlags';
8
9
// When adding attributes to the HTML or SVG allowed attribute list, be sure to
10
// also add them to this module to ensure casing and incorrect name
@@ -502,4 +503,8 @@ const possibleStandardNames = {
503
zoomandpan: 'zoomAndPan',
504
};
505
506
+if (enableNewBooleanProps) {
507
+ possibleStandardNames.inert = 'inert';
508
+}
509
+
510
export default possibleStandardNames;
packages/react-dom/src/__tests__/ReactDOMAttribute-test.js
+50
@@ -12,12 +12,14 @@
12
describe('ReactDOM unknown attribute', () => {
13
let React;
14
let ReactDOMClient;
15
+ let ReactFeatureFlags;
16
let act;
17
18
beforeEach(() => {
19
jest.resetModules();
20
React = require('react');
21
ReactDOMClient = require('react-dom/client');
22
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
23
act = require('internal-test-utils').act;
24
});
25
@@ -88,6 +90,54 @@ describe('ReactDOM unknown attribute', () => {
90
expect(el.firstChild.hasAttribute('unknown')).toBe(false);
91
});
92
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
+
141
it('passes through strings', async () => {
142
await testUnknownAttributeAssignment('a string', 'a string');
143
});
packages/react-dom/src/__tests__/ReactDOMServerIntegrationAttributes-test.js
+35
@@ -754,6 +754,41 @@ describe('ReactDOMServerIntegration', () => {
754
}
755
});
756
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
+
792
itRenders(
793
'no unknown attributes for custom elements with null value',
794
async render => {
packages/shared/ReactFeatureFlags.js
+7
@@ -198,6 +198,13 @@ export const enableReactTestRendererWarning = false;
198
// before removing them in stable in the next Major
199
export const disableLegacyMode = __NEXT_MAJOR__;
200
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
+
208
// -----------------------------------------------------------------------------
209
// Chopping Block
210
//
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -80,6 +80,7 @@ export const enableLegacyHidden = false;
80
export const forceConcurrentByDefaultForTesting = false;
81
export const allowConcurrentByDefault = false;
82
export const enableCustomElementPropertySupport = true;
83
+export const enableNewBooleanProps = true;
84
85
export const enableTransitionTracing = false;
86
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -63,6 +63,7 @@ export const forceConcurrentByDefaultForTesting = false;
63
export const enableUnifiedSyncLane = true;
64
export const allowConcurrentByDefault = false;
65
export const enableCustomElementPropertySupport = true;
66
+export const enableNewBooleanProps = true;
67
68
export const consoleManagedByDevToolsDuringStrictMode = false;
69
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -99,6 +99,7 @@ export const enableReactTestRendererWarning = false;
99
export const enableBigIntSupport = __NEXT_MAJOR__;
100
export const disableLegacyMode = __NEXT_MAJOR__;
101
export const disableLegacyContext = __NEXT_MAJOR__;
102
+export const enableNewBooleanProps = __NEXT_MAJOR__;
103
104
// Flow magic to verify the exports of this file match the original version.
105
((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+1
@@ -63,6 +63,7 @@ export const forceConcurrentByDefaultForTesting = false;
63
export const enableUnifiedSyncLane = true;
64
export const allowConcurrentByDefault = true;
65
export const enableCustomElementPropertySupport = true;
66
+export const enableNewBooleanProps = true;
67
68
export const consoleManagedByDevToolsDuringStrictMode = false;
69
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -63,6 +63,7 @@ export const forceConcurrentByDefaultForTesting = false;
63
export const enableUnifiedSyncLane = true;
64
export const allowConcurrentByDefault = true;
65
export const enableCustomElementPropertySupport = false;
66
+export const enableNewBooleanProps = false;
67
68
export const consoleManagedByDevToolsDuringStrictMode = false;
69
packages/shared/forks/ReactFeatureFlags.www.js
+2
@@ -102,6 +102,8 @@ export const allowConcurrentByDefault = true;
102
103
export const consoleManagedByDevToolsDuringStrictMode = true;
104
105
+export const enableNewBooleanProps = false;
106
+
107
export const enableFizzExternalRuntime = true;
108
109
export const forceConcurrentByDefaultForTesting = false;