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

Refactor DOM Bindings Completely Off of DOMProperty Meta Programming (#26546)

There are four places we have special cases based off the DOMProperty config: 1) DEV-only: ReactDOMUnknownPropertyHook warns for passing booleans to non-boolean attributes. We just need a simple list of all properties that are affected by that. We could probably move this in under setProp instead and have it covered by that list. 2) DEV-only: Hydration. This just needs to read the value from an attribute and compare it to what we'd expect to see if it was rendered on the client. This could use some simplification/unification of the code but I decided to just keep it simple and duplicated since code size isn't an issue. 3) DOMServerFormatConfig pushAttribute: This just maps the special case to how to emit it as a HTML attribute. 4) ReactDOMComponent setProp: This just maps the special case to how to emit it as setAttribute or removeAttribute. Basically we just have to remember to keep pushAttribute and setProp aligned. There's only one long switch in prod per environment. This just turns it all to a giant simple switch statement with string cases. This is in theory the most optimizable since syntactically all the information for a hash table is there. However, unfortunately we know that most VMs don't optimize this very well and instead just turn them into a bunch of ifs. JSC is best. We can minimize the cost by just moving common attribute to the beginning of the list. If we shipped this, maybe VMs will get it together to start optimizing this case but there's a chicken and egg problem here and the game theory reality is that we probably don't want to regress. Therefore, I intend to do a follow up after landing this which reintroduces an object indirection for simple property aliases. That should be enough to make the remaining cases palatable. I'll also extract the most common attributes to the beginning or separate ifs. Ran attribute-behavior fixture and the table is the same.

