@samitouri / QOS-React-2 / commits / bbc571aee4

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;