Sebastian Markbåge committed Apr 4, 2023 at 11:05 UTC eeabb7312f509eb2094452a4389646000ea8ea14
7 files changed +2696 -1099
packages/react-dom-bindings/src/client/CSSPropertyOperations.js
+15
@@ -72,6 +72,21 @@ export function createDangerousStringForStyles(styles) {
72 * @param {object} styles
73 */
74 export function setValueForStyles(node, styles) {
75 + if (styles != null && typeof styles !== 'object') {
76 + throw new Error(
77 + 'The `style` prop expects a mapping from style properties to values, ' +
78 + "not a string. For example, style={{marginRight: spacing + 'em'}} when " +
79 + 'using JSX.',
80 + );
81 + }
82 + if (__DEV__) {
83 + if (styles) {
84 + // Freeze the next style object so that we can assume it won't be
85 + // mutated. We have already warned for this in the past.
86 + Object.freeze(styles);
87 + }
88 + }
89 +
90 const style = node.style;
91 for (const styleName in styles) {
92 if (!styles.hasOwnProperty(styleName)) {
packages/react-dom-bindings/src/client/DOMPropertyOperations.js
+29 -299
@@ -7,181 +7,14 @@
7 * @flow
8 */
9
10 -import {
11 - BOOLEAN,
12 - OVERLOADED_BOOLEAN,
13 - NUMERIC,
14 - POSITIVE_NUMERIC,
15 -} from '../shared/DOMProperty';
16 -
10 import isAttributeNameSafe from '../shared/isAttributeNameSafe';
18 -import sanitizeURL from '../shared/sanitizeURL';
11 import {
12 enableTrustedTypesIntegration,
13 enableCustomElementPropertySupport,
22 - enableFilterEmptyStringAttributesDOM,
14 } from 'shared/ReactFeatureFlags';
15 import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
16 import {getFiberCurrentPropsFromNode} from './ReactDOMComponentTree';
17
27 -import type {PropertyInfo} from '../shared/DOMProperty';
28 -
29 -/**
30 - * Get the value for a property on a node. Only used in DEV for SSR validation.
31 - * The "expected" argument is used as a hint of what the expected value is.
32 - * Some properties have multiple equivalent values.
33 - */
34 -export function getValueForProperty(
35 - node: Element,
36 - name: string,
37 - expected: mixed,
38 - propertyInfo: PropertyInfo,
39 -): mixed {
40 - if (__DEV__) {
41 - const attributeName = propertyInfo.attributeName;
42 -
43 - if (!node.hasAttribute(attributeName)) {
44 - // shouldRemoveAttribute
45 - switch (typeof expected) {
46 - case 'function':
47 - case 'symbol': // eslint-disable-line
48 - return expected;
49 - case 'boolean': {
50 - if (!propertyInfo.acceptsBooleans) {
51 - return expected;
52 - }
53 - }
54 - }
55 - switch (propertyInfo.type) {
56 - case BOOLEAN: {
57 - if (!expected) {
58 - return expected;
59 - }
60 - break;
61 - }
62 - case OVERLOADED_BOOLEAN: {
63 - if (expected === false) {
64 - return expected;
65 - }
66 - break;
67 - }
68 - case NUMERIC: {
69 - if (isNaN(expected)) {
70 - return expected;
71 - }
72 - break;
73 - }
74 - case POSITIVE_NUMERIC: {
75 - if (isNaN(expected) || (expected: any) < 1) {
76 - return expected;
77 - }
78 - break;
79 - }
80 - }
81 - if (enableFilterEmptyStringAttributesDOM) {
82 - if (propertyInfo.removeEmptyString && expected === '') {
83 - if (__DEV__) {
84 - if (name === 'src') {
85 - console.error(
86 - 'An empty string ("") was passed to the %s attribute. ' +
87 - 'This may cause the browser to download the whole page again over the network. ' +
88 - 'To fix this, either do not render the element at all ' +
89 - 'or pass null to %s instead of an empty string.',
90 - name,
91 - name,
92 - );
93 - } else {
94 - console.error(
95 - 'An empty string ("") was passed to the %s attribute. ' +
96 - 'To fix this, either do not render the element at all ' +
97 - 'or pass null to %s instead of an empty string.',
98 - name,
99 - name,
100 - );
101 - }
102 - }
103 - return expected;
104 - }
105 - }
106 - return expected === undefined ? undefined : null;
107 - }
108 -
109 - // Even if this property uses a namespace we use getAttribute
110 - // because we assume its namespaced name is the same as our config.
111 - // To use getAttributeNS we need the local name which we don't have
112 - // in our config atm.
113 - const value = node.getAttribute(attributeName);
114 -
115 - if (expected == null) {
116 - // We had an attribute but shouldn't have had one, so read it
117 - // for the error message.
118 - return value;
119 - }
120 -
121 - // shouldRemoveAttribute
122 - switch (typeof expected) {
123 - case 'function':
124 - case 'symbol': // eslint-disable-line
125 - return value;
126 - }
127 - switch (propertyInfo.type) {
128 - case BOOLEAN: {
129 - if (expected) {
130 - // If this was a boolean, it doesn't matter what the value is
131 - // the fact that we have it is the same as the expected.
132 - // As long as it's positive.
133 - return expected;
134 - }
135 - return value;
136 - }
137 - case OVERLOADED_BOOLEAN: {
138 - if (value === '') {
139 - return true;
140 - }
141 - if (expected === false) {
142 - // We had an attribute but shouldn't have had one, so read it
143 - // for the error message.
144 - return value;
145 - }
146 - break;
147 - }
148 - case NUMERIC: {
149 - if (isNaN(expected)) {
150 - // We had an attribute but shouldn't have had one, so read it
151 - // for the error message.
152 - return value;
153 - }
154 - break;
155 - }
156 - case POSITIVE_NUMERIC: {
157 - if (isNaN(expected) || (expected: any) < 1) {
158 - // We had an attribute but shouldn't have had one, so read it
159 - // for the error message.
160 - return value;
161 - }
162 - break;
163 - }
164 - }
165 - if (__DEV__) {
166 - checkAttributeStringCoercion(expected, name);
167 - }
168 - if (propertyInfo.sanitizeURL) {
169 - // We have already verified this above.
170 - // eslint-disable-next-line react-internal/safe-string-coercion
171 - if (value === '' + (sanitizeURL(expected): any)) {
172 - return expected;
173 - }
174 - return value;
175 - }
176 - // We have already verified this above.
177 - // eslint-disable-next-line react-internal/safe-string-coercion
178 - if (value === '' + (expected: any)) {
179 - return expected;
180 - }
181 - return value;
182 - }
183 -}
184 -
18 /**
19 * Get the value for a attribute on a node. Only used in DEV for SSR validation.
20 * The third argument is used as a hint of what the expected value is. Some
@@ -271,138 +104,6 @@ export function getValueForAttributeOnCustomComponent(
104 }
105 }
106
274 -/**
275 - * Sets the value for a property on a node.
276 - *
277 - * @param {DOMElement} node
278 - * @param {string} name
279 - * @param {*} value
280 - */
281 -export function setValueForProperty(
282 - node: Element,
283 - propertyInfo: PropertyInfo,
284 - value: mixed,
285 -) {
286 - const attributeName = propertyInfo.attributeName;
287 -
288 - if (value === null) {
289 - node.removeAttribute(attributeName);
290 - return;
291 - }
292 -
293 - // shouldRemoveAttribute
294 - switch (typeof value) {
295 - case 'undefined':
296 - case 'function':
297 - case 'symbol': // eslint-disable-line
298 - node.removeAttribute(attributeName);
299 - return;
300 - case 'boolean': {
301 - if (!propertyInfo.acceptsBooleans) {
302 - node.removeAttribute(attributeName);
303 - return;
304 - }
305 - }
306 - }
307 - if (enableFilterEmptyStringAttributesDOM) {
308 - if (propertyInfo.removeEmptyString && value === '') {
309 - if (__DEV__) {
310 - if (attributeName === 'src') {
311 - console.error(
312 - 'An empty string ("") was passed to the %s attribute. ' +
313 - 'This may cause the browser to download the whole page again over the network. ' +
314 - 'To fix this, either do not render the element at all ' +
315 - 'or pass null to %s instead of an empty string.',
316 - attributeName,
317 - attributeName,
318 - );
319 - } else {
320 - console.error(
321 - 'An empty string ("") was passed to the %s attribute. ' +
322 - 'To fix this, either do not render the element at all ' +
323 - 'or pass null to %s instead of an empty string.',
324 - attributeName,
325 - attributeName,
326 - );
327 - }
328 - }
329 - node.removeAttribute(attributeName);
330 - return;
331 - }
332 - }
333 -
334 - switch (propertyInfo.type) {
335 - case BOOLEAN:
336 - if (value) {
337 - node.setAttribute(attributeName, '');
338 - } else {
339 - node.removeAttribute(attributeName);
340 - return;
341 - }
342 - break;
343 - case OVERLOADED_BOOLEAN:
344 - if (value === true) {
345 - node.setAttribute(attributeName, '');
346 - } else if (value === false) {
347 - node.removeAttribute(attributeName);
348 - } else {
349 - if (__DEV__) {
350 - checkAttributeStringCoercion(value, attributeName);
351 - }
352 - node.setAttribute(attributeName, (value: any));
353 - }
354 - return;
355 - case NUMERIC:
356 - if (!isNaN(value)) {
357 - if (__DEV__) {
358 - checkAttributeStringCoercion(value, attributeName);
359 - }
360 - node.setAttribute(attributeName, (value: any));
361 - } else {
362 - node.removeAttribute(attributeName);
363 - }
364 - break;
365 - case POSITIVE_NUMERIC:
366 - if (!isNaN(value) && (value: any) >= 1) {
367 - if (__DEV__) {
368 - checkAttributeStringCoercion(value, attributeName);
369 - }
370 - node.setAttribute(attributeName, (value: any));
371 - } else {
372 - node.removeAttribute(attributeName);
373 - }
374 - break;
375 - default: {
376 - if (__DEV__) {
377 - checkAttributeStringCoercion(value, attributeName);
378 - }
379 - let attributeValue;
380 - // `setAttribute` with objects becomes only `[object]` in IE8/9,
381 - // ('' + value) makes it output the correct toString()-value.
382 - if (enableTrustedTypesIntegration) {
383 - if (propertyInfo.sanitizeURL) {
384 - attributeValue = (sanitizeURL(value): any);
385 - } else {
386 - attributeValue = (value: any);
387 - }
388 - } else {
389 - // We have already verified this above.
390 - // eslint-disable-next-line react-internal/safe-string-coercion
391 - attributeValue = '' + (value: any);
392 - if (propertyInfo.sanitizeURL) {
393 - attributeValue = sanitizeURL(attributeValue);
394 - }
395 - }
396 - const attributeNamespace = propertyInfo.attributeNamespace;
397 - if (attributeNamespace) {
398 - node.setAttributeNS(attributeNamespace, attributeName, attributeValue);
399 - } else {
400 - node.setAttribute(attributeName, attributeValue);
401 - }
402 - }
403 - }
404 -}
405 -
107 export function setValueForAttribute(
108 node: Element,
109 name: string,
@@ -439,6 +140,35 @@ export function setValueForAttribute(
140 }
141 }
142
143 +export function setValueForNamespacedAttribute(
144 + node: Element,
145 + namespace: string,
146 + name: string,
147 + value: mixed,
148 +) {
149 + if (value === null) {
150 + node.removeAttribute(name);
151 + return;
152 + }
153 + switch (typeof value) {
154 + case 'undefined':
155 + case 'function':
156 + case 'symbol':
157 + case 'boolean': {
158 + node.removeAttribute(name);
159 + return;
160 + }
161 + }
162 + if (__DEV__) {
163 + checkAttributeStringCoercion(value, name);
164 + }
165 + node.setAttributeNS(
166 + namespace,
167 + name,
168 + enableTrustedTypesIntegration ? (value: any) : '' + (value: any),
169 + );
170 +}
171 +
172 export function setValueForPropertyOnCustomComponent(
173 node: Element,
174 name: string,
packages/react-dom-bindings/src/client/ReactDOMComponent.js
+2090 -240
@@ -16,14 +16,14 @@ import {
16
17 import {canUseDOM} from 'shared/ExecutionEnvironment';
18 import {checkHtmlStringCoercion} from 'shared/CheckStringCoercion';
19 +import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
20
21 import {
22 getValueForAttribute,
23 getValueForAttributeOnCustomComponent,
23 - getValueForProperty,
24 - setValueForProperty,
24 setValueForPropertyOnCustomComponent,
25 setValueForAttribute,
26 + setValueForNamespacedAttribute,
27 } from './DOMPropertyOperations';
28 import {
29 initWrapperState as ReactDOMInputInitWrapperState,
@@ -57,18 +57,20 @@ import {
57 validateShorthandPropertyCollisionInDev,
58 } from './CSSPropertyOperations';
59 import {HTML_NAMESPACE, getIntrinsicNamespace} from './DOMNamespaces';
60 -import {getPropertyInfo} from '../shared/DOMProperty';
60 import isCustomElement from '../shared/isCustomElement';
61 import possibleStandardNames from '../shared/possibleStandardNames';
62 import {validateProperties as validateARIAProperties} from '../shared/ReactDOMInvalidARIAHook';
63 import {validateProperties as validateInputProperties} from '../shared/ReactDOMNullInputValuePropHook';
64 import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook';
65 +import sanitizeURL from '../shared/sanitizeURL';
66
67 import {
68 enableCustomElementPropertySupport,
69 enableClientRenderFallbackOnTextMismatch,
70 enableHostSingletons,
71 disableIEWorkarounds,
72 + enableTrustedTypesIntegration,
73 + enableFilterEmptyStringAttributesDOM,
74 } from 'shared/ReactFeatureFlags';
75 import {
76 mediaEventTypes,
@@ -122,6 +124,9 @@ function warnForPropDifference(
124 if (didWarnInvalidHydration) {
125 return;
126 }
127 + if (serverValue === clientValue) {
128 + return;
129 + }
130 const normalizedClientValue =
131 normalizeMarkupForTextOrAttribute(clientValue);
132 const normalizedServerValue =
@@ -259,31 +264,18 @@ export function trapClickOnNonInteractiveElement(node: HTMLElement) {
264 node.onclick = noop;
265 }
266
267 +const xlinkNamespace = 'http://www.w3.org/1999/xlink';
268 +const xmlNamespace = 'http://www.w3.org/XML/1998/namespace';
269 +
270 function setProp(
271 domElement: Element,
272 tag: string,
273 key: string,
274 value: mixed,
267 - isCustomElementTag: boolean,
275 props: any,
276 ): void {
277 switch (key) {
278 case 'style': {
272 - if (value != null && typeof value !== 'object') {
273 - throw new Error(
274 - 'The `style` prop expects a mapping from style properties to values, ' +
275 - "not a string. For example, style={{marginRight: spacing + 'em'}} when " +
276 - 'using JSX.',
277 - );
278 - }
279 - if (__DEV__) {
280 - if (value) {
281 - // Freeze the next style object so that we can assume it won't be
282 - // mutated. We have already warned for this in the past.
283 - Object.freeze(value);
284 - }
285 - }
286 - // Relies on `updateStylesByID` not mutating `styleUpdates`.
279 setValueForStyles(domElement, value);
280 break;
281 }
@@ -378,6 +370,616 @@ function setProp(
370 // on server rendering (but we *do* want to emit it in SSR).
371 break;
372 }
373 + // These attributes accept URLs. These must not allow javascript: URLS.
374 + case 'src':
375 + case 'href':
376 + case 'action':
377 + if (enableFilterEmptyStringAttributesDOM) {
378 + if (value === '') {
379 + if (__DEV__) {
380 + if (key === 'src') {
381 + console.error(
382 + 'An empty string ("") was passed to the %s attribute. ' +
383 + 'This may cause the browser to download the whole page again over the network. ' +
384 + 'To fix this, either do not render the element at all ' +
385 + 'or pass null to %s instead of an empty string.',
386 + key,
387 + key,
388 + );
389 + } else {
390 + console.error(
391 + 'An empty string ("") was passed to the %s attribute. ' +
392 + 'To fix this, either do not render the element at all ' +
393 + 'or pass null to %s instead of an empty string.',
394 + key,
395 + key,
396 + );
397 + }
398 + }
399 + domElement.removeAttribute(key);
400 + break;
401 + }
402 + }
403 + // Fall through to the last case which shouldn't remove empty strings.
404 + // eslint-disable-next-line no-fallthrough
405 + case 'formAction': {
406 + if (
407 + value == null ||
408 + typeof value === 'function' ||
409 + typeof value === 'symbol' ||
410 + typeof value === 'boolean'
411 + ) {
412 + domElement.removeAttribute(key);
413 + break;
414 + }
415 + // `setAttribute` with objects becomes only `[object]` in IE8/9,
416 + // ('' + value) makes it output the correct toString()-value.
417 + if (__DEV__) {
418 + checkAttributeStringCoercion(value, key);
419 + }
420 + const sanitizedValue = (sanitizeURL(
421 + enableTrustedTypesIntegration ? value : '' + (value: any),
422 + ): any);
423 + domElement.setAttribute(key, sanitizedValue);
424 + break;
425 + }
426 + case 'xlinkHref': {
427 + if (
428 + value == null ||
429 + typeof value === 'function' ||
430 + typeof value === 'boolean' ||
431 + typeof value === 'symbol'
432 + ) {
433 + domElement.removeAttribute('xlink:href');
434 + break;
435 + }
436 + // `setAttribute` with objects becomes only `[object]` in IE8/9,
437 + // ('' + value) makes it output the correct toString()-value.
438 + if (__DEV__) {
439 + checkAttributeStringCoercion(value, key);
440 + }
441 + const sanitizedValue = (sanitizeURL(
442 + enableTrustedTypesIntegration ? value : '' + (value: any),
443 + ): any);
444 + domElement.setAttributeNS(xlinkNamespace, 'xlink:href', sanitizedValue);
445 + break;
446 + }
447 + case 'contentEditable':
448 + case 'spellCheck':
449 + case 'draggable':
450 + case 'value':
451 + case 'autoReverse':
452 + case 'externalResourcesRequired':
453 + case 'focusable':
454 + case 'preserveAlpha': {
455 + // Booleanish String
456 + // These are "enumerated" attributes that accept "true" and "false".
457 + // In React, we let users pass `true` and `false` even though technically
458 + // these aren't boolean attributes (they are coerced to strings).
459 + // The SVG attributes are case-sensitive. Since the HTML attributes are
460 + // insensitive they also work even though we canonically use lower case.
461 + if (
462 + value != null &&
463 + typeof value !== 'function' &&
464 + typeof value !== 'symbol'
465 + ) {
466 + if (__DEV__) {
467 + checkAttributeStringCoercion(value, key);
468 + }
469 + domElement.setAttribute(key, (value: any));
470 + } else {
471 + domElement.removeAttribute(key);
472 + }
473 + break;
474 + }
475 + // Boolean
476 + case 'allowFullScreen':
477 + case 'async':
478 + case 'autoPlay':
479 + case 'controls':
480 + case 'default':
481 + case 'defer':
482 + case 'disabled':
483 + case 'disablePictureInPicture':
484 + case 'disableRemotePlayback':
485 + case 'formNoValidate':
486 + case 'hidden':
487 + case 'loop':
488 + case 'noModule':
489 + case 'noValidate':
490 + case 'open':
491 + case 'playsInline':
492 + case 'readOnly':
493 + case 'required':
494 + case 'reversed':
495 + case 'scoped':
496 + case 'seamless':
497 + case 'itemScope': {
498 + if (value && typeof value !== 'function' && typeof value !== 'symbol') {
499 + domElement.setAttribute(key, '');
500 + } else {
501 + domElement.removeAttribute(key);
502 + }
503 + break;
504 + }
505 + // Overloaded Boolean
506 + case 'capture':
507 + case 'download': {
508 + // An attribute that can be used as a flag as well as with a value.
509 + // When true, it should be present (set either to an empty string or its name).
510 + // When false, it should be omitted.
511 + // For any other value, should be present with that value.
512 + if (value === true) {
513 + domElement.setAttribute(key, '');
514 + } else if (
515 + value !== false &&
516 + value != null &&
517 + typeof value !== 'function' &&
518 + typeof value !== 'symbol'
519 + ) {
520 + if (__DEV__) {
521 + checkAttributeStringCoercion(value, key);
522 + }
523 + domElement.setAttribute(key, (value: any));
524 + } else {
525 + domElement.removeAttribute(key);
526 + }
527 + break;
528 + }
529 + case 'cols':
530 + case 'rows':
531 + case 'size':
532 + case 'span': {
533 + // These are HTML attributes that must be positive numbers.
534 + if (
535 + value != null &&
536 + typeof value !== 'function' &&
537 + typeof value !== 'symbol' &&
538 + !isNaN(value) &&
539 + (value: any) >= 1
540 + ) {
541 + if (__DEV__) {
542 + checkAttributeStringCoercion(value, key);
543 + }
544 + domElement.setAttribute(key, (value: any));
545 + } else {
546 + domElement.removeAttribute(key);
547 + }
548 + break;
549 + }
550 + case 'rowSpan':
551 + case 'start': {
552 + // These are HTML attributes that must be numbers.
553 + if (
554 + value != null &&
555 + typeof value !== 'function' &&
556 + typeof value !== 'symbol' &&
557 + !isNaN(value)
558 + ) {
559 + if (__DEV__) {
560 + checkAttributeStringCoercion(value, key);
561 + }
562 + domElement.setAttribute(key, (value: any));
563 + } else {
564 + domElement.removeAttribute(key);
565 + }
566 + break;
567 + }
568 + // A few React string attributes have a different name.
569 + // This is a mapping from React prop names to the attribute names.
570 + case 'acceptCharset':
571 + setValueForAttribute(domElement, 'accept-charset', value);
572 + break;
573 + case 'className':
574 + setValueForAttribute(domElement, 'class', value);
575 + break;
576 + case 'htmlFor':
577 + setValueForAttribute(domElement, 'for', value);
578 + break;
579 + case 'httpEquiv':
580 + setValueForAttribute(domElement, 'http-equiv', value);
581 + break;
582 + // HTML and SVG attributes, but the SVG attribute is case sensitive.
583 + case 'tabIndex':
584 + setValueForAttribute(domElement, 'tabindex', value);
585 + break;
586 + case 'crossOrigin':
587 + setValueForAttribute(domElement, 'crossorigin', value);
588 + break;
589 + // This is a list of all SVG attributes that need special casing.
590 + // Regular attributes that just accept strings.
591 + case 'accentHeight':
592 + setValueForAttribute(domElement, 'accent-height', value);
593 + break;
594 + case 'alignmentBaseline':
595 + setValueForAttribute(domElement, 'alignment-baseline', value);
596 + break;
597 + case 'arabicForm':
598 + setValueForAttribute(domElement, 'arabic-form', value);
599 + break;
600 + case 'baselineShift':
601 + setValueForAttribute(domElement, 'baseline-shift', value);
602 + break;
603 + case 'capHeight':
604 + setValueForAttribute(domElement, 'cap-height', value);
605 + break;
606 + case 'clipPath':
607 + setValueForAttribute(domElement, 'clip-path', value);
608 + break;
609 + case 'clipRule':
610 + setValueForAttribute(domElement, 'clip-rule', value);
611 + break;
612 + case 'colorInterpolation':
613 + setValueForAttribute(domElement, 'color-interpolation', value);
614 + break;
615 + case 'colorInterpolationFilters':
616 + setValueForAttribute(domElement, 'color-interpolation-filters', value);
617 + break;
618 + case 'colorProfile':
619 + setValueForAttribute(domElement, 'color-profile', value);
620 + break;
621 + case 'colorRendering':
622 + setValueForAttribute(domElement, 'color-rendering', value);
623 + break;
624 + case 'dominantBaseline':
625 + setValueForAttribute(domElement, 'dominant-baseline', value);
626 + break;
627 + case 'enableBackground':
628 + setValueForAttribute(domElement, 'enable-background', value);
629 + break;
630 + case 'fillOpacity':
631 + setValueForAttribute(domElement, 'fill-opacity', value);
632 + break;
633 + case 'fillRule':
634 + setValueForAttribute(domElement, 'fill-rule', value);
635 + break;
636 + case 'floodColor':
637 + setValueForAttribute(domElement, 'flood-color', value);
638 + break;
639 + case 'floodOpacity':
640 + setValueForAttribute(domElement, 'flood-opacity', value);
641 + break;
642 + case 'fontFamily':
643 + setValueForAttribute(domElement, 'font-family', value);
644 + break;
645 + case 'fontSize':
646 + setValueForAttribute(domElement, 'font-size', value);
647 + break;
648 + case 'fontSizeAdjust':
649 + setValueForAttribute(domElement, 'font-size-adjust', value);
650 + break;
651 + case 'fontStretch':
652 + setValueForAttribute(domElement, 'font-stretch', value);
653 + break;
654 + case 'fontStyle':
655 + setValueForAttribute(domElement, 'font-style', value);
656 + break;
657 + case 'fontVariant':
658 + setValueForAttribute(domElement, 'font-variant', value);
659 + break;
660 + case 'fontWeight':
661 + setValueForAttribute(domElement, 'font-weight', value);
662 + break;
663 + case 'glyphName':
664 + setValueForAttribute(domElement, 'glyph-name', value);
665 + break;
666 + case 'glyphOrientationHorizontal':
667 + setValueForAttribute(domElement, 'glyph-orientation-horizontal', value);
668 + break;
669 + case 'glyphOrientationVertical':
670 + setValueForAttribute(domElement, 'glyph-orientation-vertical', value);
671 + break;
672 + case 'horizAdvX':
673 + setValueForAttribute(domElement, 'horiz-adv-x', value);
674 + break;
675 + case 'horizOriginX':
676 + setValueForAttribute(domElement, 'horiz-origin-x', value);
677 + break;
678 + case 'imageRendering':
679 + setValueForAttribute(domElement, 'image-rendering', value);
680 + break;
681 + case 'letterSpacing':
682 + setValueForAttribute(domElement, 'letter-spacing', value);
683 + break;
684 + case 'lightingColor':
685 + setValueForAttribute(domElement, 'lighting-color', value);
686 + break;
687 + case 'markerEnd':
688 + setValueForAttribute(domElement, 'marker-end', value);
689 + break;
690 + case 'markerMid':
691 + setValueForAttribute(domElement, 'marker-mid', value);
692 + break;
693 + case 'markerStart':
694 + setValueForAttribute(domElement, 'marker-start', value);
695 + break;
696 + case 'overlinePosition':
697 + setValueForAttribute(domElement, 'overline-position', value);
698 + break;
699 + case 'overlineThickness':
700 + setValueForAttribute(domElement, 'overline-thickness', value);
701 + break;
702 + case 'paintOrder':
703 + setValueForAttribute(domElement, 'paint-order', value);
704 + break;
705 + case 'panose-1':
706 + setValueForAttribute(domElement, 'panose-1', value);
707 + break;
708 + case 'pointerEvents':
709 + setValueForAttribute(domElement, 'pointer-events', value);
710 + break;
711 + case 'renderingIntent':
712 + setValueForAttribute(domElement, 'rendering-intent', value);
713 + break;
714 + case 'shapeRendering':
715 + setValueForAttribute(domElement, 'shape-rendering', value);
716 + break;
717 + case 'stopColor':
718 + setValueForAttribute(domElement, 'stop-color', value);
719 + break;
720 + case 'stopOpacity':
721 + setValueForAttribute(domElement, 'stop-opacity', value);
722 + break;
723 + case 'strikethroughPosition':
724 + setValueForAttribute(domElement, 'strikethrough-position', value);
725 + break;
726 + case 'strikethroughThickness':
727 + setValueForAttribute(domElement, 'strikethrough-thickness', value);
728 + break;
729 + case 'strokeDasharray':
730 + setValueForAttribute(domElement, 'stroke-dasharray', value);
731 + break;
732 + case 'strokeDashoffset':
733 + setValueForAttribute(domElement, 'stroke-dashoffset', value);
734 + break;
735 + case 'strokeLinecap':
736 + setValueForAttribute(domElement, 'stroke-linecap', value);
737 + break;
738 + case 'strokeLinejoin':
739 + setValueForAttribute(domElement, 'stroke-linejoin', value);
740 + break;
741 + case 'strokeMiterlimit':
742 + setValueForAttribute(domElement, 'stroke-miterlimit', value);
743 + break;
744 + case 'strokeOpacity':
745 + setValueForAttribute(domElement, 'stroke-opacity', value);
746 + break;
747 + case 'strokeWidth':
748 + setValueForAttribute(domElement, 'stroke-width', value);
749 + break;
750 + case 'textAnchor':
751 + setValueForAttribute(domElement, 'text-anchor', value);
752 + break;
753 + case 'textDecoration':
754 + setValueForAttribute(domElement, 'text-decoration', value);
755 + break;
756 + case 'textRendering':
757 + setValueForAttribute(domElement, 'text-rendering', value);
758 + break;
759 + case 'transformOrigin':
760 + setValueForAttribute(domElement, 'transform-origin', value);
761 + break;
762 + case 'underlinePosition':
763 + setValueForAttribute(domElement, 'underline-position', value);
764 + break;
765 + case 'underlineThickness':
766 + setValueForAttribute(domElement, 'underline-thickness', value);
767 + break;
768 + case 'unicodeBidi':
769 + setValueForAttribute(domElement, 'unicode-bidi', value);
770 + break;
771 + case 'unicodeRange':
772 + setValueForAttribute(domElement, 'unicode-range', value);
773 + break;
774 + case 'unitsPerEm':
775 + setValueForAttribute(domElement, 'units-per-em', value);
776 + break;
777 + case 'vAlphabetic':
778 + setValueForAttribute(domElement, 'v-alphabetic', value);
779 + break;
780 + case 'vHanging':
781 + setValueForAttribute(domElement, 'v-hanging', value);
782 + break;
783 + case 'vIdeographic':
784 + setValueForAttribute(domElement, 'v-ideographic', value);
785 + break;
786 + case 'vMathematical':
787 + setValueForAttribute(domElement, 'v-mathematical', value);
788 + break;
789 + case 'vectorEffect':
790 + setValueForAttribute(domElement, 'vector-effect', value);
791 + break;
792 + case 'vertAdvY':
793 + setValueForAttribute(domElement, 'vert-adv-y', value);
794 + break;
795 + case 'vertOriginX':
796 + setValueForAttribute(domElement, 'vert-origin-x', value);
797 + break;
798 + case 'vertOriginY':
799 + setValueForAttribute(domElement, 'vert-origin-y', value);
800 + break;
801 + case 'wordSpacing':
802 + setValueForAttribute(domElement, 'word-spacing', value);
803 + break;
804 + case 'writingMode':
805 + setValueForAttribute(domElement, 'writing-mode', value);
806 + break;
807 + case 'xmlnsXlink':
808 + setValueForAttribute(domElement, 'xmlns:xlink', value);
809 + break;
810 + case 'xHeight':
811 + setValueForAttribute(domElement, 'x-height', value);
812 + break;
813 + case 'xlinkActuate':
814 + setValueForNamespacedAttribute(
815 + domElement,
816 + xlinkNamespace,
817 + 'xlink:actuate',
818 + value,
819 + );
820 + break;
821 + case 'xlinkArcrole':
822 + setValueForNamespacedAttribute(
823 + domElement,
824 + xlinkNamespace,
825 + 'xlink:arcrole',
826 + value,
827 + );
828 + break;
829 + case 'xlinkRole':
830 + setValueForNamespacedAttribute(
831 + domElement,
832 + xlinkNamespace,
833 + 'xlink:role',
834 + value,
835 + );
836 + break;
837 + case 'xlinkShow':
838 + setValueForNamespacedAttribute(
839 + domElement,
840 + xlinkNamespace,
841 + 'xlink:show',
842 + value,
843 + );
844 + break;
845 + case 'xlinkTitle':
846 + setValueForNamespacedAttribute(
847 + domElement,
848 + xlinkNamespace,
849 + 'xlink:title',
850 + value,
851 + );
852 + break;
853 + case 'xlinkType':
854 + setValueForNamespacedAttribute(
855 + domElement,
856 + xlinkNamespace,
857 + 'xlink:type',
858 + value,
859 + );
860 + break;
861 + case 'xmlBase':
862 + setValueForNamespacedAttribute(
863 + domElement,
864 + xmlNamespace,
865 + 'xml:base',
866 + value,
867 + );
868 + break;
869 + case 'xmlLang':
870 + setValueForNamespacedAttribute(
871 + domElement,
872 + xmlNamespace,
873 + 'xml:lang',
874 + value,
875 + );
876 + break;
877 + case 'xmlSpace':
878 + setValueForNamespacedAttribute(
879 + domElement,
880 + xmlNamespace,
881 + 'xml:space',
882 + value,
883 + );
884 + break;
885 + // Properties that should not be allowed on custom elements.
886 + case 'innerText':
887 + case 'textContent':
888 + if (enableCustomElementPropertySupport) {
889 + break;
890 + }
891 + // eslint-disable-next-line no-fallthrough
892 + default: {
893 + if (
894 + key.length > 2 &&
895 + (key[0] === 'o' || key[0] === 'O') &&
896 + (key[1] === 'n' || key[1] === 'N')
897 + ) {
898 + if (
899 + __DEV__ &&
900 + registrationNameDependencies.hasOwnProperty(key) &&
901 + value != null &&
902 + typeof value !== 'function'
903 + ) {
904 + warnForInvalidEventListener(key, value);
905 + }
906 + } else {
907 + setValueForAttribute(domElement, key, value);
908 + }
909 + }
910 + }
911 +}
912 +
913 +function setPropOnCustomElement(
914 + domElement: Element,
915 + tag: string,
916 + key: string,
917 + value: mixed,
918 + props: any,
919 +): void {
920 + switch (key) {
921 + case 'style': {
922 + setValueForStyles(domElement, value);
923 + break;
924 + }
925 + case 'dangerouslySetInnerHTML': {
926 + if (value != null) {
927 + if (typeof value !== 'object' || !('__html' in value)) {
928 + throw new Error(
929 + '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +
930 + 'Please visit https://reactjs.org/link/dangerously-set-inner-html ' +
931 + 'for more information.',
932 + );
933 + }
934 + const nextHtml: any = value.__html;
935 + if (nextHtml != null) {
936 + if (props.children != null) {
937 + throw new Error(
938 + 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.',
939 + );
940 + }
941 + if (disableIEWorkarounds) {
942 + domElement.innerHTML = nextHtml;
943 + } else {
944 + setInnerHTML(domElement, nextHtml);
945 + }
946 + }
947 + }
948 + break;
949 + }
950 + case 'children': {
951 + if (typeof value === 'string') {
952 + setTextContent(domElement, value);
953 + } else if (typeof value === 'number') {
954 + setTextContent(domElement, '' + value);
955 + }
956 + break;
957 + }
958 + case 'onScroll': {
959 + if (value != null) {
960 + if (__DEV__ && typeof value !== 'function') {
961 + warnForInvalidEventListener(key, value);
962 + }
963 + listenToNonDelegatedEvent('scroll', domElement);
964 + }
965 + break;
966 + }
967 + case 'onClick': {
968 + // TODO: This cast may not be sound for SVG, MathML or custom elements.
969 + if (value != null) {
970 + if (__DEV__ && typeof value !== 'function') {
971 + warnForInvalidEventListener(key, value);
972 + }
973 + trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
974 + }
975 + break;
976 + }
977 + case 'suppressContentEditableWarning':
978 + case 'suppressHydrationWarning':
979 + case 'innerHTML': {
980 + // Noop
981 + break;
982 + }
983 case 'innerText': // Properties
984 case 'textContent':
985 if (enableCustomElementPropertySupport) {
@@ -390,33 +992,14 @@ function setProp(
992 warnForInvalidEventListener(key, value);
993 }
994 } else {
393 - if (isCustomElementTag) {
394 - if (enableCustomElementPropertySupport) {
395 - setValueForPropertyOnCustomComponent(domElement, key, value);
396 - } else {
397 - if (typeof value === 'boolean') {
398 - // Special case before the new flag is on
399 - value = '' + (value: any);
400 - }
401 - setValueForAttribute(domElement, key, value);
402 - }
995 + if (enableCustomElementPropertySupport) {
996 + setValueForPropertyOnCustomComponent(domElement, key, value);
997 } else {
404 - if (
405 - // shouldIgnoreAttribute
406 - // We have already filtered out reserved words.
407 - key.length > 2 &&
408 - (key[0] === 'o' || key[0] === 'O') &&
409 - (key[1] === 'n' || key[1] === 'N')
410 - ) {
411 - return;
412 - }
413 -
414 - const propertyInfo = getPropertyInfo(key);
415 - if (propertyInfo !== null) {
416 - setValueForProperty(domElement, propertyInfo, value);
417 - } else {
418 - setValueForAttribute(domElement, key, value);
998 + if (typeof value === 'boolean') {
999 + // Special case before the new flag is on
1000 + value = '' + (value: any);
1001 }
1002 + setValueForAttribute(domElement, key, value);
1003 }
1004 }
1005 }
@@ -475,7 +1058,7 @@ export function setInitialProperties(
1058 }
1059 // defaultChecked and defaultValue are ignored by setProp
1060 default: {
478 - setProp(domElement, tag, propKey, propValue, false, props);
1061 + setProp(domElement, tag, propKey, propValue, props);
1062 }
1063 }
1064 }
@@ -505,7 +1088,7 @@ export function setInitialProperties(
1088 }
1089 // defaultValue are ignored by setProp
1090 default: {
508 - setProp(domElement, tag, propKey, propValue, false, props);
1091 + setProp(domElement, tag, propKey, propValue, props);
1092 }
1093 }
1094 }
@@ -545,7 +1128,7 @@ export function setInitialProperties(
1128 }
1129 // defaultValue is ignored by setProp
1130 default: {
548 - setProp(domElement, tag, propKey, propValue, false, props);
1131 + setProp(domElement, tag, propKey, propValue, props);
1132 }
1133 }
1134 }
@@ -575,7 +1158,7 @@ export function setInitialProperties(
1158 break;
1159 }
1160 default: {
578 - setProp(domElement, tag, propKey, propValue, false, props);
1161 + setProp(domElement, tag, propKey, propValue, props);
1162 }
1163 }
1164 }
@@ -657,7 +1240,7 @@ export function setInitialProperties(
1240 }
1241 // defaultChecked and defaultValue are ignored by setProp
1242 default: {
660 - setProp(domElement, tag, propKey, propValue, false, props);
1243 + setProp(domElement, tag, propKey, propValue, props);
1244 }
1245 }
1246 }
@@ -665,16 +1248,28 @@ export function setInitialProperties(
1248 }
1249 }
1250
668 - const isCustomElementTag = isCustomElement(tag, props);
669 - for (const propKey in props) {
670 - if (!props.hasOwnProperty(propKey)) {
671 - continue;
672 - }
673 - const propValue = props[propKey];
674 - if (propValue == null) {
675 - continue;
1251 + if (isCustomElement(tag, props)) {
1252 + for (const propKey in props) {
1253 + if (!props.hasOwnProperty(propKey)) {
1254 + continue;
1255 + }
1256 + const propValue = props[propKey];
1257 + if (propValue == null) {
1258 + continue;
1259 + }
1260 + setPropOnCustomElement(domElement, tag, propKey, propValue, props);
1261 + }
1262 + } else {
1263 + for (const propKey in props) {
1264 + if (!props.hasOwnProperty(propKey)) {
1265 + continue;
1266 + }
1267 + const propValue = props[propKey];
1268 + if (propValue == null) {
1269 + continue;
1270 + }
1271 + setProp(domElement, tag, propKey, propValue, props);
1272 }
677 - setProp(domElement, tag, propKey, propValue, isCustomElementTag, props);
1273 }
1274 }
1275
@@ -837,7 +1432,7 @@ export function updateProperties(
1432 }
1433 // defaultChecked and defaultValue are ignored by setProp
1434 default: {
840 - setProp(domElement, tag, propKey, propValue, false, nextProps);
1435 + setProp(domElement, tag, propKey, propValue, nextProps);
1436 }
1437 }
1438 }
@@ -858,7 +1453,7 @@ export function updateProperties(
1453 }
1454 // defaultValue are ignored by setProp
1455 default: {
861 - setProp(domElement, tag, propKey, propValue, false, nextProps);
1456 + setProp(domElement, tag, propKey, propValue, nextProps);
1457 }
1458 }
1459 }
@@ -891,7 +1486,7 @@ export function updateProperties(
1486 }
1487 // defaultValue is ignored by setProp
1488 default: {
894 - setProp(domElement, tag, propKey, propValue, false, nextProps);
1489 + setProp(domElement, tag, propKey, propValue, nextProps);
1490 }
1491 }
1492 }
@@ -912,7 +1507,7 @@ export function updateProperties(
1507 break;
1508 }
1509 default: {
915 - setProp(domElement, tag, propKey, propValue, false, nextProps);
1510 + setProp(domElement, tag, propKey, propValue, nextProps);
1511 }
1512 }
1513 }
@@ -951,7 +1546,7 @@ export function updateProperties(
1546 }
1547 // defaultChecked and defaultValue are ignored by setProp
1548 default: {
954 - setProp(domElement, tag, propKey, propValue, false, nextProps);
1549 + setProp(domElement, tag, propKey, propValue, nextProps);
1550 }
1551 }
1552 }
@@ -959,12 +1554,19 @@ export function updateProperties(
1554 }
1555 }
1556
962 - const isCustomElementTag = isCustomElement(tag, nextProps);
1557 // Apply the diff.
964 - for (let i = 0; i < updatePayload.length; i += 2) {
965 - const propKey = updatePayload[i];
966 - const propValue = updatePayload[i + 1];
967 - setProp(domElement, tag, propKey, propValue, isCustomElementTag, nextProps);
1558 + if (isCustomElement(tag, nextProps)) {
1559 + for (let i = 0; i < updatePayload.length; i += 2) {
1560 + const propKey = updatePayload[i];
1561 + const propValue = updatePayload[i + 1];
1562 + setPropOnCustomElement(domElement, tag, propKey, propValue, nextProps);
1563 + }
1564 + } else {
1565 + for (let i = 0; i < updatePayload.length; i += 2) {
1566 + const propKey = updatePayload[i];
1567 + const propValue = updatePayload[i + 1];
1568 + setProp(domElement, tag, propKey, propValue, nextProps);
1569 + }
1570 }
1571 }
1572
@@ -990,10 +1592,314 @@ function diffHydratedStyles(domElement: Element, value: mixed) {
1592 if (canDiffStyleForHydrationWarning) {
1593 const expectedStyle = createDangerousStringForStyles(value);
1594 const serverValue = domElement.getAttribute('style');
993 - if (expectedStyle !== serverValue) {
994 - warnForPropDifference('style', serverValue, expectedStyle);
1595 + warnForPropDifference('style', serverValue, expectedStyle);
1596 + }
1597 +}
1598 +
1599 +function hydrateAttribute(
1600 + domElement: Element,
1601 + propKey: string,
1602 + attributeName: string,
1603 + value: any,
1604 + extraAttributes: Set<string>,
1605 +): void {
1606 + extraAttributes.delete(attributeName);
1607 + const serverValue = domElement.getAttribute(attributeName);
1608 + if (serverValue === null) {
1609 + switch (typeof value) {
1610 + case 'undefined':
1611 + case 'function':
1612 + case 'symbol':
1613 + case 'boolean':
1614 + return;
1615 + }
1616 + } else {
1617 + if (value == null) {
1618 + // We had an attribute but shouldn't have had one, so read it
1619 + // for the error message.
1620 + } else {
1621 + switch (typeof value) {
1622 + case 'function':
1623 + case 'symbol':
1624 + case 'boolean':
1625 + break;
1626 + default: {
1627 + if (__DEV__) {
1628 + checkAttributeStringCoercion(value, propKey);
1629 + }
1630 + if (serverValue === '' + value) {
1631 + return;
1632 + }
1633 + }
1634 + }
1635 + }
1636 + }
1637 + warnForPropDifference(propKey, serverValue, value);
1638 +}
1639 +
1640 +function hydrateBooleanAttribute(
1641 + domElement: Element,
1642 + propKey: string,
1643 + attributeName: string,
1644 + value: any,
1645 + extraAttributes: Set<string>,
1646 +): void {
1647 + extraAttributes.delete(attributeName);
1648 + const serverValue = domElement.getAttribute(attributeName);
1649 + if (serverValue === null) {
1650 + switch (typeof value) {
1651 + case 'function':
1652 + case 'symbol':
1653 + return;
1654 + }
1655 + if (!value) {
1656 + return;
1657 + }
1658 + } else {
1659 + switch (typeof value) {
1660 + case 'function':
1661 + case 'symbol':
1662 + break;
1663 + default: {
1664 + if (value) {
1665 + // If this was a boolean, it doesn't matter what the value is
1666 + // the fact that we have it is the same as the expected.
1667 + // As long as it's positive.
1668 + return;
1669 + }
1670 + }
1671 + }
1672 + }
1673 + warnForPropDifference(propKey, serverValue, value);
1674 +}
1675 +
1676 +function hydrateOverloadedBooleanAttribute(
1677 + domElement: Element,
1678 + propKey: string,
1679 + attributeName: string,
1680 + value: any,
1681 + extraAttributes: Set<string>,
1682 +): void {
1683 + extraAttributes.delete(attributeName);
1684 + const serverValue = domElement.getAttribute(attributeName);
1685 + if (serverValue === null) {
1686 + switch (typeof value) {
1687 + case 'undefined':
1688 + case 'function':
1689 + case 'symbol':
1690 + return;
1691 + default:
1692 + if (value === false) {
1693 + return;
1694 + }
1695 + }
1696 + } else {
1697 + if (value == null) {
1698 + // We had an attribute but shouldn't have had one, so read it
1699 + // for the error message.
1700 + } else {
1701 + switch (typeof value) {
1702 + case 'function':
1703 + case 'symbol':
1704 + break;
1705 + case 'boolean':
1706 + if (value === true && serverValue === '') {
1707 + return;
1708 + }
1709 + break;
1710 + default: {
1711 + if (__DEV__) {
1712 + checkAttributeStringCoercion(value, propKey);
1713 + }
1714 + if (serverValue === '' + value) {
1715 + return;
1716 + }
1717 + }
1718 + }
1719 + }
1720 + }
1721 + warnForPropDifference(propKey, serverValue, value);
1722 +}
1723 +
1724 +function hydrateBooleanishAttribute(
1725 + domElement: Element,
1726 + propKey: string,
1727 + attributeName: string,
1728 + value: any,
1729 + extraAttributes: Set<string>,
1730 +): void {
1731 + extraAttributes.delete(attributeName);
1732 + const serverValue = domElement.getAttribute(attributeName);
1733 + if (serverValue === null) {
1734 + switch (typeof value) {
1735 + case 'undefined':
1736 + case 'function':
1737 + case 'symbol':
1738 + return;
1739 + }
1740 + } else {
1741 + if (value == null) {
1742 + // We had an attribute but shouldn't have had one, so read it
1743 + // for the error message.
1744 + } else {
1745 + switch (typeof value) {
1746 + case 'function':
1747 + case 'symbol':
1748 + break;
1749 + default: {
1750 + if (__DEV__) {
1751 + checkAttributeStringCoercion(value, attributeName);
1752 + }
1753 + if (serverValue === '' + (value: any)) {
1754 + return;
1755 + }
1756 + }
1757 + }
1758 + }
1759 + }
1760 + warnForPropDifference(propKey, serverValue, value);
1761 +}
1762 +
1763 +function hydrateNumericAttribute(
1764 + domElement: Element,
1765 + propKey: string,
1766 + attributeName: string,
1767 + value: any,
1768 + extraAttributes: Set<string>,
1769 +): void {
1770 + extraAttributes.delete(attributeName);
1771 + const serverValue = domElement.getAttribute(attributeName);
1772 + if (serverValue === null) {
1773 + switch (typeof value) {
1774 + case 'undefined':
1775 + case 'function':
1776 + case 'symbol':
1777 + case 'boolean':
1778 + return;
1779 + default:
1780 + if (isNaN(value)) {
1781 + return;
1782 + }
1783 + }
1784 + } else {
1785 + if (value == null) {
1786 + // We had an attribute but shouldn't have had one, so read it
1787 + // for the error message.
1788 + } else {
1789 + switch (typeof value) {
1790 + case 'function':
1791 + case 'symbol':
1792 + case 'boolean':
1793 + break;
1794 + default: {
1795 + if (isNaN(value)) {
1796 + // We had an attribute but shouldn't have had one, so read it
1797 + // for the error message.
1798 + break;
1799 + }
1800 + if (__DEV__) {
1801 + checkAttributeStringCoercion(value, propKey);
1802 + }
1803 + if (serverValue === '' + value) {
1804 + return;
1805 + }
1806 + }
1807 + }
1808 + }
1809 + }
1810 + warnForPropDifference(propKey, serverValue, value);
1811 +}
1812 +
1813 +function hydratePositiveNumericAttribute(
1814 + domElement: Element,
1815 + propKey: string,
1816 + attributeName: string,
1817 + value: any,
1818 + extraAttributes: Set<string>,
1819 +): void {
1820 + extraAttributes.delete(attributeName);
1821 + const serverValue = domElement.getAttribute(attributeName);
1822 + if (serverValue === null) {
1823 + switch (typeof value) {
1824 + case 'undefined':
1825 + case 'function':
1826 + case 'symbol':
1827 + case 'boolean':
1828 + return;
1829 + default:
1830 + if (isNaN(value) || value < 1) {
1831 + return;
1832 + }
1833 + }
1834 + } else {
1835 + if (value == null) {
1836 + // We had an attribute but shouldn't have had one, so read it
1837 + // for the error message.
1838 + } else {
1839 + switch (typeof value) {
1840 + case 'function':
1841 + case 'symbol':
1842 + case 'boolean':
1843 + break;
1844 + default: {
1845 + if (isNaN(value) || value < 1) {
1846 + // We had an attribute but shouldn't have had one, so read it
1847 + // for the error message.
1848 + break;
1849 + }
1850 + if (__DEV__) {
1851 + checkAttributeStringCoercion(value, propKey);
1852 + }
1853 + if (serverValue === '' + value) {
1854 + return;
1855 + }
1856 + }
1857 + }
1858 + }
1859 + }
1860 + warnForPropDifference(propKey, serverValue, value);
1861 +}
1862 +
1863 +function hydrateSanitizedAttribute(
1864 + domElement: Element,
1865 + propKey: string,
1866 + attributeName: string,
1867 + value: any,
1868 + extraAttributes: Set<string>,
1869 +): void {
1870 + extraAttributes.delete(attributeName);
1871 + const serverValue = domElement.getAttribute(attributeName);
1872 + if (serverValue === null) {
1873 + switch (typeof value) {
1874 + case 'undefined':
1875 + case 'function':
1876 + case 'symbol':
1877 + case 'boolean':
1878 + return;
1879 + }
1880 + } else {
1881 + if (value == null) {
1882 + // We had an attribute but shouldn't have had one, so read it
1883 + // for the error message.
1884 + } else {
1885 + switch (typeof value) {
1886 + case 'function':
1887 + case 'symbol':
1888 + case 'boolean':
1889 + break;
1890 + default: {
1891 + if (__DEV__) {
1892 + checkAttributeStringCoercion(value, propKey);
1893 + }
1894 + const sanitizedValue = sanitizeURL('' + value);
1895 + if (serverValue === sanitizedValue) {
1896 + return;
1897 + }
1898 + }
1899 + }
1900 }
1901 }
1902 + warnForPropDifference(propKey, serverValue, value);
1903 }
1904
1905 function diffHydratedCustomComponent(
@@ -1001,19 +1907,19 @@ function diffHydratedCustomComponent(
1907 tag: string,
1908 props: Object,
1909 parentNamespaceDev: string,
1004 - extraAttributeNames: Set<string>,
1910 + extraAttributes: Set<string>,
1911 ) {
1912 for (const propKey in props) {
1913 if (!props.hasOwnProperty(propKey)) {
1914 continue;
1915 }
1010 - const nextProp = props[propKey];
1011 - if (nextProp == null) {
1916 + const value = props[propKey];
1917 + if (value == null) {
1918 continue;
1919 }
1920 if (registrationNameDependencies.hasOwnProperty(propKey)) {
1015 - if (typeof nextProp !== 'function') {
1016 - warnForInvalidEventListener(propKey, nextProp);
1921 + if (typeof value !== 'function') {
1922 + warnForInvalidEventListener(propKey, value);
1923 }
1924 continue;
1925 }
@@ -1033,17 +1939,15 @@ function diffHydratedCustomComponent(
1939 continue;
1940 case 'dangerouslySetInnerHTML':
1941 const serverHTML = domElement.innerHTML;
1036 - const nextHtml = nextProp ? nextProp.__html : undefined;
1942 + const nextHtml = value ? value.__html : undefined;
1943 if (nextHtml != null) {
1944 const expectedHTML = normalizeHTML(domElement, nextHtml);
1039 - if (expectedHTML !== serverHTML) {
1040 - warnForPropDifference(propKey, serverHTML, expectedHTML);
1041 - }
1945 + warnForPropDifference(propKey, serverHTML, expectedHTML);
1946 }
1947 continue;
1948 case 'style':
1045 - extraAttributeNames.delete(propKey);
1046 - diffHydratedStyles(domElement, nextProp);
1949 + extraAttributes.delete(propKey);
1950 + diffHydratedStyles(domElement, value);
1951 continue;
1952 case 'offsetParent':
1953 case 'offsetTop':
@@ -1054,7 +1958,7 @@ function diffHydratedCustomComponent(
1958 case 'outerText':
1959 case 'outerHTML':
1960 if (enableCustomElementPropertySupport) {
1057 - extraAttributeNames.delete(propKey.toLowerCase());
1961 + extraAttributes.delete(propKey.toLowerCase());
1962 if (__DEV__) {
1963 console.error(
1964 'Assignment to read-only property will result in a no-op: `%s`',
@@ -1067,15 +1971,13 @@ function diffHydratedCustomComponent(
1971 case 'className':
1972 if (enableCustomElementPropertySupport) {
1973 // className is a special cased property on the server to render as an attribute.
1070 - extraAttributeNames.delete('class');
1974 + extraAttributes.delete('class');
1975 const serverValue = getValueForAttributeOnCustomComponent(
1976 domElement,
1977 'class',
1074 - nextProp,
1978 + value,
1979 );
1076 - if (nextProp !== serverValue) {
1077 - warnForPropDifference('className', serverValue, nextProp);
1078 - }
1980 + warnForPropDifference('className', serverValue, value);
1981 continue;
1982 }
1983 // eslint-disable-next-line no-fallthrough
@@ -1085,18 +1987,16 @@ function diffHydratedCustomComponent(
1987 ownNamespaceDev = getIntrinsicNamespace(tag);
1988 }
1989 if (ownNamespaceDev === HTML_NAMESPACE) {
1088 - extraAttributeNames.delete(propKey.toLowerCase());
1990 + extraAttributes.delete(propKey.toLowerCase());
1991 } else {
1090 - extraAttributeNames.delete(propKey);
1992 + extraAttributes.delete(propKey);
1993 }
1994 const serverValue = getValueForAttributeOnCustomComponent(
1995 domElement,
1996 propKey,
1095 - nextProp,
1997 + value,
1998 );
1097 - if (nextProp !== serverValue) {
1098 - warnForPropDifference(propKey, serverValue, nextProp);
1099 - }
1999 + warnForPropDifference(propKey, serverValue, value);
2000 }
2001 }
2002 }
@@ -1107,19 +2007,19 @@ function diffHydratedGenericElement(
2007 tag: string,
2008 props: Object,
2009 parentNamespaceDev: string,
1110 - extraAttributeNames: Set<string>,
2010 + extraAttributes: Set<string>,
2011 ) {
2012 for (const propKey in props) {
2013 if (!props.hasOwnProperty(propKey)) {
2014 continue;
2015 }
1116 - const nextProp = props[propKey];
1117 - if (nextProp == null) {
2016 + const value = props[propKey];
2017 + if (value == null) {
2018 continue;
2019 }
2020 if (registrationNameDependencies.hasOwnProperty(propKey)) {
1121 - if (typeof nextProp !== 'function') {
1122 - warnForInvalidEventListener(propKey, nextProp);
2021 + if (typeof value !== 'function') {
2022 + warnForInvalidEventListener(propKey, value);
2023 }
2024 continue;
2025 }
@@ -1142,157 +2042,1110 @@ function diffHydratedGenericElement(
2042 continue;
2043 case 'dangerouslySetInnerHTML':
2044 const serverHTML = domElement.innerHTML;
1145 - const nextHtml = nextProp ? nextProp.__html : undefined;
2045 + const nextHtml = value ? value.__html : undefined;
2046 if (nextHtml != null) {
2047 const expectedHTML = normalizeHTML(domElement, nextHtml);
1148 - if (expectedHTML !== serverHTML) {
1149 - warnForPropDifference(propKey, serverHTML, expectedHTML);
1150 - }
2048 + warnForPropDifference(propKey, serverHTML, expectedHTML);
2049 }
2050 continue;
2051 case 'style':
1154 - extraAttributeNames.delete(propKey);
1155 - diffHydratedStyles(domElement, nextProp);
2052 + extraAttributes.delete(propKey);
2053 + diffHydratedStyles(domElement, value);
2054 continue;
2055 case 'multiple': {
1158 - extraAttributeNames.delete(propKey);
2056 + extraAttributes.delete(propKey);
2057 const serverValue = (domElement: any).multiple;
1160 - if (nextProp !== serverValue) {
1161 - warnForPropDifference('multiple', serverValue, nextProp);
1162 - }
2058 + warnForPropDifference(propKey, serverValue, value);
2059 continue;
2060 }
2061 case 'muted': {
1166 - extraAttributeNames.delete(propKey);
2062 + extraAttributes.delete(propKey);
2063 const serverValue = (domElement: any).muted;
1168 - if (nextProp !== serverValue) {
1169 - warnForPropDifference('muted', serverValue, nextProp);
1170 - }
2064 + warnForPropDifference(propKey, serverValue, value);
2065 continue;
2066 }
1173 - default:
1174 - if (
1175 - // shouldIgnoreAttribute
1176 - // We have already filtered out null/undefined and reserved words.
1177 - propKey.length > 2 &&
1178 - (propKey[0] === 'o' || propKey[0] === 'O') &&
1179 - (propKey[1] === 'n' || propKey[1] === 'N')
1180 - ) {
1181 - continue;
1182 - }
1183 - const propertyInfo = getPropertyInfo(propKey);
1184 - let isMismatchDueToBadCasing = false;
1185 - let serverValue;
1186 - if (propertyInfo !== null) {
1187 - extraAttributeNames.delete(propertyInfo.attributeName);
1188 - serverValue = getValueForProperty(
1189 - domElement,
1190 - propKey,
1191 - nextProp,
1192 - propertyInfo,
1193 - );
1194 - } else {
1195 - let ownNamespaceDev = parentNamespaceDev;
1196 - if (ownNamespaceDev === HTML_NAMESPACE) {
1197 - ownNamespaceDev = getIntrinsicNamespace(tag);
1198 - }
1199 - if (ownNamespaceDev === HTML_NAMESPACE) {
1200 - extraAttributeNames.delete(propKey.toLowerCase());
1201 - } else {
1202 - const standardName = getPossibleStandardName(propKey);
1203 - if (standardName !== null && standardName !== propKey) {
1204 - // If an SVG prop is supplied with bad casing, it will
1205 - // be successfully parsed from HTML, but will produce a mismatch
1206 - // (and would be incorrectly rendered on the client).
1207 - // However, we already warn about bad casing elsewhere.
1208 - // So we'll skip the misleading extra mismatch warning in this case.
1209 - isMismatchDueToBadCasing = true;
1210 - extraAttributeNames.delete(standardName);
2067 + case 'autoFocus': {
2068 + extraAttributes.delete('autofocus');
2069 + const serverValue = (domElement: any).autofocus;
2070 + warnForPropDifference(propKey, serverValue, value);
2071 + continue;
2072 + }
2073 + case 'src':
2074 + case 'href':
2075 + case 'action':
2076 + if (enableFilterEmptyStringAttributesDOM) {
2077 + if (value === '') {
2078 + if (__DEV__) {
2079 + if (propKey === 'src') {
2080 + console.error(
2081 + 'An empty string ("") was passed to the %s attribute. ' +
2082 + 'This may cause the browser to download the whole page again over the network. ' +
2083 + 'To fix this, either do not render the element at all ' +
2084 + 'or pass null to %s instead of an empty string.',
2085 + propKey,
2086 + propKey,
2087 + );
2088 + } else {
2089 + console.error(
2090 + 'An empty string ("") was passed to the %s attribute. ' +
2091 + 'To fix this, either do not render the element at all ' +
2092 + 'or pass null to %s instead of an empty string.',
2093 + propKey,
2094 + propKey,
2095 + );
2096 + }
2097 }
1212 - extraAttributeNames.delete(propKey);
2098 + hydrateSanitizedAttribute(
2099 + domElement,
2100 + propKey,
2101 + propKey,
2102 + null,
2103 + extraAttributes,
2104 + );
2105 + continue;
2106 }
1214 - serverValue = getValueForAttribute(domElement, propKey, nextProp);
1215 - }
1216 -
1217 - if (nextProp !== serverValue && !isMismatchDueToBadCasing) {
1218 - warnForPropDifference(propKey, serverValue, nextProp);
2107 }
1220 - }
1221 - }
1222 -}
1223 -
1224 -export function diffHydratedProperties(
1225 - domElement: Element,
1226 - tag: string,
1227 - props: Object,
1228 - isConcurrentMode: boolean,
1229 - shouldWarnDev: boolean,
1230 - parentNamespaceDev: string,
1231 -): null | Array<mixed> {
1232 - if (__DEV__) {
1233 - validatePropertiesInDevelopment(tag, props);
1234 - }
1235 -
1236 - // TODO: Make sure that we check isMounted before firing any of these events.
1237 - switch (tag) {
1238 - case 'dialog':
1239 - listenToNonDelegatedEvent('cancel', domElement);
1240 - listenToNonDelegatedEvent('close', domElement);
1241 - break;
1242 - case 'iframe':
1243 - case 'object':
1244 - case 'embed':
1245 - // We listen to this event in case to ensure emulated bubble
1246 - // listeners still fire for the load event.
1247 - listenToNonDelegatedEvent('load', domElement);
1248 - break;
1249 - case 'video':
1250 - case 'audio':
1251 - // We listen to these events in case to ensure emulated bubble
1252 - // listeners still fire for all the media events.
1253 - for (let i = 0; i < mediaEventTypes.length; i++) {
1254 - listenToNonDelegatedEvent(mediaEventTypes[i], domElement);
2108 + hydrateSanitizedAttribute(
2109 + domElement,
2110 + propKey,
2111 + propKey,
2112 + value,
2113 + extraAttributes,
2114 + );
2115 + continue;
2116 + case 'formAction':
2117 + hydrateSanitizedAttribute(
2118 + domElement,
2119 + propKey,
2120 + 'formaction',
2121 + value,
2122 + extraAttributes,
2123 + );
2124 + continue;
2125 + case 'xlinkHref':
2126 + hydrateSanitizedAttribute(
2127 + domElement,
2128 + propKey,
2129 + 'xlink:href',
2130 + value,
2131 + extraAttributes,
2132 + );
2133 + continue;
2134 + case 'contentEditable': {
2135 + // Lower-case Booleanish String
2136 + hydrateBooleanishAttribute(
2137 + domElement,
2138 + propKey,
2139 + 'contenteditable',
2140 + value,
2141 + extraAttributes,
2142 + );
2143 + continue;
2144 }
1256 - break;
1257 - case 'source':
1258 - // We listen to this event in case to ensure emulated bubble
1259 - // listeners still fire for the error event.
1260 - listenToNonDelegatedEvent('error', domElement);
1261 - break;
1262 - case 'img':
1263 - case 'image':
1264 - case 'link':
1265 - // We listen to these events in case to ensure emulated bubble
1266 - // listeners still fire for error and load events.
1267 - listenToNonDelegatedEvent('error', domElement);
1268 - listenToNonDelegatedEvent('load', domElement);
1269 - break;
1270 - case 'details':
1271 - // We listen to this event in case to ensure emulated bubble
1272 - // listeners still fire for the toggle event.
1273 - listenToNonDelegatedEvent('toggle', domElement);
1274 - break;
1275 - case 'input':
1276 - ReactDOMInputInitWrapperState(domElement, props);
1277 - // We listen to this event in case to ensure emulated bubble
1278 - // listeners still fire for the invalid event.
1279 - listenToNonDelegatedEvent('invalid', domElement);
1280 - // TODO: Make sure we check if this is still unmounted or do any clean
1281 - // up necessary since we never stop tracking anymore.
1282 - track((domElement: any));
1283 - // For input and textarea we current always set the value property at
1284 - // post mount to force it to diverge from attributes. However, for
1285 - // option and select we don't quite do the same thing and select
1286 - // is not resilient to the DOM state changing so we don't do that here.
1287 - // TODO: Consider not doing this for input and textarea.
1288 - ReactDOMInputPostMountWrapper(domElement, props, true);
1289 - break;
1290 - case 'option':
1291 - ReactDOMOptionValidateProps(domElement, props);
1292 - break;
1293 - case 'select':
1294 - ReactDOMSelectInitWrapperState(domElement, props);
1295 - // We listen to this event in case to ensure emulated bubble
2145 + case 'spellCheck': {
2146 + // Lower-case Booleanish String
2147 + hydrateBooleanishAttribute(
2148 + domElement,
2149 + propKey,
2150 + 'spellcheck',
2151 + value,
2152 + extraAttributes,
2153 + );
2154 + continue;
2155 + }
2156 + case 'draggable':
2157 + case 'autoReverse':
2158 + case 'externalResourcesRequired':
2159 + case 'focusable':
2160 + case 'preserveAlpha': {
2161 + // Case-sensitive Booleanish String
2162 + hydrateBooleanishAttribute(
2163 + domElement,
2164 + propKey,
2165 + propKey,
2166 + value,
2167 + extraAttributes,
2168 + );
2169 + continue;
2170 + }
2171 + case 'allowFullScreen':
2172 + case 'async':
2173 + case 'autoPlay':
2174 + case 'controls':
2175 + case 'default':
2176 + case 'defer':
2177 + case 'disabled':
2178 + case 'disablePictureInPicture':
2179 + case 'disableRemotePlayback':
2180 + case 'formNoValidate':
2181 + case 'hidden':
2182 + case 'loop':
2183 + case 'noModule':
2184 + case 'noValidate':
2185 + case 'open':
2186 + case 'playsInline':
2187 + case 'readOnly':
2188 + case 'required':
2189 + case 'reversed':
2190 + case 'scoped':
2191 + case 'seamless':
2192 + case 'itemScope': {
2193 + // Some of these need to be lower case to remove them from the extraAttributes list.
2194 + hydrateBooleanAttribute(
2195 + domElement,
2196 + propKey,
2197 + propKey.toLowerCase(),
2198 + value,
2199 + extraAttributes,
2200 + );
2201 + continue;
2202 + }
2203 + case 'capture':
2204 + case 'download': {
2205 + hydrateOverloadedBooleanAttribute(
2206 + domElement,
2207 + propKey,
2208 + propKey,
2209 + value,
2210 + extraAttributes,
2211 + );
2212 + continue;
2213 + }
2214 + case 'cols':
2215 + case 'rows':
2216 + case 'size':
2217 + case 'span': {
2218 + hydratePositiveNumericAttribute(
2219 + domElement,
2220 + propKey,
2221 + propKey,
2222 + value,
2223 + extraAttributes,
2224 + );
2225 + continue;
2226 + }
2227 + case 'rowSpan': {
2228 + hydrateNumericAttribute(
2229 + domElement,
2230 + propKey,
2231 + 'rowspan',
2232 + value,
2233 + extraAttributes,
2234 + );
2235 + continue;
2236 + }
2237 + case 'start': {
2238 + hydrateNumericAttribute(
2239 + domElement,
2240 + propKey,
2241 + propKey,
2242 + value,
2243 + extraAttributes,
2244 + );
2245 + continue;
2246 + }
2247 + // A few React string attributes have a different name.
2248 + // This is a mapping from React prop names to the attribute names.
2249 + case 'acceptCharset':
2250 + hydrateAttribute(
2251 + domElement,
2252 + propKey,
2253 + 'accept-charset',
2254 + value,
2255 + extraAttributes,
2256 + );
2257 + continue;
2258 + case 'className':
2259 + hydrateAttribute(domElement, propKey, 'class', value, extraAttributes);
2260 + continue;
2261 + case 'htmlFor':
2262 + hydrateAttribute(domElement, propKey, 'for', value, extraAttributes);
2263 + continue;
2264 + case 'httpEquiv':
2265 + hydrateAttribute(
2266 + domElement,
2267 + propKey,
2268 + 'http-equiv',
2269 + value,
2270 + extraAttributes,
2271 + );
2272 + continue;
2273 + case 'tabIndex':
2274 + hydrateAttribute(
2275 + domElement,
2276 + propKey,
2277 + 'tabindex',
2278 + value,
2279 + extraAttributes,
2280 + );
2281 + continue;
2282 + case 'crossOrigin':
2283 + hydrateAttribute(
2284 + domElement,
2285 + propKey,
2286 + 'crossorigin',
2287 + value,
2288 + extraAttributes,
2289 + );
2290 + continue;
2291 + case 'accentHeight':
2292 + hydrateAttribute(
2293 + domElement,
2294 + propKey,
2295 + 'accent-height',
2296 + value,
2297 + extraAttributes,
2298 + );
2299 + continue;
2300 + case 'alignmentBaseline':
2301 + hydrateAttribute(
2302 + domElement,
2303 + propKey,
2304 + 'alignment-baseline',
2305 + value,
2306 + extraAttributes,
2307 + );
2308 + continue;
2309 + case 'arabicForm':
2310 + hydrateAttribute(
2311 + domElement,
2312 + propKey,
2313 + 'arabic-form',
2314 + value,
2315 + extraAttributes,
2316 + );
2317 + continue;
2318 + case 'baselineShift':
2319 + hydrateAttribute(
2320 + domElement,
2321 + propKey,
2322 + 'baseline-shift',
2323 + value,
2324 + extraAttributes,
2325 + );
2326 + continue;
2327 + case 'capHeight':
2328 + hydrateAttribute(
2329 + domElement,
2330 + propKey,
2331 + 'cap-height',
2332 + value,
2333 + extraAttributes,
2334 + );
2335 + continue;
2336 + case 'clipPath':
2337 + hydrateAttribute(
2338 + domElement,
2339 + propKey,
2340 + 'clip-path',
2341 + value,
2342 + extraAttributes,
2343 + );
2344 + continue;
2345 + case 'clipRule':
2346 + hydrateAttribute(
2347 + domElement,
2348 + propKey,
2349 + 'clip-rule',
2350 + value,
2351 + extraAttributes,
2352 + );
2353 + continue;
2354 + case 'colorInterpolation':
2355 + hydrateAttribute(
2356 + domElement,
2357 + propKey,
2358 + 'color-interpolation',
2359 + value,
2360 + extraAttributes,
2361 + );
2362 + continue;
2363 + case 'colorInterpolationFilters':
2364 + hydrateAttribute(
2365 + domElement,
2366 + propKey,
2367 + 'color-interpolation-filters',
2368 + value,
2369 + extraAttributes,
2370 + );
2371 + continue;
2372 + case 'colorProfile':
2373 + hydrateAttribute(
2374 + domElement,
2375 + propKey,
2376 + 'color-profile',
2377 + value,
2378 + extraAttributes,
2379 + );
2380 + continue;
2381 + case 'colorRendering':
2382 + hydrateAttribute(
2383 + domElement,
2384 + propKey,
2385 + 'color-rendering',
2386 + value,
2387 + extraAttributes,
2388 + );
2389 + continue;
2390 + case 'dominantBaseline':
2391 + hydrateAttribute(
2392 + domElement,
2393 + propKey,
2394 + 'dominant-baseline',
2395 + value,
2396 + extraAttributes,
2397 + );
2398 + continue;
2399 + case 'enableBackground':
2400 + hydrateAttribute(
2401 + domElement,
2402 + propKey,
2403 + 'enable-background',
2404 + value,
2405 + extraAttributes,
2406 + );
2407 + continue;
2408 + case 'fillOpacity':
2409 + hydrateAttribute(
2410 + domElement,
2411 + propKey,
2412 + 'fill-opacity',
2413 + value,
2414 + extraAttributes,
2415 + );
2416 + continue;
2417 + case 'fillRule':
2418 + hydrateAttribute(
2419 + domElement,
2420 + propKey,
2421 + 'fill-rule',
2422 + value,
2423 + extraAttributes,
2424 + );
2425 + continue;
2426 + case 'floodColor':
2427 + hydrateAttribute(
2428 + domElement,
2429 + propKey,
2430 + 'flood-color',
2431 + value,
2432 + extraAttributes,
2433 + );
2434 + continue;
2435 + case 'floodOpacity':
2436 + hydrateAttribute(
2437 + domElement,
2438 + propKey,
2439 + 'flood-opacity',
2440 + value,
2441 + extraAttributes,
2442 + );
2443 + continue;
2444 + case 'fontFamily':
2445 + hydrateAttribute(
2446 + domElement,
2447 + propKey,
2448 + 'font-family',
2449 + value,
2450 + extraAttributes,
2451 + );
2452 + continue;
2453 + case 'fontSize':
2454 + hydrateAttribute(
2455 + domElement,
2456 + propKey,
2457 + 'font-size',
2458 + value,
2459 + extraAttributes,
2460 + );
2461 + continue;
2462 + case 'fontSizeAdjust':
2463 + hydrateAttribute(
2464 + domElement,
2465 + propKey,
2466 + 'font-size-adjust',
2467 + value,
2468 + extraAttributes,
2469 + );
2470 + continue;
2471 + case 'fontStretch':
2472 + hydrateAttribute(
2473 + domElement,
2474 + propKey,
2475 + 'font-stretch',
2476 + value,
2477 + extraAttributes,
2478 + );
2479 + continue;
2480 + case 'fontStyle':
2481 + hydrateAttribute(
2482 + domElement,
2483 + propKey,
2484 + 'font-style',
2485 + value,
2486 + extraAttributes,
2487 + );
2488 + continue;
2489 + case 'fontVariant':
2490 + hydrateAttribute(
2491 + domElement,
2492 + propKey,
2493 + 'font-variant',
2494 + value,
2495 + extraAttributes,
2496 + );
2497 + continue;
2498 + case 'fontWeight':
2499 + hydrateAttribute(
2500 + domElement,
2501 + propKey,
2502 + 'font-weight',
2503 + value,
2504 + extraAttributes,
2505 + );
2506 + continue;
2507 + case 'glyphName':
2508 + hydrateAttribute(
2509 + domElement,
2510 + propKey,
2511 + 'glyph-name',
2512 + value,
2513 + extraAttributes,
2514 + );
2515 + continue;
2516 + case 'glyphOrientationHorizontal':
2517 + hydrateAttribute(
2518 + domElement,
2519 + propKey,
2520 + 'glyph-orientation-horizontal',
2521 + value,
2522 + extraAttributes,
2523 + );
2524 + continue;
2525 + case 'glyphOrientationVertical':
2526 + hydrateAttribute(
2527 + domElement,
2528 + propKey,
2529 + 'glyph-orientation-vertical',
2530 + value,
2531 + extraAttributes,
2532 + );
2533 + continue;
2534 + case 'horizAdvX':
2535 + hydrateAttribute(
2536 + domElement,
2537 + propKey,
2538 + 'horiz-adv-x',
2539 + value,
2540 + extraAttributes,
2541 + );
2542 + continue;
2543 + case 'horizOriginX':
2544 + hydrateAttribute(
2545 + domElement,
2546 + propKey,
2547 + 'horiz-origin-x',
2548 + value,
2549 + extraAttributes,
2550 + );
2551 + continue;
2552 + case 'imageRendering':
2553 + hydrateAttribute(
2554 + domElement,
2555 + propKey,
2556 + 'image-rendering',
2557 + value,
2558 + extraAttributes,
2559 + );
2560 + continue;
2561 + case 'letterSpacing':
2562 + hydrateAttribute(
2563 + domElement,
2564 + propKey,
2565 + 'letter-spacing',
2566 + value,
2567 + extraAttributes,
2568 + );
2569 + continue;
2570 + case 'lightingColor':
2571 + hydrateAttribute(
2572 + domElement,
2573 + propKey,
2574 + 'lighting-color',
2575 + value,
2576 + extraAttributes,
2577 + );
2578 + continue;
2579 + case 'markerEnd':
2580 + hydrateAttribute(
2581 + domElement,
2582 + propKey,
2583 + 'marker-end',
2584 + value,
2585 + extraAttributes,
2586 + );
2587 + continue;
2588 + case 'markerMid':
2589 + hydrateAttribute(
2590 + domElement,
2591 + propKey,
2592 + 'marker-mid',
2593 + value,
2594 + extraAttributes,
2595 + );
2596 + continue;
2597 + case 'markerStart':
2598 + hydrateAttribute(
2599 + domElement,
2600 + propKey,
2601 + 'marker-start',
2602 + value,
2603 + extraAttributes,
2604 + );
2605 + continue;
2606 + case 'overlinePosition':
2607 + hydrateAttribute(
2608 + domElement,
2609 + propKey,
2610 + 'overline-position',
2611 + value,
2612 + extraAttributes,
2613 + );
2614 + continue;
2615 + case 'overlineThickness':
2616 + hydrateAttribute(
2617 + domElement,
2618 + propKey,
2619 + 'overline-thickness',
2620 + value,
2621 + extraAttributes,
2622 + );
2623 + continue;
2624 + case 'paintOrder':
2625 + hydrateAttribute(
2626 + domElement,
2627 + propKey,
2628 + 'paint-order',
2629 + value,
2630 + extraAttributes,
2631 + );
2632 + continue;
2633 + case 'panose-1':
2634 + hydrateAttribute(
2635 + domElement,
2636 + propKey,
2637 + 'panose-1',
2638 + value,
2639 + extraAttributes,
2640 + );
2641 + continue;
2642 + case 'pointerEvents':
2643 + hydrateAttribute(
2644 + domElement,
2645 + propKey,
2646 + 'pointer-events',
2647 + value,
2648 + extraAttributes,
2649 + );
2650 + continue;
2651 + case 'renderingIntent':
2652 + hydrateAttribute(
2653 + domElement,
2654 + propKey,
2655 + 'rendering-intent',
2656 + value,
2657 + extraAttributes,
2658 + );
2659 + continue;
2660 + case 'shapeRendering':
2661 + hydrateAttribute(
2662 + domElement,
2663 + propKey,
2664 + 'shape-rendering',
2665 + value,
2666 + extraAttributes,
2667 + );
2668 + continue;
2669 + case 'stopColor':
2670 + hydrateAttribute(
2671 + domElement,
2672 + propKey,
2673 + 'stop-color',
2674 + value,
2675 + extraAttributes,
2676 + );
2677 + continue;
2678 + case 'stopOpacity':
2679 + hydrateAttribute(
2680 + domElement,
2681 + propKey,
2682 + 'stop-opacity',
2683 + value,
2684 + extraAttributes,
2685 + );
2686 + continue;
2687 + case 'strikethroughPosition':
2688 + hydrateAttribute(
2689 + domElement,
2690 + propKey,
2691 + 'strikethrough-position',
2692 + value,
2693 + extraAttributes,
2694 + );
2695 + continue;
2696 + case 'strikethroughThickness':
2697 + hydrateAttribute(
2698 + domElement,
2699 + propKey,
2700 + 'strikethrough-thickness',
2701 + value,
2702 + extraAttributes,
2703 + );
2704 + continue;
2705 + case 'strokeDasharray':
2706 + hydrateAttribute(
2707 + domElement,
2708 + propKey,
2709 + 'stroke-dasharray',
2710 + value,
2711 + extraAttributes,
2712 + );
2713 + continue;
2714 + case 'strokeDashoffset':
2715 + hydrateAttribute(
2716 + domElement,
2717 + propKey,
2718 + 'stroke-dashoffset',
2719 + value,
2720 + extraAttributes,
2721 + );
2722 + continue;
2723 + case 'strokeLinecap':
2724 + hydrateAttribute(
2725 + domElement,
2726 + propKey,
2727 + 'stroke-linecap',
2728 + value,
2729 + extraAttributes,
2730 + );
2731 + continue;
2732 + case 'strokeLinejoin':
2733 + hydrateAttribute(
2734 + domElement,
2735 + propKey,
2736 + 'stroke-linejoin',
2737 + value,
2738 + extraAttributes,
2739 + );
2740 + continue;
2741 + case 'strokeMiterlimit':
2742 + hydrateAttribute(
2743 + domElement,
2744 + propKey,
2745 + 'stroke-miterlimit',
2746 + value,
2747 + extraAttributes,
2748 + );
2749 + continue;
2750 + case 'strokeOpacity':
2751 + hydrateAttribute(
2752 + domElement,
2753 + propKey,
2754 + 'stroke-opacity',
2755 + value,
2756 + extraAttributes,
2757 + );
2758 + continue;
2759 + case 'strokeWidth':
2760 + hydrateAttribute(
2761 + domElement,
2762 + propKey,
2763 + 'stroke-width',
2764 + value,
2765 + extraAttributes,
2766 + );
2767 + continue;
2768 + case 'textAnchor':
2769 + hydrateAttribute(
2770 + domElement,
2771 + propKey,
2772 + 'text-anchor',
2773 + value,
2774 + extraAttributes,
2775 + );
2776 + continue;
2777 + case 'textDecoration':
2778 + hydrateAttribute(
2779 + domElement,
2780 + propKey,
2781 + 'text-decoration',
2782 + value,
2783 + extraAttributes,
2784 + );
2785 + continue;
2786 + case 'textRendering':
2787 + hydrateAttribute(
2788 + domElement,
2789 + propKey,
2790 + 'text-rendering',
2791 + value,
2792 + extraAttributes,
2793 + );
2794 + continue;
2795 + case 'transformOrigin':
2796 + hydrateAttribute(
2797 + domElement,
2798 + propKey,
2799 + 'transform-origin',
2800 + value,
2801 + extraAttributes,
2802 + );
2803 + continue;
2804 + case 'underlinePosition':
2805 + hydrateAttribute(
2806 + domElement,
2807 + propKey,
2808 + 'underline-position',
2809 + value,
2810 + extraAttributes,
2811 + );
2812 + continue;
2813 + case 'underlineThickness':
2814 + hydrateAttribute(
2815 + domElement,
2816 + propKey,
2817 + 'underline-thickness',
2818 + value,
2819 + extraAttributes,
2820 + );
2821 + continue;
2822 + case 'unicodeBidi':
2823 + hydrateAttribute(
2824 + domElement,
2825 + propKey,
2826 + 'unicode-bidi',
2827 + value,
2828 + extraAttributes,
2829 + );
2830 + continue;
2831 + case 'unicodeRange':
2832 + hydrateAttribute(
2833 + domElement,
2834 + propKey,
2835 + 'unicode-range',
2836 + value,
2837 + extraAttributes,
2838 + );
2839 + continue;
2840 + case 'unitsPerEm':
2841 + hydrateAttribute(
2842 + domElement,
2843 + propKey,
2844 + 'units-per-em',
2845 + value,
2846 + extraAttributes,
2847 + );
2848 + continue;
2849 + case 'vAlphabetic':
2850 + hydrateAttribute(
2851 + domElement,
2852 + propKey,
2853 + 'v-alphabetic',
2854 + value,
2855 + extraAttributes,
2856 + );
2857 + continue;
2858 + case 'vHanging':
2859 + hydrateAttribute(
2860 + domElement,
2861 + propKey,
2862 + 'v-hanging',
2863 + value,
2864 + extraAttributes,
2865 + );
2866 + continue;
2867 + case 'vIdeographic':
2868 + hydrateAttribute(
2869 + domElement,
2870 + propKey,
2871 + 'v-ideographic',
2872 + value,
2873 + extraAttributes,
2874 + );
2875 + continue;
2876 + case 'vMathematical':
2877 + hydrateAttribute(
2878 + domElement,
2879 + propKey,
2880 + 'v-mathematical',
2881 + value,
2882 + extraAttributes,
2883 + );
2884 + continue;
2885 + case 'vectorEffect':
2886 + hydrateAttribute(
2887 + domElement,
2888 + propKey,
2889 + 'vector-effect',
2890 + value,
2891 + extraAttributes,
2892 + );
2893 + continue;
2894 + case 'vertAdvY':
2895 + hydrateAttribute(
2896 + domElement,
2897 + propKey,
2898 + 'vert-adv-y',
2899 + value,
2900 + extraAttributes,
2901 + );
2902 + continue;
2903 + case 'vertOriginX':
2904 + hydrateAttribute(
2905 + domElement,
2906 + propKey,
2907 + 'vert-origin-x',
2908 + value,
2909 + extraAttributes,
2910 + );
2911 + continue;
2912 + case 'vertOriginY':
2913 + hydrateAttribute(
2914 + domElement,
2915 + propKey,
2916 + 'vert-origin-y',
2917 + value,
2918 + extraAttributes,
2919 + );
2920 + continue;
2921 + case 'wordSpacing':
2922 + hydrateAttribute(
2923 + domElement,
2924 + propKey,
2925 + 'word-spacing',
2926 + value,
2927 + extraAttributes,
2928 + );
2929 + continue;
2930 + case 'writingMode':
2931 + hydrateAttribute(
2932 + domElement,
2933 + propKey,
2934 + 'writing-mode',
2935 + value,
2936 + extraAttributes,
2937 + );
2938 + continue;
2939 + case 'xmlnsXlink':
2940 + hydrateAttribute(
2941 + domElement,
2942 + propKey,
2943 + 'xmlns:xlink',
2944 + value,
2945 + extraAttributes,
2946 + );
2947 + continue;
2948 + case 'xHeight':
2949 + hydrateAttribute(
2950 + domElement,
2951 + propKey,
2952 + 'x-height',
2953 + value,
2954 + extraAttributes,
2955 + );
2956 + continue;
2957 + case 'xlinkActuate':
2958 + hydrateAttribute(
2959 + domElement,
2960 + propKey,
2961 + 'xlink:actuate',
2962 + value,
2963 + extraAttributes,
2964 + );
2965 + continue;
2966 + case 'xlinkArcrole':
2967 + hydrateAttribute(
2968 + domElement,
2969 + propKey,
2970 + 'xlink:arcrole',
2971 + value,
2972 + extraAttributes,
2973 + );
2974 + continue;
2975 + case 'xlinkRole':
2976 + hydrateAttribute(
2977 + domElement,
2978 + propKey,
2979 + 'xlink:role',
2980 + value,
2981 + extraAttributes,
2982 + );
2983 + continue;
2984 + case 'xlinkShow':
2985 + hydrateAttribute(
2986 + domElement,
2987 + propKey,
2988 + 'xlink:show',
2989 + value,
2990 + extraAttributes,
2991 + );
2992 + continue;
2993 + case 'xlinkTitle':
2994 + hydrateAttribute(
2995 + domElement,
2996 + propKey,
2997 + 'xlink:title',
2998 + value,
2999 + extraAttributes,
3000 + );
3001 + continue;
3002 + case 'xlinkType':
3003 + hydrateAttribute(
3004 + domElement,
3005 + propKey,
3006 + 'xlink:type',
3007 + value,
3008 + extraAttributes,
3009 + );
3010 + continue;
3011 + case 'xmlBase':
3012 + hydrateAttribute(
3013 + domElement,
3014 + propKey,
3015 + 'xml:base',
3016 + value,
3017 + extraAttributes,
3018 + );
3019 + continue;
3020 + case 'xmlLang':
3021 + hydrateAttribute(
3022 + domElement,
3023 + propKey,
3024 + 'xml:lang',
3025 + value,
3026 + extraAttributes,
3027 + );
3028 + continue;
3029 + case 'xmlSpace':
3030 + hydrateAttribute(
3031 + domElement,
3032 + propKey,
3033 + 'xml:space',
3034 + value,
3035 + extraAttributes,
3036 + );
3037 + continue;
3038 + default: {
3039 + if (
3040 + // shouldIgnoreAttribute
3041 + // We have already filtered out null/undefined and reserved words.
3042 + propKey.length > 2 &&
3043 + (propKey[0] === 'o' || propKey[0] === 'O') &&
3044 + (propKey[1] === 'n' || propKey[1] === 'N')
3045 + ) {
3046 + continue;
3047 + }
3048 + let isMismatchDueToBadCasing = false;
3049 + let ownNamespaceDev = parentNamespaceDev;
3050 + if (ownNamespaceDev === HTML_NAMESPACE) {
3051 + ownNamespaceDev = getIntrinsicNamespace(tag);
3052 + }
3053 + if (ownNamespaceDev === HTML_NAMESPACE) {
3054 + extraAttributes.delete(propKey.toLowerCase());
3055 + } else {
3056 + const standardName = getPossibleStandardName(propKey);
3057 + if (standardName !== null && standardName !== propKey) {
3058 + // If an SVG prop is supplied with bad casing, it will
3059 + // be successfully parsed from HTML, but will produce a mismatch
3060 + // (and would be incorrectly rendered on the client).
3061 + // However, we already warn about bad casing elsewhere.
3062 + // So we'll skip the misleading extra mismatch warning in this case.
3063 + isMismatchDueToBadCasing = true;
3064 + extraAttributes.delete(standardName);
3065 + }
3066 + extraAttributes.delete(propKey);
3067 + }
3068 + const serverValue = getValueForAttribute(domElement, propKey, value);
3069 + if (!isMismatchDueToBadCasing) {
3070 + warnForPropDifference(propKey, serverValue, value);
3071 + }
3072 + }
3073 + }
3074 + }
3075 +}
3076 +
3077 +export function diffHydratedProperties(
3078 + domElement: Element,
3079 + tag: string,
3080 + props: Object,
3081 + isConcurrentMode: boolean,
3082 + shouldWarnDev: boolean,
3083 + parentNamespaceDev: string,
3084 +): null | Array<mixed> {
3085 + if (__DEV__) {
3086 + validatePropertiesInDevelopment(tag, props);
3087 + }
3088 +
3089 + // TODO: Make sure that we check isMounted before firing any of these events.
3090 + switch (tag) {
3091 + case 'dialog':
3092 + listenToNonDelegatedEvent('cancel', domElement);
3093 + listenToNonDelegatedEvent('close', domElement);
3094 + break;
3095 + case 'iframe':
3096 + case 'object':
3097 + case 'embed':
3098 + // We listen to this event in case to ensure emulated bubble
3099 + // listeners still fire for the load event.
3100 + listenToNonDelegatedEvent('load', domElement);
3101 + break;
3102 + case 'video':
3103 + case 'audio':
3104 + // We listen to these events in case to ensure emulated bubble
3105 + // listeners still fire for all the media events.
3106 + for (let i = 0; i < mediaEventTypes.length; i++) {
3107 + listenToNonDelegatedEvent(mediaEventTypes[i], domElement);
3108 + }
3109 + break;
3110 + case 'source':
3111 + // We listen to this event in case to ensure emulated bubble
3112 + // listeners still fire for the error event.
3113 + listenToNonDelegatedEvent('error', domElement);
3114 + break;
3115 + case 'img':
3116 + case 'image':
3117 + case 'link':
3118 + // We listen to these events in case to ensure emulated bubble
3119 + // listeners still fire for error and load events.
3120 + listenToNonDelegatedEvent('error', domElement);
3121 + listenToNonDelegatedEvent('load', domElement);
3122 + break;
3123 + case 'details':
3124 + // We listen to this event in case to ensure emulated bubble
3125 + // listeners still fire for the toggle event.
3126 + listenToNonDelegatedEvent('toggle', domElement);
3127 + break;
3128 + case 'input':
3129 + ReactDOMInputInitWrapperState(domElement, props);
3130 + // We listen to this event in case to ensure emulated bubble
3131 + // listeners still fire for the invalid event.
3132 + listenToNonDelegatedEvent('invalid', domElement);
3133 + // TODO: Make sure we check if this is still unmounted or do any clean
3134 + // up necessary since we never stop tracking anymore.
3135 + track((domElement: any));
3136 + // For input and textarea we current always set the value property at
3137 + // post mount to force it to diverge from attributes. However, for
3138 + // option and select we don't quite do the same thing and select
3139 + // is not resilient to the DOM state changing so we don't do that here.
3140 + // TODO: Consider not doing this for input and textarea.
3141 + ReactDOMInputPostMountWrapper(domElement, props, true);
3142 + break;
3143 + case 'option':
3144 + ReactDOMOptionValidateProps(domElement, props);
3145 + break;
3146 + case 'select':
3147 + ReactDOMSelectInitWrapperState(domElement, props);
3148 + // We listen to this event in case to ensure emulated bubble
3149 // listeners still fire for the invalid event.
3150 listenToNonDelegatedEvent('invalid', domElement);
3151 break;
@@ -1346,7 +3199,7 @@ export function diffHydratedProperties(
3199 }
3200
3201 if (__DEV__ && shouldWarnDev) {
1349 - const extraAttributeNames: Set<string> = new Set();
3202 + const extraAttributes: Set<string> = new Set();
3203 const attributes = domElement.attributes;
3204 for (let i = 0; i < attributes.length; i++) {
3205 const name = attributes[i].name.toLowerCase();
@@ -1362,7 +3215,7 @@ export function diffHydratedProperties(
3215 default:
3216 // Intentionally use the original name.
3217 // See discussion in https://github.com/facebook/react/pull/10676.
1365 - extraAttributeNames.add(attributes[i].name);
3218 + extraAttributes.add(attributes[i].name);
3219 }
3220 }
3221 if (isCustomElement(tag, props)) {
@@ -1371,7 +3224,7 @@ export function diffHydratedProperties(
3224 tag,
3225 props,
3226 parentNamespaceDev,
1374 - extraAttributeNames,
3227 + extraAttributes,
3228 );
3229 } else {
3230 diffHydratedGenericElement(
@@ -1379,14 +3232,11 @@ export function diffHydratedProperties(
3232 tag,
3233 props,
3234 parentNamespaceDev,
1382 - extraAttributeNames,
3235 + extraAttributes,
3236 );
3237 }
1385 - if (
1386 - extraAttributeNames.size > 0 &&
1387 - props.suppressHydrationWarning !== true
1388 - ) {
1389 - warnForExtraAttributes(extraAttributeNames);
3238 + if (extraAttributes.size > 0 && props.suppressHydrationWarning !== true) {
3239 + warnForExtraAttributes(extraAttributes);
3240 }
3241 }
3242
packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js
+500 -133
@@ -39,13 +39,6 @@ import {
39 } from 'react-server/src/ReactServerStreamConfig';
40
41 import isAttributeNameSafe from '../shared/isAttributeNameSafe';
42 -import {
43 - getPropertyInfo,
44 - BOOLEAN,
45 - OVERLOADED_BOOLEAN,
46 - NUMERIC,
47 - POSITIVE_NUMERIC,
48 -} from '../shared/DOMProperty';
42 import isUnitlessNumber from '../shared/isUnitlessNumber';
43
44 import {checkControlledValueProps} from '../shared/ReactControlledValuePropTypes';
@@ -621,6 +614,26 @@ function pushBooleanAttribute(
614 }
615 }
616
617 +function pushStringAttribute(
618 + target: Array<Chunk | PrecomputedChunk>,
619 + name: string,
620 + value: string | boolean | number | Function | Object, // not null or undefined
621 +): void {
622 + if (
623 + typeof value !== 'function' &&
624 + typeof value !== 'symbol' &&
625 + typeof value !== 'boolean'
626 + ) {
627 + target.push(
628 + attributeSeparator,
629 + stringToChunk(name),
630 + attributeAssign,
631 + stringToChunk(escapeTextForBrowser(value)),
632 + attributeEnd,
633 + );
634 + }
635 +}
636 +
637 function pushAttribute(
638 target: Array<Chunk | PrecomputedChunk>,
639 name: string,
@@ -638,151 +651,505 @@ function pushAttribute(
651 case 'suppressHydrationWarning':
652 // Ignored. These are built-in to React on the client.
653 return;
654 + case 'autoFocus':
655 case 'multiple':
642 - case 'muted':
643 - pushBooleanAttribute(target, name, value);
656 + case 'muted': {
657 + pushBooleanAttribute(target, name.toLowerCase(), value);
658 return;
645 - }
646 - if (
647 - // shouldIgnoreAttribute
648 - // We have already filtered out null/undefined and reserved words.
649 - name.length > 2 &&
650 - (name[0] === 'o' || name[0] === 'O') &&
651 - (name[1] === 'n' || name[1] === 'N')
652 - ) {
653 - return;
654 - }
655 -
656 - const propertyInfo = getPropertyInfo(name);
657 - if (propertyInfo !== null) {
658 - // shouldRemoveAttribute
659 - switch (typeof value) {
660 - case 'function':
661 - case 'symbol': // eslint-disable-line
662 - return;
663 - case 'boolean': {
664 - if (!propertyInfo.acceptsBooleans) {
665 - return;
666 - }
667 - }
659 }
669 - if (enableFilterEmptyStringAttributesDOM) {
670 - if (propertyInfo.removeEmptyString && value === '') {
671 - if (__DEV__) {
672 - if (name === 'src') {
673 - console.error(
674 - 'An empty string ("") was passed to the %s attribute. ' +
675 - 'This may cause the browser to download the whole page again over the network. ' +
676 - 'To fix this, either do not render the element at all ' +
677 - 'or pass null to %s instead of an empty string.',
678 - name,
679 - name,
680 - );
681 - } else {
682 - console.error(
683 - 'An empty string ("") was passed to the %s attribute. ' +
684 - 'To fix this, either do not render the element at all ' +
685 - 'or pass null to %s instead of an empty string.',
686 - name,
687 - name,
688 - );
660 + case 'src':
661 + case 'href':
662 + case 'action':
663 + if (enableFilterEmptyStringAttributesDOM) {
664 + if (value === '') {
665 + if (__DEV__) {
666 + if (name === 'src') {
667 + console.error(
668 + 'An empty string ("") was passed to the %s attribute. ' +
669 + 'This may cause the browser to download the whole page again over the network. ' +
670 + 'To fix this, either do not render the element at all ' +
671 + 'or pass null to %s instead of an empty string.',
672 + name,
673 + name,
674 + );
675 + } else {
676 + console.error(
677 + 'An empty string ("") was passed to the %s attribute. ' +
678 + 'To fix this, either do not render the element at all ' +
679 + 'or pass null to %s instead of an empty string.',
680 + name,
681 + name,
682 + );
683 + }
684 }
685 + return;
686 }
687 + }
688 + // Fall through to the last case which shouldn't remove empty strings.
689 + // eslint-disable-next-line no-fallthrough
690 + case 'formAction': {
691 + if (
692 + value == null ||
693 + typeof value === 'function' ||
694 + typeof value === 'symbol' ||
695 + typeof value === 'boolean'
696 + ) {
697 return;
698 }
699 + if (__DEV__) {
700 + checkAttributeStringCoercion(value, name);
701 + }
702 + const sanitizedValue = sanitizeURL('' + value);
703 + target.push(
704 + attributeSeparator,
705 + stringToChunk(name),
706 + attributeAssign,
707 + stringToChunk(escapeTextForBrowser(sanitizedValue)),
708 + attributeEnd,
709 + );
710 + return;
711 }
694 -
695 - const attributeName = propertyInfo.attributeName;
696 - const attributeNameChunk = stringToChunk(attributeName); // TODO: If it's known we can cache the chunk.
697 -
698 - switch (propertyInfo.type) {
699 - case BOOLEAN:
700 - if (value) {
701 - target.push(
702 - attributeSeparator,
703 - attributeNameChunk,
704 - attributeEmptyString,
705 - );
706 - }
707 - return;
708 - case OVERLOADED_BOOLEAN:
709 - if (value === true) {
710 - target.push(
711 - attributeSeparator,
712 - attributeNameChunk,
713 - attributeEmptyString,
714 - );
715 - } else if (value === false) {
716 - // Ignored
717 - } else {
718 - target.push(
719 - attributeSeparator,
720 - attributeNameChunk,
721 - attributeAssign,
722 - stringToChunk(escapeTextForBrowser(value)),
723 - attributeEnd,
724 - );
725 - }
712 + case 'xlinkHref': {
713 + if (
714 + typeof value === 'function' ||
715 + typeof value === 'symbol' ||
716 + typeof value === 'boolean'
717 + ) {
718 return;
727 - case NUMERIC:
728 - if (!isNaN(value)) {
729 - target.push(
730 - attributeSeparator,
731 - attributeNameChunk,
732 - attributeAssign,
733 - stringToChunk(escapeTextForBrowser(value)),
734 - attributeEnd,
735 - );
736 - }
737 - break;
738 - case POSITIVE_NUMERIC:
739 - if (!isNaN(value) && (value: any) >= 1) {
740 - target.push(
741 - attributeSeparator,
742 - attributeNameChunk,
743 - attributeAssign,
744 - stringToChunk(escapeTextForBrowser(value)),
745 - attributeEnd,
746 - );
747 - }
748 - break;
749 - default:
750 - if (__DEV__) {
751 - checkAttributeStringCoercion(value, attributeName);
752 - }
753 - if (propertyInfo.sanitizeURL) {
754 - // We've already checked above.
755 - // eslint-disable-next-line react-internal/safe-string-coercion
756 - value = sanitizeURL('' + (value: any));
757 - }
719 + }
720 + if (__DEV__) {
721 + checkAttributeStringCoercion(value, name);
722 + }
723 + const sanitizedValue = sanitizeURL('' + value);
724 + target.push(
725 + attributeSeparator,
726 + stringToChunk('xlink:href'),
727 + attributeAssign,
728 + stringToChunk(escapeTextForBrowser(sanitizedValue)),
729 + attributeEnd,
730 + );
731 + return;
732 + }
733 + case 'contentEditable':
734 + case 'spellCheck':
735 + case 'draggable':
736 + case 'value':
737 + case 'autoReverse':
738 + case 'externalResourcesRequired':
739 + case 'focusable':
740 + case 'preserveAlpha': {
741 + // Booleanish String
742 + // These are "enumerated" attributes that accept "true" and "false".
743 + // In React, we let users pass `true` and `false` even though technically
744 + // these aren't boolean attributes (they are coerced to strings).
745 + if (typeof value !== 'function' && typeof value !== 'symbol') {
746 target.push(
747 attributeSeparator,
760 - attributeNameChunk,
748 + stringToChunk(name),
749 attributeAssign,
750 stringToChunk(escapeTextForBrowser(value)),
751 attributeEnd,
752 );
753 + }
754 + return;
755 + }
756 + case 'allowFullScreen':
757 + case 'async':
758 + case 'autoPlay':
759 + case 'controls':
760 + case 'default':
761 + case 'defer':
762 + case 'disabled':
763 + case 'disablePictureInPicture':
764 + case 'disableRemotePlayback':
765 + case 'formNoValidate':
766 + case 'hidden':
767 + case 'loop':
768 + case 'noModule':
769 + case 'noValidate':
770 + case 'open':
771 + case 'playsInline':
772 + case 'readOnly':
773 + case 'required':
774 + case 'reversed':
775 + case 'scoped':
776 + case 'seamless':
777 + case 'itemScope': {
778 + // Boolean
779 + if (value && typeof value !== 'function' && typeof value !== 'symbol') {
780 + target.push(
781 + attributeSeparator,
782 + stringToChunk(name),
783 + attributeEmptyString,
784 + );
785 + }
786 + return;
787 + }
788 + case 'capture':
789 + case 'download': {
790 + // Overloaded Boolean
791 + if (value === true) {
792 + target.push(
793 + attributeSeparator,
794 + stringToChunk(name),
795 + attributeEmptyString,
796 + );
797 + } else if (value === false) {
798 + // Ignored
799 + } else if (typeof value !== 'function' && typeof value !== 'symbol') {
800 + target.push(
801 + attributeSeparator,
802 + stringToChunk(name),
803 + attributeAssign,
804 + stringToChunk(escapeTextForBrowser(value)),
805 + attributeEnd,
806 + );
807 + }
808 + return;
809 + }
810 + case 'cols':
811 + case 'rows':
812 + case 'size':
813 + case 'span': {
814 + // These are HTML attributes that must be positive numbers.
815 + if (
816 + typeof value !== 'function' &&
817 + typeof value !== 'symbol' &&
818 + !isNaN(value) &&
819 + (value: any) >= 1
820 + ) {
821 + target.push(
822 + attributeSeparator,
823 + stringToChunk(name),
824 + attributeAssign,
825 + stringToChunk(escapeTextForBrowser(value)),
826 + attributeEnd,
827 + );
828 + }
829 + return;
830 + }
831 + case 'rowSpan':
832 + case 'start': {
833 + // These are HTML attributes that must be numbers.
834 + if (
835 + typeof value !== 'function' &&
836 + typeof value !== 'symbol' &&
837 + !isNaN(value)
838 + ) {
839 + target.push(
840 + attributeSeparator,
841 + stringToChunk(name),
842 + attributeAssign,
843 + stringToChunk(escapeTextForBrowser(value)),
844 + attributeEnd,
845 + );
846 + }
847 + return;
848 }
766 - } else if (isAttributeNameSafe(name)) {
767 - // shouldRemoveAttribute
768 - switch (typeof value) {
769 - case 'function':
770 - case 'symbol': // eslint-disable-line
849 + // A few React string attributes have a different name.
850 + // This is a mapping from React prop names to the attribute names.
851 + case 'acceptCharset':
852 + pushStringAttribute(target, 'accept-charset', value);
853 + return;
854 + case 'className':
855 + pushStringAttribute(target, 'class', value);
856 + return;
857 + case 'htmlFor':
858 + pushStringAttribute(target, 'for', value);
859 + return;
860 + case 'httpEquiv':
861 + pushStringAttribute(target, 'http-equiv', value);
862 + return;
863 + // HTML and SVG attributes, but the SVG attribute is case sensitive.
864 + case 'tabIndex':
865 + pushStringAttribute(target, 'tabindex', value);
866 + return;
867 + case 'crossOrigin':
868 + pushStringAttribute(target, 'crossorigin', value);
869 + return;
870 + // This is a list of all SVG attributes that need special casing.
871 + // Regular attributes that just accept strings.
872 + case 'accentHeight':
873 + pushStringAttribute(target, 'accent-height', value);
874 + return;
875 + case 'alignmentBaseline':
876 + pushStringAttribute(target, 'alignment-baseline', value);
877 + return;
878 + case 'arabicForm':
879 + pushStringAttribute(target, 'arabic-form', value);
880 + return;
881 + case 'baselineShift':
882 + pushStringAttribute(target, 'baseline-shift', value);
883 + return;
884 + case 'capHeight':
885 + pushStringAttribute(target, 'cap-height', value);
886 + return;
887 + case 'clipPath':
888 + pushStringAttribute(target, 'clip-path', value);
889 + return;
890 + case 'clipRule':
891 + pushStringAttribute(target, 'clip-rule', value);
892 + return;
893 + case 'colorInterpolation':
894 + pushStringAttribute(target, 'color-interpolation', value);
895 + return;
896 + case 'colorInterpolationFilters':
897 + pushStringAttribute(target, 'color-interpolation-filters', value);
898 + return;
899 + case 'colorProfile':
900 + pushStringAttribute(target, 'color-profile', value);
901 + return;
902 + case 'colorRendering':
903 + pushStringAttribute(target, 'color-rendering', value);
904 + return;
905 + case 'dominantBaseline':
906 + pushStringAttribute(target, 'dominant-baseline', value);
907 + return;
908 + case 'enableBackground':
909 + pushStringAttribute(target, 'enable-background', value);
910 + return;
911 + case 'fillOpacity':
912 + pushStringAttribute(target, 'fill-opacity', value);
913 + return;
914 + case 'fillRule':
915 + pushStringAttribute(target, 'fill-rule', value);
916 + return;
917 + case 'floodColor':
918 + pushStringAttribute(target, 'flood-color', value);
919 + return;
920 + case 'floodOpacity':
921 + pushStringAttribute(target, 'flood-opacity', value);
922 + return;
923 + case 'fontFamily':
924 + pushStringAttribute(target, 'font-family', value);
925 + return;
926 + case 'fontSize':
927 + pushStringAttribute(target, 'font-size', value);
928 + return;
929 + case 'fontSizeAdjust':
930 + pushStringAttribute(target, 'font-size-adjust', value);
931 + return;
932 + case 'fontStretch':
933 + pushStringAttribute(target, 'font-stretch', value);
934 + return;
935 + case 'fontStyle':
936 + pushStringAttribute(target, 'font-style', value);
937 + return;
938 + case 'fontVariant':
939 + pushStringAttribute(target, 'font-variant', value);
940 + return;
941 + case 'fontWeight':
942 + pushStringAttribute(target, 'font-weight', value);
943 + return;
944 + case 'glyphName':
945 + pushStringAttribute(target, 'glyph-name', value);
946 + return;
947 + case 'glyphOrientationHorizontal':
948 + pushStringAttribute(target, 'glyph-orientation-horizontal', value);
949 + return;
950 + case 'glyphOrientationVertical':
951 + pushStringAttribute(target, 'glyph-orientation-vertical', value);
952 + return;
953 + case 'horizAdvX':
954 + pushStringAttribute(target, 'horiz-adv-x', value);
955 + return;
956 + case 'horizOriginX':
957 + pushStringAttribute(target, 'horiz-origin-x', value);
958 + return;
959 + case 'imageRendering':
960 + pushStringAttribute(target, 'image-rendering', value);
961 + return;
962 + case 'letterSpacing':
963 + pushStringAttribute(target, 'letter-spacing', value);
964 + return;
965 + case 'lightingColor':
966 + pushStringAttribute(target, 'lighting-color', value);
967 + return;
968 + case 'markerEnd':
969 + pushStringAttribute(target, 'marker-end', value);
970 + return;
971 + case 'markerMid':
972 + pushStringAttribute(target, 'marker-mid', value);
973 + return;
974 + case 'markerStart':
975 + pushStringAttribute(target, 'marker-start', value);
976 + return;
977 + case 'overlinePosition':
978 + pushStringAttribute(target, 'overline-position', value);
979 + return;
980 + case 'overlineThickness':
981 + pushStringAttribute(target, 'overline-thickness', value);
982 + return;
983 + case 'paintOrder':
984 + pushStringAttribute(target, 'paint-order', value);
985 + return;
986 + case 'panose-1':
987 + pushStringAttribute(target, 'panose-1', value);
988 + return;
989 + case 'pointerEvents':
990 + pushStringAttribute(target, 'pointer-events', value);
991 + return;
992 + case 'renderingIntent':
993 + pushStringAttribute(target, 'rendering-intent', value);
994 + return;
995 + case 'shapeRendering':
996 + pushStringAttribute(target, 'shape-rendering', value);
997 + return;
998 + case 'stopColor':
999 + pushStringAttribute(target, 'stop-color', value);
1000 + return;
1001 + case 'stopOpacity':
1002 + pushStringAttribute(target, 'stop-opacity', value);
1003 + return;
1004 + case 'strikethroughPosition':
1005 + pushStringAttribute(target, 'strikethrough-position', value);
1006 + return;
1007 + case 'strikethroughThickness':
1008 + pushStringAttribute(target, 'strikethrough-thickness', value);
1009 + return;
1010 + case 'strokeDasharray':
1011 + pushStringAttribute(target, 'stroke-dasharray', value);
1012 + return;
1013 + case 'strokeDashoffset':
1014 + pushStringAttribute(target, 'stroke-dashoffset', value);
1015 + return;
1016 + case 'strokeLinecap':
1017 + pushStringAttribute(target, 'stroke-linecap', value);
1018 + return;
1019 + case 'strokeLinejoin':
1020 + pushStringAttribute(target, 'stroke-linejoin', value);
1021 + return;
1022 + case 'strokeMiterlimit':
1023 + pushStringAttribute(target, 'stroke-miterlimit', value);
1024 + return;
1025 + case 'strokeOpacity':
1026 + pushStringAttribute(target, 'stroke-opacity', value);
1027 + return;
1028 + case 'strokeWidth':
1029 + pushStringAttribute(target, 'stroke-width', value);
1030 + return;
1031 + case 'textAnchor':
1032 + pushStringAttribute(target, 'text-anchor', value);
1033 + return;
1034 + case 'textDecoration':
1035 + pushStringAttribute(target, 'text-decoration', value);
1036 + return;
1037 + case 'textRendering':
1038 + pushStringAttribute(target, 'text-rendering', value);
1039 + return;
1040 + case 'transformOrigin':
1041 + pushStringAttribute(target, 'transform-origin', value);
1042 + return;
1043 + case 'underlinePosition':
1044 + pushStringAttribute(target, 'underline-position', value);
1045 + return;
1046 + case 'underlineThickness':
1047 + pushStringAttribute(target, 'underline-thickness', value);
1048 + return;
1049 + case 'unicodeBidi':
1050 + pushStringAttribute(target, 'unicode-bidi', value);
1051 + return;
1052 + case 'unicodeRange':
1053 + pushStringAttribute(target, 'unicode-range', value);
1054 + return;
1055 + case 'unitsPerEm':
1056 + pushStringAttribute(target, 'units-per-em', value);
1057 + return;
1058 + case 'vAlphabetic':
1059 + pushStringAttribute(target, 'v-alphabetic', value);
1060 + return;
1061 + case 'vHanging':
1062 + pushStringAttribute(target, 'v-hanging', value);
1063 + return;
1064 + case 'vIdeographic':
1065 + pushStringAttribute(target, 'v-ideographic', value);
1066 + return;
1067 + case 'vMathematical':
1068 + pushStringAttribute(target, 'v-mathematical', value);
1069 + return;
1070 + case 'vectorEffect':
1071 + pushStringAttribute(target, 'vector-effect', value);
1072 + return;
1073 + case 'vertAdvY':
1074 + pushStringAttribute(target, 'vert-adv-y', value);
1075 + return;
1076 + case 'vertOriginX':
1077 + pushStringAttribute(target, 'vert-origin-x', value);
1078 + return;
1079 + case 'vertOriginY':
1080 + pushStringAttribute(target, 'vert-origin-y', value);
1081 + return;
1082 + case 'wordSpacing':
1083 + pushStringAttribute(target, 'word-spacing', value);
1084 + return;
1085 + case 'writingMode':
1086 + pushStringAttribute(target, 'writing-mode', value);
1087 + return;
1088 + case 'xmlnsXlink':
1089 + pushStringAttribute(target, 'xmlns:xlink', value);
1090 + return;
1091 + case 'xHeight':
1092 + pushStringAttribute(target, 'x-height', value);
1093 + return;
1094 + case 'xlinkActuate':
1095 + pushStringAttribute(target, 'xlink:actuate', value);
1096 + break;
1097 + case 'xlinkArcrole':
1098 + pushStringAttribute(target, 'xlink:arcrole', value);
1099 + break;
1100 + case 'xlinkRole':
1101 + pushStringAttribute(target, 'xlink:role', value);
1102 + break;
1103 + case 'xlinkShow':
1104 + pushStringAttribute(target, 'xlink:show', value);
1105 + break;
1106 + case 'xlinkTitle':
1107 + pushStringAttribute(target, 'xlink:title', value);
1108 + break;
1109 + case 'xlinkType':
1110 + pushStringAttribute(target, 'xlink:type', value);
1111 + break;
1112 + case 'xmlBase':
1113 + pushStringAttribute(target, 'xml:base', value);
1114 + break;
1115 + case 'xmlLang':
1116 + pushStringAttribute(target, 'xml:lang', value);
1117 + break;
1118 + case 'xmlSpace':
1119 + pushStringAttribute(target, 'xml:space', value);
1120 + break;
1121 + default:
1122 + if (
1123 + // shouldIgnoreAttribute
1124 + // We have already filtered out null/undefined and reserved words.
1125 + name.length > 2 &&
1126 + (name[0] === 'o' || name[0] === 'O') &&
1127 + (name[1] === 'n' || name[1] === 'N')
1128 + ) {
1129 return;
772 - case 'boolean': {
773 - const prefix = name.toLowerCase().slice(0, 5);
774 - if (prefix !== 'data-' && prefix !== 'aria-') {
775 - return;
1130 + }
1131 +
1132 + if (isAttributeNameSafe(name)) {
1133 + // shouldRemoveAttribute
1134 + switch (typeof value) {
1135 + case 'function':
1136 + case 'symbol': // eslint-disable-line
1137 + return;
1138 + case 'boolean': {
1139 + const prefix = name.toLowerCase().slice(0, 5);
1140 + if (prefix !== 'data-' && prefix !== 'aria-') {
1141 + return;
1142 + }
1143 + }
1144 }
1145 + target.push(
1146 + attributeSeparator,
1147 + stringToChunk(name),
1148 + attributeAssign,
1149 + stringToChunk(escapeTextForBrowser(value)),
1150 + attributeEnd,
1151 + );
1152 }
778 - }
779 - target.push(
780 - attributeSeparator,
781 - stringToChunk(name),
782 - attributeAssign,
783 - stringToChunk(escapeTextForBrowser(value)),
784 - attributeEnd,
785 - );
1153 }
1154 }
1155
packages/react-dom-bindings/src/shared/DOMProperty.js deleted
-411
@@ -1,411 +0,0 @@
1 -/**
2 - * Copyright (c) Meta Platforms, Inc. and affiliates.
3 - *
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 - * @flow
8 - */
9 -
10 -type PropertyType = 0 | 1 | 2 | 3 | 4 | 5 | 6;
11 -
12 -// A simple string attribute.
13 -// Attributes that aren't in the filter are presumed to have this type.
14 -export const STRING = 1;
15 -
16 -// A string attribute that accepts booleans in React. In HTML, these are called
17 -// "enumerated" attributes with "true" and "false" as possible values.
18 -// When true, it should be set to a "true" string.
19 -// When false, it should be set to a "false" string.
20 -export const BOOLEANISH_STRING = 2;
21 -
22 -// A real boolean attribute.
23 -// When true, it should be present (set either to an empty string or its name).
24 -// When false, it should be omitted.
25 -export const BOOLEAN = 3;
26 -
27 -// An attribute that can be used as a flag as well as with a value.
28 -// When true, it should be present (set either to an empty string or its name).
29 -// When false, it should be omitted.
30 -// For any other value, should be present with that value.
31 -export const OVERLOADED_BOOLEAN = 4;
32 -
33 -// An attribute that must be numeric or parse as a numeric.
34 -// When falsy, it should be removed.
35 -export const NUMERIC = 5;
36 -
37 -// An attribute that must be positive numeric or parse as a positive numeric.
38 -// When falsy, it should be removed.
39 -export const POSITIVE_NUMERIC = 6;
40 -
41 -export type PropertyInfo = {
42 - +acceptsBooleans: boolean,
43 - +attributeName: string,
44 - +attributeNamespace: string | null,
45 - +type: PropertyType,
46 - +sanitizeURL: boolean,
47 - +removeEmptyString: boolean,
48 -};
49 -
50 -export function getPropertyInfo(name: string): PropertyInfo | null {
51 - return properties.hasOwnProperty(name) ? properties[name] : null;
52 -}
53 -
54 -// $FlowFixMe[missing-this-annot]
55 -function PropertyInfoRecord(
56 - type: PropertyType,
57 - attributeName: string,
58 - attributeNamespace: string | null,
59 - sanitizeURL: boolean,
60 - removeEmptyString: boolean,
61 -) {
62 - this.acceptsBooleans =
63 - type === BOOLEANISH_STRING ||
64 - type === BOOLEAN ||
65 - type === OVERLOADED_BOOLEAN;
66 - this.attributeName = attributeName;
67 - this.attributeNamespace = attributeNamespace;
68 - this.type = type;
69 - this.sanitizeURL = sanitizeURL;
70 - this.removeEmptyString = removeEmptyString;
71 -}
72 -
73 -// When adding attributes to this list, be sure to also add them to
74 -// the `possibleStandardNames` module to ensure casing and incorrect
75 -// name warnings.
76 -const properties: {[string]: $FlowFixMe} = {};
77 -
78 -// A few React string attributes have a different name.
79 -// This is a mapping from React prop names to the attribute names.
80 -[
81 - ['acceptCharset', 'accept-charset'],
82 - ['className', 'class'],
83 - ['htmlFor', 'for'],
84 - ['httpEquiv', 'http-equiv'],
85 -].forEach(([name, attributeName]) => {
86 - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
87 - properties[name] = new PropertyInfoRecord(
88 - STRING,
89 - attributeName, // attributeName
90 - null, // attributeNamespace
91 - false, // sanitizeURL
92 - false, // removeEmptyString
93 - );
94 -});
95 -
96 -// These are "enumerated" HTML attributes that accept "true" and "false".
97 -// In React, we let users pass `true` and `false` even though technically
98 -// these aren't boolean attributes (they are coerced to strings).
99 -['contentEditable', 'draggable', 'spellCheck', 'value'].forEach(name => {
100 - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
101 - properties[name] = new PropertyInfoRecord(
102 - BOOLEANISH_STRING,
103 - name.toLowerCase(), // attributeName
104 - null, // attributeNamespace
105 - false, // sanitizeURL
106 - false, // removeEmptyString
107 - );
108 -});
109 -
110 -// These are "enumerated" SVG attributes that accept "true" and "false".
111 -// In React, we let users pass `true` and `false` even though technically
112 -// these aren't boolean attributes (they are coerced to strings).
113 -// Since these are SVG attributes, their attribute names are case-sensitive.
114 -[
115 - 'autoReverse',
116 - 'externalResourcesRequired',
117 - 'focusable',
118 - 'preserveAlpha',
119 -].forEach(name => {
120 - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
121 - properties[name] = new PropertyInfoRecord(
122 - BOOLEANISH_STRING,
123 - name, // attributeName
124 - null, // attributeNamespace
125 - false, // sanitizeURL
126 - false, // removeEmptyString
127 - );
128 -});
129 -
130 -// These are HTML boolean attributes.
131 -[
132 - 'allowFullScreen',
133 - 'async',
134 - // Note: there is a special case that prevents it from being written to the DOM
135 - // on the client side because the browsers are inconsistent. Instead we call focus().
136 - 'autoFocus',
137 - 'autoPlay',
138 - 'controls',
139 - 'default',
140 - 'defer',
141 - 'disabled',
142 - 'disablePictureInPicture',
143 - 'disableRemotePlayback',
144 - 'formNoValidate',
145 - 'hidden',
146 - 'loop',
147 - 'noModule',
148 - 'noValidate',
149 - 'open',
150 - 'playsInline',
151 - 'readOnly',
152 - 'required',
153 - 'reversed',
154 - 'scoped',
155 - 'seamless',
156 - // Microdata
157 - 'itemScope',
158 -].forEach(name => {
159 - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
160 - properties[name] = new PropertyInfoRecord(
161 - BOOLEAN,
162 - name.toLowerCase(), // attributeName
163 - null, // attributeNamespace
164 - false, // sanitizeURL
165 - false, // removeEmptyString
166 - );
167 -});
168 -
169 -// These are HTML attributes that are "overloaded booleans": they behave like
170 -// booleans, but can also accept a string value.
171 -[
172 - 'capture',
173 - 'download',
174 -
175 - // NOTE: if you add a camelCased prop to this list,
176 - // you'll need to set attributeName to name.toLowerCase()
177 - // instead in the assignment below.
178 -].forEach(name => {
179 - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
180 - properties[name] = new PropertyInfoRecord(
181 - OVERLOADED_BOOLEAN,
182 - name, // attributeName
183 - null, // attributeNamespace
184 - false, // sanitizeURL
185 - false, // removeEmptyString
186 - );
187 -});
188 -
189 -// These are HTML attributes that must be positive numbers.
190 -[
191 - 'cols',
192 - 'rows',
193 - 'size',
194 - 'span',
195 -
196 - // NOTE: if you add a camelCased prop to this list,
197 - // you'll need to set attributeName to name.toLowerCase()
198 - // instead in the assignment below.
199 -].forEach(name => {
200 - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
201 - properties[name] = new PropertyInfoRecord(
202 - POSITIVE_NUMERIC,
203 - name, // attributeName
204 - null, // attributeNamespace
205 - false, // sanitizeURL
206 - false, // removeEmptyString
207 - );
208 -});
209 -
210 -// These are HTML attributes that must be numbers.
211 -['rowSpan', 'start'].forEach(name => {
212 - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
213 - properties[name] = new PropertyInfoRecord(
214 - NUMERIC,
215 - name.toLowerCase(), // attributeName
216 - null, // attributeNamespace
217 - false, // sanitizeURL
218 - false, // removeEmptyString
219 - );
220 -});
221 -
222 -const CAMELIZE = /[\-\:]([a-z])/g;
223 -const capitalize = (token: string) => token[1].toUpperCase();
224 -
225 -// This is a list of all SVG attributes that need special casing, namespacing,
226 -// or boolean value assignment. Regular attributes that just accept strings
227 -// and have the same names are omitted, just like in the HTML attribute filter.
228 -// Some of these attributes can be hard to find. This list was created by
229 -// scraping the MDN documentation.
230 -[
231 - 'accent-height',
232 - 'alignment-baseline',
233 - 'arabic-form',
234 - 'baseline-shift',
235 - 'cap-height',
236 - 'clip-path',
237 - 'clip-rule',
238 - 'color-interpolation',
239 - 'color-interpolation-filters',
240 - 'color-profile',
241 - 'color-rendering',
242 - 'dominant-baseline',
243 - 'enable-background',
244 - 'fill-opacity',
245 - 'fill-rule',
246 - 'flood-color',
247 - 'flood-opacity',
248 - 'font-family',
249 - 'font-size',
250 - 'font-size-adjust',
251 - 'font-stretch',
252 - 'font-style',
253 - 'font-variant',
254 - 'font-weight',
255 - 'glyph-name',
256 - 'glyph-orientation-horizontal',
257 - 'glyph-orientation-vertical',
258 - 'horiz-adv-x',
259 - 'horiz-origin-x',
260 - 'image-rendering',
261 - 'letter-spacing',
262 - 'lighting-color',
263 - 'marker-end',
264 - 'marker-mid',
265 - 'marker-start',
266 - 'overline-position',
267 - 'overline-thickness',
268 - 'paint-order',
269 - 'panose-1',
270 - 'pointer-events',
271 - 'rendering-intent',
272 - 'shape-rendering',
273 - 'stop-color',
274 - 'stop-opacity',
275 - 'strikethrough-position',
276 - 'strikethrough-thickness',
277 - 'stroke-dasharray',
278 - 'stroke-dashoffset',
279 - 'stroke-linecap',
280 - 'stroke-linejoin',
281 - 'stroke-miterlimit',
282 - 'stroke-opacity',
283 - 'stroke-width',
284 - 'text-anchor',
285 - 'text-decoration',
286 - 'text-rendering',
287 - 'transform-origin',
288 - 'underline-position',
289 - 'underline-thickness',
290 - 'unicode-bidi',
291 - 'unicode-range',
292 - 'units-per-em',
293 - 'v-alphabetic',
294 - 'v-hanging',
295 - 'v-ideographic',
296 - 'v-mathematical',
297 - 'vector-effect',
298 - 'vert-adv-y',
299 - 'vert-origin-x',
300 - 'vert-origin-y',
301 - 'word-spacing',
302 - 'writing-mode',
303 - 'xmlns:xlink',
304 - 'x-height',
305 -
306 - // NOTE: if you add a camelCased prop to this list,
307 - // you'll need to set attributeName to name.toLowerCase()
308 - // instead in the assignment below.
309 -].forEach(attributeName => {
310 - const name = attributeName.replace(CAMELIZE, capitalize);
311 - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
312 - properties[name] = new PropertyInfoRecord(
313 - STRING,
314 - attributeName,
315 - null, // attributeNamespace
316 - false, // sanitizeURL
317 - false, // removeEmptyString
318 - );
319 -});
320 -
321 -// String SVG attributes with the xlink namespace.
322 -[
323 - 'xlink:actuate',
324 - 'xlink:arcrole',
325 - 'xlink:role',
326 - 'xlink:show',
327 - 'xlink:title',
328 - 'xlink:type',
329 -
330 - // NOTE: if you add a camelCased prop to this list,
331 - // you'll need to set attributeName to name.toLowerCase()
332 - // instead in the assignment below.
333 -].forEach(attributeName => {
334 - const name = attributeName.replace(CAMELIZE, capitalize);
335 - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
336 - properties[name] = new PropertyInfoRecord(
337 - STRING,
338 - attributeName,
339 - 'http://www.w3.org/1999/xlink',
340 - false, // sanitizeURL
341 - false, // removeEmptyString
342 - );
343 -});
344 -
345 -// String SVG attributes with the xml namespace.
346 -[
347 - 'xml:base',
348 - 'xml:lang',
349 - 'xml:space',
350 -
351 - // NOTE: if you add a camelCased prop to this list,
352 - // you'll need to set attributeName to name.toLowerCase()
353 - // instead in the assignment below.
354 -].forEach(attributeName => {
355 - const name = attributeName.replace(CAMELIZE, capitalize);
356 - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
357 - properties[name] = new PropertyInfoRecord(
358 - STRING,
359 - attributeName,
360 - 'http://www.w3.org/XML/1998/namespace',
361 - false, // sanitizeURL
362 - false, // removeEmptyString
363 - );
364 -});
365 -
366 -// These attribute exists both in HTML and SVG.
367 -// The attribute name is case-sensitive in SVG so we can't just use
368 -// the React name like we do for attributes that exist only in HTML.
369 -['tabIndex', 'crossOrigin'].forEach(attributeName => {
370 - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
371 - properties[attributeName] = new PropertyInfoRecord(
372 - STRING,
373 - attributeName.toLowerCase(), // attributeName
374 - null, // attributeNamespace
375 - false, // sanitizeURL
376 - false, // removeEmptyString
377 - );
378 -});
379 -
380 -// These attributes accept URLs. These must not allow javascript: URLS.
381 -// These will also need to accept Trusted Types object in the future.
382 -const xlinkHref = 'xlinkHref';
383 -// $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
384 -properties[xlinkHref] = new PropertyInfoRecord(
385 - STRING,
386 - 'xlink:href',
387 - 'http://www.w3.org/1999/xlink',
388 - true, // sanitizeURL
389 - false, // removeEmptyString
390 -);
391 -
392 -const formAction = 'formAction';
393 -// $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
394 -properties[formAction] = new PropertyInfoRecord(
395 - STRING,
396 - 'formaction', // attributeName
397 - null, // attributeNamespace
398 - true, // sanitizeURL
399 - false, // removeEmptyString
400 -);
401 -
402 -['src', 'href', 'action'].forEach(attributeName => {
403 - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
404 - properties[attributeName] = new PropertyInfoRecord(
405 - STRING,
406 - attributeName.toLowerCase(), // attributeName
407 - null, // attributeNamespace
408 - true, // sanitizeURL
409 - true, // removeEmptyString
410 - );
411 -});
packages/react-dom-bindings/src/shared/ReactDOMUnknownPropertyHook.js
+61 -15
@@ -5,7 +5,6 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 -import {BOOLEAN, getPropertyInfo} from './DOMProperty';
8 import {ATTRIBUTE_NAME_CHAR} from './isAttributeNameSafe';
9 import isCustomElement from './isCustomElement';
10 import possibleStandardNames from './possibleStandardNames';
@@ -131,8 +130,6 @@ function validateProperty(tagName, name, value, eventRegistry) {
130 return true;
131 }
132
134 - const propertyInfo = getPropertyInfo(name);
135 -
133 // Known attributes should match the casing specified in the property config.
134 if (possibleStandardNames.hasOwnProperty(lowerCasedName)) {
135 const standardName = possibleStandardNames[lowerCasedName];
@@ -184,20 +181,49 @@ function validateProperty(tagName, name, value, eventRegistry) {
181 switch (typeof value) {
182 case 'boolean': {
183 switch (name) {
184 + case 'autoFocus':
185 case 'checked':
188 - case 'selected':
186 case 'multiple':
190 - case 'muted': {
187 + case 'muted':
188 + case 'selected':
189 + case 'contentEditable':
190 + case 'spellCheck':
191 + case 'draggable':
192 + case 'value':
193 + case 'autoReverse':
194 + case 'externalResourcesRequired':
195 + case 'focusable':
196 + case 'preserveAlpha':
197 + case 'allowFullScreen':
198 + case 'async':
199 + case 'autoPlay':
200 + case 'controls':
201 + case 'default':
202 + case 'defer':
203 + case 'disabled':
204 + case 'disablePictureInPicture':
205 + case 'disableRemotePlayback':
206 + case 'formNoValidate':
207 + case 'hidden':
208 + case 'loop':
209 + case 'noModule':
210 + case 'noValidate':
211 + case 'open':
212 + case 'playsInline':
213 + case 'readOnly':
214 + case 'required':
215 + case 'reversed':
216 + case 'scoped':
217 + case 'seamless':
218 + case 'itemScope':
219 + case 'capture':
220 + case 'download': {
221 // Boolean properties can accept boolean values
222 return true;
223 }
224 default: {
195 - if (propertyInfo === null) {
196 - const prefix = name.toLowerCase().slice(0, 5);
197 - if (prefix === 'data-' || prefix === 'aria-') {
198 - return true;
199 - }
200 - } else if (propertyInfo.acceptsBooleans) {
225 + const prefix = name.toLowerCase().slice(0, 5);
226 + if (prefix === 'data-' || prefix === 'aria-') {
227 return true;
228 }
229 if (value) {
@@ -244,13 +270,33 @@ function validateProperty(tagName, name, value, eventRegistry) {
270 case 'checked':
271 case 'selected':
272 case 'multiple':
247 - case 'muted': {
273 + case 'muted':
274 + case 'allowFullScreen':
275 + case 'async':
276 + case 'autoPlay':
277 + case 'controls':
278 + case 'default':
279 + case 'defer':
280 + case 'disabled':
281 + case 'disablePictureInPicture':
282 + case 'disableRemotePlayback':
283 + case 'formNoValidate':
284 + case 'hidden':
285 + case 'loop':
286 + case 'noModule':
287 + case 'noValidate':
288 + case 'open':
289 + case 'playsInline':
290 + case 'readOnly':
291 + case 'required':
292 + case 'reversed':
293 + case 'scoped':
294 + case 'seamless':
295 + case 'itemScope': {
296 break;
297 }
298 default: {
251 - if (propertyInfo === null || propertyInfo.type !== BOOLEAN) {
252 - return true;
253 - }
299 + return true;
300 }
301 }
302 console.error(
packages/react-dom/src/__tests__/ReactDOMServerIntegrationUntrustedURL-test.js
+1 -1
@@ -339,7 +339,7 @@ describe('ReactDOMServerIntegration - Untrusted URLs - disableJavaScriptURLs', (
339 // The hydration validation calls it one extra time.
340 // TODO: It would be good if we only called toString once for
341 // consistency but the code structure makes that hard right now.
342 - expectedToStringCalls = 5;
342 + expectedToStringCalls = 4;
343 } else if (__DEV__) {
344 // Checking for string coercion problems results in double the
345 // toString calls in DEV