main
js 3,430 lines 103 KB
Raw
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 import type {HostContext, HostContextDev} from './ReactFiberConfigDOM';
11
12 import {HostContextNamespaceNone} from './ReactFiberConfigDOM';
13
14 import {
15 registrationNameDependencies,
16 possibleRegistrationNames,
17 } from '../events/EventRegistry';
18
19 import {checkHtmlStringCoercion} from 'shared/CheckStringCoercion';
20 import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
21 import {checkControlledValueProps} from '../shared/ReactControlledValuePropTypes';
22
23 import {
24 getValueForAttribute,
25 getValueForAttributeOnCustomComponent,
26 setValueForPropertyOnCustomComponent,
27 setValueForKnownAttribute,
28 setValueForAttribute,
29 setValueForNamespacedAttribute,
30 } from './DOMPropertyOperations';
31 import {
32 validateInputProps,
33 initInput,
34 updateInput,
35 restoreControlledInputState,
36 } from './ReactDOMInput';
37 import {validateOptionProps} from './ReactDOMOption';
38 import {
39 validateSelectProps,
40 initSelect,
41 restoreControlledSelectState,
42 updateSelect,
43 } from './ReactDOMSelect';
44 import {
45 validateTextareaProps,
46 initTextarea,
47 updateTextarea,
48 restoreControlledTextareaState,
49 } from './ReactDOMTextarea';
50 import {setSrcObject} from './ReactDOMSrcObject';
51 import {validateTextNesting} from './validateDOMNesting';
52 import setTextContent from './setTextContent';
53 import {
54 createDangerousStringForStyles,
55 setValueForStyles,
56 } from './CSSPropertyOperations';
57 import {SVG_NAMESPACE, MATH_NAMESPACE} from './DOMNamespaces';
58 import isCustomElement from '../shared/isCustomElement';
59 import getAttributeAlias from '../shared/getAttributeAlias';
60 import possibleStandardNames from '../shared/possibleStandardNames';
61 import {validateProperties as validateARIAProperties} from '../shared/ReactDOMInvalidARIAHook';
62 import {validateProperties as validateInputProperties} from '../shared/ReactDOMNullInputValuePropHook';
63 import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook';
64 import sanitizeURL from '../shared/sanitizeURL';
65
66 import noop from 'shared/noop';
67
68 import {trackHostMutation} from 'react-reconciler/src/ReactFiberMutationTracking';
69
70 import {
71 enableHydrationChangeEvent,
72 enableScrollEndPolyfill,
73 enableSrcObject,
74 enableTrustedTypesIntegration,
75 enableViewTransition,
76 enableViewTransitionParentEnterExit,
77 } from 'shared/ReactFeatureFlags';
78 import {
79 mediaEventTypes,
80 listenToNonDelegatedEvent,
81 } from '../events/DOMPluginEventSystem';
82
83 let didWarnControlledToUncontrolled = false;
84 let didWarnUncontrolledToControlled = false;
85 let didWarnFormActionType = false;
86 let didWarnFormActionName = false;
87 let didWarnFormActionTarget = false;
88 let didWarnFormActionMethod = false;
89 let didWarnForNewBooleanPropsWithEmptyValue: {[string]: boolean};
90 let didWarnPopoverTargetObject = false;
91 if (__DEV__) {
92 didWarnForNewBooleanPropsWithEmptyValue = {};
93 }
94
95 function validatePropertiesInDevelopment(type: string, props: any) {
96 if (__DEV__) {
97 validateARIAProperties(type, props);
98 validateInputProperties(type, props);
99 validateUnknownProperties(type, props, {
100 registrationNameDependencies,
101 possibleRegistrationNames,
102 });
103 if (
104 props.contentEditable &&
105 !props.suppressContentEditableWarning &&
106 props.children != null
107 ) {
108 console.error(
109 'A component is `contentEditable` and contains `children` managed by ' +
110 'React. It is now your responsibility to guarantee that none of ' +
111 'those nodes are unexpectedly modified or duplicated. This is ' +
112 'probably not intentional.',
113 );
114 }
115 }
116 }
117
118 function validateFormActionInDevelopment(
119 tag: string,
120 key: string,
121 value: mixed,
122 props: any,
123 ) {
124 if (__DEV__) {
125 if (value == null) {
126 return;
127 }
128 if (tag === 'form') {
129 if (key === 'formAction') {
130 console.error(
131 'You can only pass the formAction prop to <input> or <button>. Use the action prop on <form>.',
132 );
133 } else if (typeof value === 'function') {
134 if (
135 (props.encType != null || props.method != null) &&
136 !didWarnFormActionMethod
137 ) {
138 didWarnFormActionMethod = true;
139 console.error(
140 'Cannot specify a encType or method for a form that specifies a ' +
141 'function as the action. React provides those automatically. ' +
142 'They will get overridden.',
143 );
144 }
145 if (props.target != null && !didWarnFormActionTarget) {
146 didWarnFormActionTarget = true;
147 console.error(
148 'Cannot specify a target for a form that specifies a function as the action. ' +
149 'The function will always be executed in the same window.',
150 );
151 }
152 }
153 } else if (tag === 'input' || tag === 'button') {
154 if (key === 'action') {
155 console.error(
156 'You can only pass the action prop to <form>. Use the formAction prop on <input> or <button>.',
157 );
158 } else if (
159 tag === 'input' &&
160 props.type !== 'submit' &&
161 props.type !== 'image' &&
162 !didWarnFormActionType
163 ) {
164 didWarnFormActionType = true;
165 console.error(
166 'An input can only specify a formAction along with type="submit" or type="image".',
167 );
168 } else if (
169 tag === 'button' &&
170 props.type != null &&
171 props.type !== 'submit' &&
172 !didWarnFormActionType
173 ) {
174 didWarnFormActionType = true;
175 console.error(
176 'A button can only specify a formAction along with type="submit" or no type.',
177 );
178 } else if (typeof value === 'function') {
179 // Function form actions cannot control the form properties
180 if (props.name != null && !didWarnFormActionName) {
181 didWarnFormActionName = true;
182 console.error(
183 'Cannot specify a "name" prop for a button that specifies a function as a formAction. ' +
184 'React needs it to encode which action should be invoked. It will get overridden.',
185 );
186 }
187 if (
188 (props.formEncType != null || props.formMethod != null) &&
189 !didWarnFormActionMethod
190 ) {
191 didWarnFormActionMethod = true;
192 console.error(
193 'Cannot specify a formEncType or formMethod for a button that specifies a ' +
194 'function as a formAction. React provides those automatically. They will get overridden.',
195 );
196 }
197 if (props.formTarget != null && !didWarnFormActionTarget) {
198 didWarnFormActionTarget = true;
199 console.error(
200 'Cannot specify a formTarget for a button that specifies a function as a formAction. ' +
201 'The function will always be executed in the same window.',
202 );
203 }
204 }
205 } else {
206 if (key === 'action') {
207 console.error('You can only pass the action prop to <form>.');
208 } else {
209 console.error(
210 'You can only pass the formAction prop to <input> or <button>.',
211 );
212 }
213 }
214 }
215 }
216
217 function warnForPropDifference(
218 propName: string,
219 serverValue: mixed,
220 clientValue: mixed,
221 serverDifferences: {[propName: string]: mixed},
222 ): void {
223 if (__DEV__) {
224 if (serverValue === clientValue) {
225 return;
226 }
227 const normalizedClientValue =
228 normalizeMarkupForTextOrAttribute(clientValue);
229 const normalizedServerValue =
230 normalizeMarkupForTextOrAttribute(serverValue);
231 if (normalizedServerValue === normalizedClientValue) {
232 return;
233 }
234
235 serverDifferences[propName] = serverValue;
236 }
237 }
238
239 function hasViewTransition(htmlElement: HTMLElement): boolean {
240 return !!(
241 htmlElement.getAttribute('vt-share') ||
242 htmlElement.getAttribute('vt-exit') ||
243 htmlElement.getAttribute('vt-enter') ||
244 htmlElement.getAttribute('vt-update') ||
245 (enableViewTransitionParentEnterExit &&
246 (htmlElement.getAttribute('vt-parent-enter') ||
247 htmlElement.getAttribute('vt-parent-exit')))
248 );
249 }
250
251 function isExpectedViewTransitionName(htmlElement: HTMLElement): boolean {
252 if (!hasViewTransition(htmlElement)) {
253 // We didn't expect to see a view transition name applied.
254 return false;
255 }
256 const expectedVtName = htmlElement.getAttribute('vt-name');
257 const actualVtName: string = (htmlElement.style as any)[
258 'view-transition-name'
259 ];
260 if (expectedVtName) {
261 return expectedVtName === actualVtName;
262 } else {
263 // Auto-generated name.
264 // TODO: If Fizz starts applying a prefix to this name, we need to consider that.
265 return actualVtName.startsWith('_T_');
266 }
267 }
268
269 function warnForExtraAttributes(
270 domElement: Element,
271 attributeNames: Set<string>,
272 serverDifferences: {[propName: string]: mixed},
273 ) {
274 if (__DEV__) {
275 attributeNames.forEach(function (attributeName) {
276 if (attributeName === 'style') {
277 if (domElement.getAttribute(attributeName) === '') {
278 // Skip empty style. It's fine.
279 return;
280 }
281 const htmlElement = domElement as any as HTMLElement;
282 const style = htmlElement.style;
283 const isOnlyVTStyles =
284 (style.length === 1 && style[0] === 'view-transition-name') ||
285 (style.length === 2 &&
286 style[0] === 'view-transition-class' &&
287 style[1] === 'view-transition-name');
288 if (isOnlyVTStyles && isExpectedViewTransitionName(htmlElement)) {
289 // If the only extra style was the view-transition-name that we applied from the Fizz
290 // runtime, then we should ignore it.
291 } else {
292 serverDifferences.style = getStylesObjectFromElement(domElement);
293 }
294 } else {
295 serverDifferences[getPropNameFromAttributeName(attributeName)] =
296 domElement.getAttribute(attributeName);
297 }
298 });
299 }
300 }
301
302 function warnForInvalidEventListener(registrationName: string, listener: any) {
303 if (__DEV__) {
304 if (listener === false) {
305 console.error(
306 'Expected `%s` listener to be a function, instead got `false`.\n\n' +
307 'If you used to conditionally omit it with %s={condition && value}, ' +
308 'pass %s={condition ? value : undefined} instead.',
309 registrationName,
310 registrationName,
311 registrationName,
312 );
313 } else {
314 console.error(
315 'Expected `%s` listener to be a function, instead got a value of `%s` type.',
316 registrationName,
317 typeof listener,
318 );
319 }
320 }
321 }
322
323 // Parse the HTML and read it back to normalize the HTML string so that it
324 // can be used for comparison.
325 function normalizeHTML(parent: Element, html: string) {
326 if (__DEV__) {
327 // We could have created a separate document here to avoid
328 // re-initializing custom elements if they exist. But this breaks
329 // how <noscript> is being handled. So we use the same document.
330 // See the discussion in https://github.com/facebook/react/pull/11157.
331 const testElement =
332 parent.namespaceURI === MATH_NAMESPACE ||
333 parent.namespaceURI === SVG_NAMESPACE
334 ? parent.ownerDocument.createElementNS(
335 parent.namespaceURI as any,
336 parent.tagName,
337 )
338 : parent.ownerDocument.createElement(parent.tagName);
339 testElement.innerHTML = html;
340 return testElement.innerHTML;
341 }
342 }
343
344 // HTML parsing normalizes CR and CRLF to LF.
345 // It also can turn \u0000 into \uFFFD inside attributes.
346 // https://www.w3.org/TR/html5/single-page.html#preprocessing-the-input-stream
347 // If we have a mismatch, it might be caused by that.
348 // We will still patch up in this case but not fire the warning.
349 const NORMALIZE_NEWLINES_REGEX = /\r\n?/g;
350 const NORMALIZE_NULL_AND_REPLACEMENT_REGEX = /\u0000|\uFFFD/g;
351
352 function normalizeMarkupForTextOrAttribute(markup: mixed): string {
353 if (__DEV__) {
354 checkHtmlStringCoercion(markup);
355 }
356 const markupString =
357 typeof markup === 'string' ? markup : '' + (markup as any);
358 return markupString
359 .replace(NORMALIZE_NEWLINES_REGEX, '\n')
360 .replace(NORMALIZE_NULL_AND_REPLACEMENT_REGEX, '');
361 }
362
363 function checkForUnmatchedText(
364 serverText: string,
365 clientText: string | number | bigint,
366 ) {
367 const normalizedClientText = normalizeMarkupForTextOrAttribute(clientText);
368 const normalizedServerText = normalizeMarkupForTextOrAttribute(serverText);
369 if (normalizedServerText === normalizedClientText) {
370 return true;
371 }
372 return false;
373 }
374
375 export function trapClickOnNonInteractiveElement(node: HTMLElement) {
376 // Mobile Safari does not fire properly bubble click events on
377 // non-interactive elements, which means delegated click listeners do not
378 // fire. The workaround for this bug involves attaching an empty click
379 // listener on the target node.
380 // https://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
381 // Just set it using the onclick property so that we don't have to manage any
382 // bookkeeping for it. HostSingleton release clears the property only if it
383 // still points to this noop.
384 // TODO: Only do this for the relevant Safaris maybe?
385 node.onclick = noop;
386 }
387
388 export function clearClickListener(node: HTMLElement) {
389 if (node.onclick === noop) {
390 node.onclick = null;
391 }
392 }
393
394 const xlinkNamespace = 'http://www.w3.org/1999/xlink';
395 const xmlNamespace = 'http://www.w3.org/XML/1998/namespace';
396
397 function setProp(
398 domElement: Element,
399 tag: string,
400 key: string,
401 value: mixed,
402 props: any,
403 prevValue: mixed,
404 ): void {
405 switch (key) {
406 case 'children': {
407 if (typeof value === 'string') {
408 if (__DEV__) {
409 validateTextNesting(value, tag, false);
410 }
411 // Avoid setting initial textContent when the text is empty. In IE11 setting
412 // textContent on a <textarea> will cause the placeholder to not
413 // show within the <textarea> until it has been focused and blurred again.
414 // https://github.com/facebook/react/issues/6731#issuecomment-254874553
415 const canSetTextContent =
416 tag !== 'body' && (tag !== 'textarea' || value !== '');
417 if (canSetTextContent) {
418 setTextContent(domElement, value);
419 }
420 } else if (typeof value === 'number' || typeof value === 'bigint') {
421 if (__DEV__) {
422 // $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint
423 validateTextNesting('' + value, tag, false);
424 }
425 const canSetTextContent = tag !== 'body';
426 if (canSetTextContent) {
427 // $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint
428 setTextContent(domElement, '' + value);
429 }
430 } else {
431 return;
432 }
433 break;
434 }
435 // These are very common props and therefore are in the beginning of the switch.
436 // TODO: aria-label is a very common prop but allows booleans so is not like the others
437 // but should ideally go in this list too.
438 case 'className':
439 setValueForKnownAttribute(domElement, 'class', value);
440 break;
441 case 'tabIndex':
442 // This has to be case sensitive in SVG.
443 setValueForKnownAttribute(domElement, 'tabindex', value);
444 break;
445 case 'dir':
446 case 'role':
447 case 'viewBox':
448 case 'width':
449 case 'height': {
450 setValueForKnownAttribute(domElement, key, value);
451 break;
452 }
453 case 'style': {
454 setValueForStyles(domElement, value, prevValue);
455 return;
456 }
457 // These attributes accept URLs. These must not allow javascript: URLS.
458 case 'data':
459 if (tag !== 'object') {
460 setValueForKnownAttribute(domElement, 'data', value);
461 break;
462 }
463 // fallthrough
464 case 'src': {
465 if (enableSrcObject && typeof value === 'object' && value !== null) {
466 // Some tags support object sources like Blob, File, MediaSource and MediaStream.
467 if (tag === 'img' || tag === 'video' || tag === 'audio') {
468 try {
469 setSrcObject(domElement, tag, value);
470 break;
471 } catch (x) {
472 // If URL.createObjectURL() errors, it was probably some other object type
473 // that should be toString:ed instead, so we just fall-through to the normal
474 // path.
475 }
476 } else {
477 if (__DEV__) {
478 try {
479 // This should always error.
480 URL.revokeObjectURL(URL.createObjectURL(value as any));
481 if (tag === 'source') {
482 console.error(
483 'Passing Blob, MediaSource or MediaStream to <source src> is not supported. ' +
484 'Pass it directly to <img src>, <video src> or <audio src> instead.',
485 );
486 } else {
487 console.error(
488 'Passing Blob, MediaSource or MediaStream to <%s src> is not supported.',
489 tag,
490 );
491 }
492 } catch (x) {}
493 }
494 }
495 }
496 // Fallthrough
497 }
498 case 'href': {
499 if (
500 value === '' &&
501 // <a href=""> is fine for "reload" links.
502 !(tag === 'a' && key === 'href')
503 ) {
504 if (__DEV__) {
505 if (key === 'src') {
506 console.error(
507 'An empty string ("") was passed to the %s attribute. ' +
508 'This may cause the browser to download the whole page again over the network. ' +
509 'To fix this, either do not render the element at all ' +
510 'or pass null to %s instead of an empty string.',
511 key,
512 key,
513 );
514 } else {
515 console.error(
516 'An empty string ("") was passed to the %s attribute. ' +
517 'To fix this, either do not render the element at all ' +
518 'or pass null to %s instead of an empty string.',
519 key,
520 key,
521 );
522 }
523 }
524 domElement.removeAttribute(key);
525 break;
526 }
527 if (
528 value == null ||
529 typeof value === 'function' ||
530 typeof value === 'symbol' ||
531 typeof value === 'boolean'
532 ) {
533 domElement.removeAttribute(key);
534 break;
535 }
536 // `setAttribute` with objects becomes only `[object]` in IE8/9,
537 // ('' + value) makes it output the correct toString()-value.
538 if (__DEV__) {
539 checkAttributeStringCoercion(value, key);
540 }
541 const sanitizedValue = sanitizeURL(
542 enableTrustedTypesIntegration ? value : '' + (value as any),
543 ) as any;
544 domElement.setAttribute(key, sanitizedValue);
545 break;
546 }
547 case 'action':
548 case 'formAction': {
549 // TODO: Consider moving these special cases to the form, input and button tags.
550 if (__DEV__) {
551 validateFormActionInDevelopment(tag, key, value, props);
552 }
553 if (typeof value === 'function') {
554 // Set a javascript URL that doesn't do anything. We don't expect this to be invoked
555 // because we'll preventDefault, but it can happen if a form is manually submitted or
556 // if someone calls stopPropagation before React gets the event.
557 // If CSP is used to block javascript: URLs that's fine too. It just won't show this
558 // error message but the URL will be logged.
559 domElement.setAttribute(
560 key,
561 // eslint-disable-next-line no-script-url
562 "javascript:throw new Error('" +
563 'A React form was unexpectedly submitted. If you called form.submit() manually, ' +
564 "consider using form.requestSubmit() instead. If you\\'re trying to use " +
565 'event.stopPropagation() in a submit event handler, consider also calling ' +
566 'event.preventDefault().' +
567 "')",
568 );
569 break;
570 } else if (typeof prevValue === 'function') {
571 // When we're switching off a Server Action that was originally hydrated.
572 // The server control these fields during SSR that are now trailing.
573 // The regular diffing doesn't apply since we compare against the previous props.
574 // Instead, we need to force them to be set to whatever they should be now.
575 // This would be a lot cleaner if we did this whole fork in the per-tag approach.
576 if (key === 'formAction') {
577 if (tag !== 'input') {
578 // Setting the name here isn't completely safe for inputs if this is switching
579 // to become a radio button. In that case we let the tag based override take
580 // control.
581 setProp(domElement, tag, 'name', props.name, props, null);
582 }
583 setProp(
584 domElement,
585 tag,
586 'formEncType',
587 props.formEncType,
588 props,
589 null,
590 );
591 setProp(domElement, tag, 'formMethod', props.formMethod, props, null);
592 setProp(domElement, tag, 'formTarget', props.formTarget, props, null);
593 } else {
594 setProp(domElement, tag, 'encType', props.encType, props, null);
595 setProp(domElement, tag, 'method', props.method, props, null);
596 setProp(domElement, tag, 'target', props.target, props, null);
597 }
598 }
599 if (
600 value == null ||
601 typeof value === 'symbol' ||
602 typeof value === 'boolean'
603 ) {
604 domElement.removeAttribute(key);
605 break;
606 }
607 // `setAttribute` with objects becomes only `[object]` in IE8/9,
608 // ('' + value) makes it output the correct toString()-value.
609 if (__DEV__) {
610 checkAttributeStringCoercion(value, key);
611 }
612 const sanitizedValue = sanitizeURL(
613 enableTrustedTypesIntegration ? value : '' + (value as any),
614 ) as any;
615 domElement.setAttribute(key, sanitizedValue);
616 break;
617 }
618 case 'onClick': {
619 // TODO: This cast may not be sound for SVG, MathML or custom elements.
620 if (value != null) {
621 if (__DEV__ && typeof value !== 'function') {
622 warnForInvalidEventListener(key, value);
623 }
624 trapClickOnNonInteractiveElement(domElement as any as HTMLElement);
625 }
626 return;
627 }
628 case 'onScroll': {
629 if (value != null) {
630 if (__DEV__ && typeof value !== 'function') {
631 warnForInvalidEventListener(key, value);
632 }
633 listenToNonDelegatedEvent('scroll', domElement);
634 }
635 return;
636 }
637 case 'onScrollEnd': {
638 if (value != null) {
639 if (__DEV__ && typeof value !== 'function') {
640 warnForInvalidEventListener(key, value);
641 }
642 listenToNonDelegatedEvent('scrollend', domElement);
643 if (enableScrollEndPolyfill) {
644 // For use by the polyfill.
645 listenToNonDelegatedEvent('scroll', domElement);
646 }
647 }
648 return;
649 }
650 case 'dangerouslySetInnerHTML': {
651 if (value != null) {
652 if (typeof value !== 'object' || !('__html' in value)) {
653 throw new Error(
654 '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +
655 'Please visit https://react.dev/link/dangerously-set-inner-html ' +
656 'for more information.',
657 );
658 }
659 const nextHtml: any = value.__html;
660 if (nextHtml != null) {
661 if (props.children != null) {
662 throw new Error(
663 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.',
664 );
665 }
666 const lastHtml: any =
667 prevValue != null ? (prevValue as any).__html : undefined;
668 if (lastHtml !== nextHtml) {
669 domElement.innerHTML = nextHtml;
670 }
671 }
672 }
673 break;
674 }
675 // Note: `option.selected` is not updated if `select.multiple` is
676 // disabled with `removeAttribute`. We have special logic for handling this.
677 case 'multiple': {
678 (domElement as any).multiple =
679 value && typeof value !== 'function' && typeof value !== 'symbol';
680 break;
681 }
682 case 'muted': {
683 (domElement as any).muted =
684 value && typeof value !== 'function' && typeof value !== 'symbol';
685 break;
686 }
687 case 'suppressContentEditableWarning':
688 case 'suppressHydrationWarning':
689 case 'defaultValue': // Reserved
690 case 'defaultChecked':
691 case 'innerHTML':
692 case 'ref': {
693 // TODO: `ref` is pretty common, should we move it up?
694 // Noop
695 break;
696 }
697 case 'autoFocus': {
698 // We polyfill it separately on the client during commit.
699 // We could have excluded it in the property list instead of
700 // adding a special case here, but then it wouldn't be emitted
701 // on server rendering (but we *do* want to emit it in SSR).
702 break;
703 }
704 case 'xlinkHref': {
705 if (
706 value == null ||
707 typeof value === 'function' ||
708 typeof value === 'boolean' ||
709 typeof value === 'symbol'
710 ) {
711 domElement.removeAttribute('xlink:href');
712 break;
713 }
714 // `setAttribute` with objects becomes only `[object]` in IE8/9,
715 // ('' + value) makes it output the correct toString()-value.
716 if (__DEV__) {
717 checkAttributeStringCoercion(value, key);
718 }
719 const sanitizedValue = sanitizeURL(
720 enableTrustedTypesIntegration ? value : '' + (value as any),
721 ) as any;
722 domElement.setAttributeNS(xlinkNamespace, 'xlink:href', sanitizedValue);
723 break;
724 }
725 case 'contentEditable':
726 case 'spellCheck':
727 case 'draggable':
728 case 'value':
729 case 'autoReverse':
730 case 'externalResourcesRequired':
731 case 'focusable':
732 case 'preserveAlpha': {
733 // Booleanish String
734 // These are "enumerated" attributes that accept "true" and "false".
735 // In React, we let users pass `true` and `false` even though technically
736 // these aren't boolean attributes (they are coerced to strings).
737 // The SVG attributes are case-sensitive. Since the HTML attributes are
738 // insensitive they also work even though we canonically use lower case.
739 if (
740 value != null &&
741 typeof value !== 'function' &&
742 typeof value !== 'symbol'
743 ) {
744 if (__DEV__) {
745 checkAttributeStringCoercion(value, key);
746 }
747 domElement.setAttribute(
748 key,
749 enableTrustedTypesIntegration ? (value as any) : '' + (value as any),
750 );
751 } else {
752 domElement.removeAttribute(key);
753 }
754 break;
755 }
756 // Boolean
757 case 'inert': {
758 if (__DEV__) {
759 if (value === '' && !didWarnForNewBooleanPropsWithEmptyValue[key]) {
760 didWarnForNewBooleanPropsWithEmptyValue[key] = true;
761 console.error(
762 'Received an empty string for a boolean attribute `%s`. ' +
763 'This will treat the attribute as if it were false. ' +
764 'Either pass `false` to silence this warning, or ' +
765 'pass `true` if you used an empty string in earlier versions of React to indicate this attribute is true.',
766 key,
767 );
768 }
769 }
770 }
771 // Fallthrough for boolean props that don't have a warning for empty strings.
772 case 'allowFullScreen':
773 case 'async':
774 case 'autoPlay':
775 case 'controls':
776 case 'credentialless':
777 case 'default':
778 case 'defer':
779 case 'disabled':
780 case 'disablePictureInPicture':
781 case 'disableRemotePlayback':
782 case 'formNoValidate':
783 case 'hidden':
784 case 'loop':
785 case 'noModule':
786 case 'noValidate':
787 case 'open':
788 case 'playsInline':
789 case 'readOnly':
790 case 'required':
791 case 'reversed':
792 case 'scoped':
793 case 'seamless':
794 case 'itemScope': {
795 if (value && typeof value !== 'function' && typeof value !== 'symbol') {
796 domElement.setAttribute(key, '');
797 } else {
798 domElement.removeAttribute(key);
799 }
800 break;
801 }
802 // Overloaded Boolean
803 case 'capture':
804 case 'download': {
805 // An attribute that can be used as a flag as well as with a value.
806 // When true, it should be present (set either to an empty string or its name).
807 // When false, it should be omitted.
808 // For any other value, should be present with that value.
809 if (value === true) {
810 domElement.setAttribute(key, '');
811 } else if (
812 value !== false &&
813 value != null &&
814 typeof value !== 'function' &&
815 typeof value !== 'symbol'
816 ) {
817 if (__DEV__) {
818 checkAttributeStringCoercion(value, key);
819 }
820 domElement.setAttribute(key, value as any);
821 } else {
822 domElement.removeAttribute(key);
823 }
824 break;
825 }
826 case 'cols':
827 case 'rows':
828 case 'size':
829 case 'span': {
830 // These are HTML attributes that must be positive numbers.
831 if (
832 value != null &&
833 typeof value !== 'function' &&
834 typeof value !== 'symbol' &&
835 !isNaN(value) &&
836 (value as any) >= 1
837 ) {
838 if (__DEV__) {
839 checkAttributeStringCoercion(value, key);
840 }
841 domElement.setAttribute(key, value as any);
842 } else {
843 domElement.removeAttribute(key);
844 }
845 break;
846 }
847 case 'rowSpan':
848 case 'start': {
849 // These are HTML attributes that must be numbers.
850 if (
851 value != null &&
852 typeof value !== 'function' &&
853 typeof value !== 'symbol' &&
854 !isNaN(value)
855 ) {
856 if (__DEV__) {
857 checkAttributeStringCoercion(value, key);
858 }
859 domElement.setAttribute(key, value as any);
860 } else {
861 domElement.removeAttribute(key);
862 }
863 break;
864 }
865 case 'popover':
866 listenToNonDelegatedEvent('beforetoggle', domElement);
867 listenToNonDelegatedEvent('toggle', domElement);
868 setValueForAttribute(domElement, 'popover', value);
869 break;
870 case 'xlinkActuate':
871 setValueForNamespacedAttribute(
872 domElement,
873 xlinkNamespace,
874 'xlink:actuate',
875 value,
876 );
877 break;
878 case 'xlinkArcrole':
879 setValueForNamespacedAttribute(
880 domElement,
881 xlinkNamespace,
882 'xlink:arcrole',
883 value,
884 );
885 break;
886 case 'xlinkRole':
887 setValueForNamespacedAttribute(
888 domElement,
889 xlinkNamespace,
890 'xlink:role',
891 value,
892 );
893 break;
894 case 'xlinkShow':
895 setValueForNamespacedAttribute(
896 domElement,
897 xlinkNamespace,
898 'xlink:show',
899 value,
900 );
901 break;
902 case 'xlinkTitle':
903 setValueForNamespacedAttribute(
904 domElement,
905 xlinkNamespace,
906 'xlink:title',
907 value,
908 );
909 break;
910 case 'xlinkType':
911 setValueForNamespacedAttribute(
912 domElement,
913 xlinkNamespace,
914 'xlink:type',
915 value,
916 );
917 break;
918 case 'xmlBase':
919 setValueForNamespacedAttribute(
920 domElement,
921 xmlNamespace,
922 'xml:base',
923 value,
924 );
925 break;
926 case 'xmlLang':
927 setValueForNamespacedAttribute(
928 domElement,
929 xmlNamespace,
930 'xml:lang',
931 value,
932 );
933 break;
934 case 'xmlSpace':
935 setValueForNamespacedAttribute(
936 domElement,
937 xmlNamespace,
938 'xml:space',
939 value,
940 );
941 break;
942 // Properties that should not be allowed on custom elements.
943 case 'is': {
944 if (__DEV__) {
945 if (prevValue != null) {
946 console.error(
947 'Cannot update the "is" prop after it has been initialized.',
948 );
949 }
950 }
951 // TODO: We shouldn't actually set this attribute, because we've already
952 // passed it to createElement. We don't also need the attribute.
953 // However, our tests currently query for it so it's plausible someone
954 // else does too so it's break.
955 setValueForAttribute(domElement, 'is', value);
956 break;
957 }
958 case 'innerText':
959 case 'textContent':
960 return;
961 case 'popoverTarget':
962 if (__DEV__) {
963 if (
964 !didWarnPopoverTargetObject &&
965 value != null &&
966 typeof value === 'object'
967 ) {
968 didWarnPopoverTargetObject = true;
969 console.error(
970 'The `popoverTarget` prop expects the ID of an Element as a string. Received %s instead.',
971 value,
972 );
973 }
974 }
975 // Fall through
976 default: {
977 if (
978 key.length > 2 &&
979 (key[0] === 'o' || key[0] === 'O') &&
980 (key[1] === 'n' || key[1] === 'N')
981 ) {
982 if (
983 __DEV__ &&
984 registrationNameDependencies.hasOwnProperty(key) &&
985 value != null &&
986 typeof value !== 'function'
987 ) {
988 warnForInvalidEventListener(key, value);
989 }
990 // Updating events doesn't affect the visuals.
991 return;
992 } else {
993 const attributeName = getAttributeAlias(key);
994 setValueForAttribute(domElement, attributeName, value);
995 }
996 }
997 }
998 // To avoid marking things as host mutations we do early returns above.
999 trackHostMutation();
1000 }
1001
1002 function setPropOnCustomElement(
1003 domElement: Element,
1004 tag: string,
1005 key: string,
1006 value: mixed,
1007 props: any,
1008 prevValue: mixed,
1009 ): void {
1010 switch (key) {
1011 case 'style': {
1012 setValueForStyles(domElement, value, prevValue);
1013 return;
1014 }
1015 case 'dangerouslySetInnerHTML': {
1016 if (value != null) {
1017 if (typeof value !== 'object' || !('__html' in value)) {
1018 throw new Error(
1019 '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +
1020 'Please visit https://react.dev/link/dangerously-set-inner-html ' +
1021 'for more information.',
1022 );
1023 }
1024 const nextHtml: any = value.__html;
1025 if (nextHtml != null) {
1026 if (props.children != null) {
1027 throw new Error(
1028 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.',
1029 );
1030 }
1031 const lastHtml: any =
1032 prevValue != null ? (prevValue as any).__html : undefined;
1033 if (lastHtml !== nextHtml) {
1034 domElement.innerHTML = nextHtml;
1035 }
1036 }
1037 }
1038 break;
1039 }
1040 case 'children': {
1041 if (typeof value === 'string') {
1042 setTextContent(domElement, value);
1043 } else if (typeof value === 'number' || typeof value === 'bigint') {
1044 // $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint
1045 setTextContent(domElement, '' + value);
1046 } else {
1047 return;
1048 }
1049 break;
1050 }
1051 case 'onScroll': {
1052 if (value != null) {
1053 if (__DEV__ && typeof value !== 'function') {
1054 warnForInvalidEventListener(key, value);
1055 }
1056 listenToNonDelegatedEvent('scroll', domElement);
1057 }
1058 return;
1059 }
1060 case 'onScrollEnd': {
1061 if (value != null) {
1062 if (__DEV__ && typeof value !== 'function') {
1063 warnForInvalidEventListener(key, value);
1064 }
1065 listenToNonDelegatedEvent('scrollend', domElement);
1066 if (enableScrollEndPolyfill) {
1067 // For use by the polyfill.
1068 listenToNonDelegatedEvent('scroll', domElement);
1069 }
1070 }
1071 return;
1072 }
1073 case 'onClick': {
1074 // TODO: This cast may not be sound for SVG, MathML or custom elements.
1075 if (value != null) {
1076 if (__DEV__ && typeof value !== 'function') {
1077 warnForInvalidEventListener(key, value);
1078 }
1079 trapClickOnNonInteractiveElement(domElement as any as HTMLElement);
1080 }
1081 return;
1082 }
1083 case 'suppressContentEditableWarning':
1084 case 'suppressHydrationWarning':
1085 case 'innerHTML':
1086 case 'ref': {
1087 // Noop
1088 return;
1089 }
1090 case 'innerText': // Properties
1091 case 'textContent':
1092 return;
1093 // Fall through
1094 default: {
1095 if (registrationNameDependencies.hasOwnProperty(key)) {
1096 if (__DEV__ && value != null && typeof value !== 'function') {
1097 warnForInvalidEventListener(key, value);
1098 }
1099 return;
1100 } else {
1101 setValueForPropertyOnCustomComponent(domElement, key, value);
1102 // We track mutations inside this call.
1103 return;
1104 }
1105 }
1106 }
1107 // To avoid marking things as host mutations we do early returns above.
1108 trackHostMutation();
1109 }
1110
1111 export function setInitialProperties(
1112 domElement: Element,
1113 tag: string,
1114 props: Object,
1115 ): void {
1116 if (__DEV__) {
1117 validatePropertiesInDevelopment(tag, props);
1118 }
1119
1120 // TODO: Make sure that we check isMounted before firing any of these events.
1121
1122 switch (tag) {
1123 case 'div':
1124 case 'span':
1125 case 'svg':
1126 case 'path':
1127 case 'a':
1128 case 'g':
1129 case 'p':
1130 case 'li': {
1131 // Fast track the most common tag types
1132 break;
1133 }
1134 // img tags previously were implemented as void elements with non delegated events however Safari (and possibly Firefox)
1135 // begin fetching the image as soon as the `src` or `srcSet` property is set and if we set these before other properties
1136 // that can modify the request (such as crossorigin) or the resource fetch (such as sizes) then the browser will load
1137 // the wrong thing or load more than one thing. This implementation ensures src and srcSet are set on the instance last
1138 case 'img': {
1139 listenToNonDelegatedEvent('error', domElement);
1140 listenToNonDelegatedEvent('load', domElement);
1141 // Mostly a port of Void Element logic with special casing to ensure srcset and src are set last
1142 let hasSrc = false;
1143 let hasSrcSet = false;
1144 for (const propKey in props) {
1145 if (!props.hasOwnProperty(propKey)) {
1146 continue;
1147 }
1148 const propValue = props[propKey];
1149 if (propValue == null) {
1150 continue;
1151 }
1152 switch (propKey) {
1153 case 'src':
1154 hasSrc = true;
1155 break;
1156 case 'srcSet':
1157 hasSrcSet = true;
1158 break;
1159 case 'children':
1160 case 'dangerouslySetInnerHTML': {
1161 // TODO: Can we make this a DEV warning to avoid this deny list?
1162 throw new Error(
1163 `${tag} is a void element tag and must neither have \`children\` nor ` +
1164 'use `dangerouslySetInnerHTML`.',
1165 );
1166 }
1167 // defaultChecked and defaultValue are ignored by setProp
1168 default: {
1169 setProp(domElement, tag, propKey, propValue, props, null);
1170 }
1171 }
1172 }
1173 if (hasSrcSet) {
1174 setProp(domElement, tag, 'srcSet', props.srcSet, props, null);
1175 }
1176 if (hasSrc) {
1177 setProp(domElement, tag, 'src', props.src, props, null);
1178 }
1179 return;
1180 }
1181 case 'input': {
1182 if (__DEV__) {
1183 checkControlledValueProps('input', props);
1184 }
1185 // We listen to this event in case to ensure emulated bubble
1186 // listeners still fire for the invalid event.
1187 listenToNonDelegatedEvent('invalid', domElement);
1188
1189 let name = null;
1190 let type = null;
1191 let value = null;
1192 let defaultValue = null;
1193 let checked = null;
1194 let defaultChecked = null;
1195 for (const propKey in props) {
1196 if (!props.hasOwnProperty(propKey)) {
1197 continue;
1198 }
1199 const propValue = props[propKey];
1200 if (propValue == null) {
1201 continue;
1202 }
1203 switch (propKey) {
1204 case 'name': {
1205 name = propValue;
1206 break;
1207 }
1208 case 'type': {
1209 type = propValue;
1210 break;
1211 }
1212 case 'checked': {
1213 checked = propValue;
1214 break;
1215 }
1216 case 'defaultChecked': {
1217 defaultChecked = propValue;
1218 break;
1219 }
1220 case 'value': {
1221 value = propValue;
1222 break;
1223 }
1224 case 'defaultValue': {
1225 defaultValue = propValue;
1226 break;
1227 }
1228 case 'children':
1229 case 'dangerouslySetInnerHTML': {
1230 if (propValue != null) {
1231 throw new Error(
1232 `${tag} is a void element tag and must neither have \`children\` nor ` +
1233 'use `dangerouslySetInnerHTML`.',
1234 );
1235 }
1236 break;
1237 }
1238 default: {
1239 setProp(domElement, tag, propKey, propValue, props, null);
1240 }
1241 }
1242 }
1243 // TODO: Make sure we check if this is still unmounted or do any clean
1244 // up necessary since we never stop tracking anymore.
1245 validateInputProps(domElement, props);
1246 initInput(
1247 domElement,
1248 value,
1249 defaultValue,
1250 checked,
1251 defaultChecked,
1252 type,
1253 name,
1254 false,
1255 );
1256 return;
1257 }
1258 case 'select': {
1259 if (__DEV__) {
1260 checkControlledValueProps('select', props);
1261 }
1262 // We listen to this event in case to ensure emulated bubble
1263 // listeners still fire for the invalid event.
1264 listenToNonDelegatedEvent('invalid', domElement);
1265 let value = null;
1266 let defaultValue = null;
1267 let multiple = null;
1268 for (const propKey in props) {
1269 if (!props.hasOwnProperty(propKey)) {
1270 continue;
1271 }
1272 const propValue = props[propKey];
1273 if (propValue == null) {
1274 continue;
1275 }
1276 switch (propKey) {
1277 case 'value': {
1278 value = propValue;
1279 // This is handled by initSelect below.
1280 break;
1281 }
1282 case 'defaultValue': {
1283 defaultValue = propValue;
1284 // This is handled by initSelect below.
1285 break;
1286 }
1287 case 'multiple': {
1288 multiple = propValue;
1289 // TODO: We don't actually have to fall through here because we set it
1290 // in initSelect anyway. We can remove the special case in setProp.
1291 }
1292 // Fallthrough
1293 default: {
1294 setProp(domElement, tag, propKey, propValue, props, null);
1295 }
1296 }
1297 }
1298 validateSelectProps(domElement, props);
1299 initSelect(domElement, value, defaultValue, multiple);
1300 return;
1301 }
1302 case 'textarea': {
1303 if (__DEV__) {
1304 checkControlledValueProps('textarea', props);
1305 }
1306 // We listen to this event in case to ensure emulated bubble
1307 // listeners still fire for the invalid event.
1308 listenToNonDelegatedEvent('invalid', domElement);
1309 let value = null;
1310 let defaultValue = null;
1311 let children = null;
1312 for (const propKey in props) {
1313 if (!props.hasOwnProperty(propKey)) {
1314 continue;
1315 }
1316 const propValue = props[propKey];
1317 if (propValue == null) {
1318 continue;
1319 }
1320 switch (propKey) {
1321 case 'value': {
1322 value = propValue;
1323 // This is handled by initTextarea below.
1324 break;
1325 }
1326 case 'defaultValue': {
1327 defaultValue = propValue;
1328 break;
1329 }
1330 case 'children': {
1331 children = propValue;
1332 // Handled by initTextarea above.
1333 break;
1334 }
1335 case 'dangerouslySetInnerHTML': {
1336 if (propValue != null) {
1337 // TODO: Do we really need a special error message for this. It's also pretty blunt.
1338 throw new Error(
1339 '`dangerouslySetInnerHTML` does not make sense on <textarea>.',
1340 );
1341 }
1342 break;
1343 }
1344 default: {
1345 setProp(domElement, tag, propKey, propValue, props, null);
1346 }
1347 }
1348 }
1349 // TODO: Make sure we check if this is still unmounted or do any clean
1350 // up necessary since we never stop tracking anymore.
1351 validateTextareaProps(domElement, props);
1352 initTextarea(domElement, value, defaultValue, children);
1353 return;
1354 }
1355 case 'option': {
1356 validateOptionProps(domElement, props);
1357 for (const propKey in props) {
1358 if (!props.hasOwnProperty(propKey)) {
1359 continue;
1360 }
1361 const propValue = props[propKey];
1362 if (propValue == null) {
1363 continue;
1364 }
1365 switch (propKey) {
1366 case 'selected': {
1367 // TODO: Remove support for selected on option.
1368 (domElement as any).selected =
1369 propValue &&
1370 typeof propValue !== 'function' &&
1371 typeof propValue !== 'symbol';
1372 break;
1373 }
1374 default: {
1375 setProp(domElement, tag, propKey, propValue, props, null);
1376 }
1377 }
1378 }
1379 return;
1380 }
1381 case 'dialog': {
1382 listenToNonDelegatedEvent('beforetoggle', domElement);
1383 listenToNonDelegatedEvent('toggle', domElement);
1384 listenToNonDelegatedEvent('cancel', domElement);
1385 listenToNonDelegatedEvent('close', domElement);
1386 break;
1387 }
1388 case 'iframe':
1389 case 'object': {
1390 // We listen to this event in case to ensure emulated bubble
1391 // listeners still fire for the load event.
1392 listenToNonDelegatedEvent('load', domElement);
1393 break;
1394 }
1395 case 'video':
1396 case 'audio': {
1397 // We listen to these events in case to ensure emulated bubble
1398 // listeners still fire for all the media events.
1399 for (let i = 0; i < mediaEventTypes.length; i++) {
1400 listenToNonDelegatedEvent(mediaEventTypes[i], domElement);
1401 }
1402 break;
1403 }
1404 case 'image': {
1405 // We listen to these events in case to ensure emulated bubble
1406 // listeners still fire for error and load events.
1407 listenToNonDelegatedEvent('error', domElement);
1408 listenToNonDelegatedEvent('load', domElement);
1409 break;
1410 }
1411 case 'details': {
1412 // We listen to this event in case to ensure emulated bubble
1413 // listeners still fire for the toggle event.
1414 listenToNonDelegatedEvent('toggle', domElement);
1415 break;
1416 }
1417 case 'embed':
1418 case 'source':
1419 case 'link': {
1420 // These are void elements that also need delegated events.
1421 listenToNonDelegatedEvent('error', domElement);
1422 listenToNonDelegatedEvent('load', domElement);
1423 // We fallthrough to the return of the void elements
1424 }
1425 case 'area':
1426 case 'base':
1427 case 'br':
1428 case 'col':
1429 case 'hr':
1430 case 'keygen':
1431 case 'meta':
1432 case 'param':
1433 case 'track':
1434 case 'wbr':
1435 case 'menuitem': {
1436 // Void elements
1437 for (const propKey in props) {
1438 if (!props.hasOwnProperty(propKey)) {
1439 continue;
1440 }
1441 const propValue = props[propKey];
1442 if (propValue == null) {
1443 continue;
1444 }
1445 switch (propKey) {
1446 case 'children':
1447 case 'dangerouslySetInnerHTML': {
1448 // TODO: Can we make this a DEV warning to avoid this deny list?
1449 throw new Error(
1450 `${tag} is a void element tag and must neither have \`children\` nor ` +
1451 'use `dangerouslySetInnerHTML`.',
1452 );
1453 }
1454 // defaultChecked and defaultValue are ignored by setProp
1455 default: {
1456 setProp(domElement, tag, propKey, propValue, props, null);
1457 }
1458 }
1459 }
1460 return;
1461 }
1462 default: {
1463 if (isCustomElement(tag, props)) {
1464 for (const propKey in props) {
1465 if (!props.hasOwnProperty(propKey)) {
1466 continue;
1467 }
1468 const propValue = props[propKey];
1469 if (propValue === undefined) {
1470 continue;
1471 }
1472 setPropOnCustomElement(
1473 domElement,
1474 tag,
1475 propKey,
1476 propValue,
1477 props,
1478 undefined,
1479 );
1480 }
1481 return;
1482 }
1483 }
1484 }
1485
1486 for (const propKey in props) {
1487 if (!props.hasOwnProperty(propKey)) {
1488 continue;
1489 }
1490 const propValue = props[propKey];
1491 if (propValue == null) {
1492 continue;
1493 }
1494 setProp(domElement, tag, propKey, propValue, props, null);
1495 }
1496 }
1497
1498 export type SingletonType = 'html' | 'head' | 'body';
1499
1500 const emptyProps = {};
1501
1502 export function clearSingletonProperties(
1503 domElement: Element,
1504 tag: SingletonType,
1505 props: Object,
1506 ): void {
1507 // This is equivalent to updating to empty props for tags without
1508 // tag-specific update logic. Host singletons are limited to html, head, and
1509 // body, so they always use this generic path.
1510 for (const propKey in props) {
1511 const propValue = props[propKey];
1512 if (props.hasOwnProperty(propKey) && propValue != null) {
1513 setProp(domElement, tag, propKey, null, emptyProps, propValue);
1514 }
1515 }
1516 }
1517
1518 export function updateProperties(
1519 domElement: Element,
1520 tag: string,
1521 lastProps: Object,
1522 nextProps: Object,
1523 ): void {
1524 if (__DEV__) {
1525 validatePropertiesInDevelopment(tag, nextProps);
1526 }
1527
1528 switch (tag) {
1529 case 'div':
1530 case 'span':
1531 case 'svg':
1532 case 'path':
1533 case 'a':
1534 case 'g':
1535 case 'p':
1536 case 'li': {
1537 // Fast track the most common tag types
1538 break;
1539 }
1540 case 'input': {
1541 let name = null;
1542 let type = null;
1543 let value = null;
1544 let defaultValue = null;
1545 let lastDefaultValue = null;
1546 let checked = null;
1547 let defaultChecked = null;
1548 for (const propKey in lastProps) {
1549 const lastProp = lastProps[propKey];
1550 if (lastProps.hasOwnProperty(propKey) && lastProp != null) {
1551 switch (propKey) {
1552 case 'checked': {
1553 break;
1554 }
1555 case 'value': {
1556 // This is handled by updateWrapper below.
1557 break;
1558 }
1559 case 'defaultValue': {
1560 lastDefaultValue = lastProp;
1561 }
1562 // defaultChecked and defaultValue are ignored by setProp
1563 // Fallthrough
1564 default: {
1565 if (!nextProps.hasOwnProperty(propKey))
1566 setProp(domElement, tag, propKey, null, nextProps, lastProp);
1567 }
1568 }
1569 }
1570 }
1571 for (const propKey in nextProps) {
1572 const nextProp = nextProps[propKey];
1573 const lastProp = lastProps[propKey];
1574 if (
1575 nextProps.hasOwnProperty(propKey) &&
1576 (nextProp != null || lastProp != null)
1577 ) {
1578 switch (propKey) {
1579 case 'type': {
1580 if (nextProp !== lastProp) {
1581 trackHostMutation();
1582 }
1583 type = nextProp;
1584 break;
1585 }
1586 case 'name': {
1587 if (nextProp !== lastProp) {
1588 trackHostMutation();
1589 }
1590 name = nextProp;
1591 break;
1592 }
1593 case 'checked': {
1594 if (nextProp !== lastProp) {
1595 trackHostMutation();
1596 }
1597 checked = nextProp;
1598 break;
1599 }
1600 case 'defaultChecked': {
1601 if (nextProp !== lastProp) {
1602 trackHostMutation();
1603 }
1604 defaultChecked = nextProp;
1605 break;
1606 }
1607 case 'value': {
1608 if (nextProp !== lastProp) {
1609 trackHostMutation();
1610 }
1611 value = nextProp;
1612 break;
1613 }
1614 case 'defaultValue': {
1615 if (nextProp !== lastProp) {
1616 trackHostMutation();
1617 }
1618 defaultValue = nextProp;
1619 break;
1620 }
1621 case 'children':
1622 case 'dangerouslySetInnerHTML': {
1623 if (nextProp != null) {
1624 throw new Error(
1625 `${tag} is a void element tag and must neither have \`children\` nor ` +
1626 'use `dangerouslySetInnerHTML`.',
1627 );
1628 }
1629 break;
1630 }
1631 default: {
1632 if (nextProp !== lastProp)
1633 setProp(
1634 domElement,
1635 tag,
1636 propKey,
1637 nextProp,
1638 nextProps,
1639 lastProp,
1640 );
1641 }
1642 }
1643 }
1644 }
1645
1646 if (__DEV__) {
1647 const wasControlled =
1648 lastProps.type === 'checkbox' || lastProps.type === 'radio'
1649 ? lastProps.checked != null
1650 : lastProps.value != null;
1651 const isControlled =
1652 nextProps.type === 'checkbox' || nextProps.type === 'radio'
1653 ? nextProps.checked != null
1654 : nextProps.value != null;
1655
1656 if (
1657 !wasControlled &&
1658 isControlled &&
1659 !didWarnUncontrolledToControlled
1660 ) {
1661 console.error(
1662 'A component is changing an uncontrolled input to be controlled. ' +
1663 'This is likely caused by the value changing from undefined to ' +
1664 'a defined value, which should not happen. ' +
1665 'Decide between using a controlled or uncontrolled input ' +
1666 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components',
1667 );
1668 didWarnUncontrolledToControlled = true;
1669 }
1670 if (
1671 wasControlled &&
1672 !isControlled &&
1673 !didWarnControlledToUncontrolled
1674 ) {
1675 console.error(
1676 'A component is changing a controlled input to be uncontrolled. ' +
1677 'This is likely caused by the value changing from a defined to ' +
1678 'undefined, which should not happen. ' +
1679 'Decide between using a controlled or uncontrolled input ' +
1680 'element for the lifetime of the component. More info: https://react.dev/link/controlled-components',
1681 );
1682 didWarnControlledToUncontrolled = true;
1683 }
1684 }
1685
1686 // Update the wrapper around inputs *after* updating props. This has to
1687 // happen after updating the rest of props. Otherwise HTML5 input validations
1688 // raise warnings and prevent the new value from being assigned.
1689 updateInput(
1690 domElement,
1691 value,
1692 defaultValue,
1693 lastDefaultValue,
1694 checked,
1695 defaultChecked,
1696 type,
1697 name,
1698 );
1699 return;
1700 }
1701 case 'select': {
1702 let value = null;
1703 let defaultValue = null;
1704 let multiple = null;
1705 let wasMultiple = null;
1706 for (const propKey in lastProps) {
1707 const lastProp = lastProps[propKey];
1708 if (lastProps.hasOwnProperty(propKey) && lastProp != null) {
1709 switch (propKey) {
1710 case 'value': {
1711 // This is handled by updateWrapper below.
1712 break;
1713 }
1714 // defaultValue are ignored by setProp
1715 case 'multiple': {
1716 wasMultiple = lastProp;
1717 // TODO: Move special case in here from setProp.
1718 }
1719 // Fallthrough
1720 default: {
1721 if (!nextProps.hasOwnProperty(propKey)) {
1722 setProp(domElement, tag, propKey, null, nextProps, lastProp);
1723 }
1724 }
1725 }
1726 }
1727 }
1728 for (const propKey in nextProps) {
1729 const nextProp = nextProps[propKey];
1730 const lastProp = lastProps[propKey];
1731 if (
1732 nextProps.hasOwnProperty(propKey) &&
1733 (nextProp != null || lastProp != null)
1734 ) {
1735 switch (propKey) {
1736 case 'value': {
1737 if (nextProp !== lastProp) {
1738 trackHostMutation();
1739 }
1740 value = nextProp;
1741 // This is handled by updateSelect below.
1742 break;
1743 }
1744 case 'defaultValue': {
1745 if (nextProp !== lastProp) {
1746 trackHostMutation();
1747 }
1748 defaultValue = nextProp;
1749 break;
1750 }
1751 case 'multiple': {
1752 if (nextProp !== lastProp) {
1753 trackHostMutation();
1754 }
1755 multiple = nextProp;
1756 // TODO: Just move the special case in here from setProp.
1757 }
1758 // Fallthrough
1759 default: {
1760 if (nextProp !== lastProp)
1761 setProp(
1762 domElement,
1763 tag,
1764 propKey,
1765 nextProp,
1766 nextProps,
1767 lastProp,
1768 );
1769 }
1770 }
1771 }
1772 }
1773 // <select> value update needs to occur after <option> children
1774 // reconciliation
1775 updateSelect(domElement, value, defaultValue, multiple, wasMultiple);
1776 return;
1777 }
1778 case 'textarea': {
1779 let value = null;
1780 let defaultValue = null;
1781 for (const propKey in lastProps) {
1782 const lastProp = lastProps[propKey];
1783 if (
1784 lastProps.hasOwnProperty(propKey) &&
1785 lastProp != null &&
1786 !nextProps.hasOwnProperty(propKey)
1787 ) {
1788 switch (propKey) {
1789 case 'value': {
1790 // This is handled by updateTextarea below.
1791 break;
1792 }
1793 case 'children': {
1794 // TODO: This doesn't actually do anything if it updates.
1795 break;
1796 }
1797 // defaultValue is ignored by setProp
1798 default: {
1799 setProp(domElement, tag, propKey, null, nextProps, lastProp);
1800 }
1801 }
1802 }
1803 }
1804 for (const propKey in nextProps) {
1805 const nextProp = nextProps[propKey];
1806 const lastProp = lastProps[propKey];
1807 if (
1808 nextProps.hasOwnProperty(propKey) &&
1809 (nextProp != null || lastProp != null)
1810 ) {
1811 switch (propKey) {
1812 case 'value': {
1813 if (nextProp !== lastProp) {
1814 trackHostMutation();
1815 }
1816 value = nextProp;
1817 // This is handled by updateTextarea below.
1818 break;
1819 }
1820 case 'defaultValue': {
1821 if (nextProp !== lastProp) {
1822 trackHostMutation();
1823 }
1824 defaultValue = nextProp;
1825 break;
1826 }
1827 case 'children': {
1828 // TODO: This doesn't actually do anything if it updates.
1829 break;
1830 }
1831 case 'dangerouslySetInnerHTML': {
1832 if (nextProp != null) {
1833 // TODO: Do we really need a special error message for this. It's also pretty blunt.
1834 throw new Error(
1835 '`dangerouslySetInnerHTML` does not make sense on <textarea>.',
1836 );
1837 }
1838 break;
1839 }
1840 default: {
1841 if (nextProp !== lastProp)
1842 setProp(
1843 domElement,
1844 tag,
1845 propKey,
1846 nextProp,
1847 nextProps,
1848 lastProp,
1849 );
1850 }
1851 }
1852 }
1853 }
1854 updateTextarea(domElement, value, defaultValue);
1855 return;
1856 }
1857 case 'option': {
1858 for (const propKey in lastProps) {
1859 const lastProp = lastProps[propKey];
1860 if (
1861 lastProps.hasOwnProperty(propKey) &&
1862 lastProp != null &&
1863 !nextProps.hasOwnProperty(propKey)
1864 ) {
1865 switch (propKey) {
1866 case 'selected': {
1867 // TODO: Remove support for selected on option.
1868 (domElement as any).selected = false;
1869 break;
1870 }
1871 default: {
1872 setProp(domElement, tag, propKey, null, nextProps, lastProp);
1873 }
1874 }
1875 }
1876 }
1877 for (const propKey in nextProps) {
1878 const nextProp = nextProps[propKey];
1879 const lastProp = lastProps[propKey];
1880 if (
1881 nextProps.hasOwnProperty(propKey) &&
1882 nextProp !== lastProp &&
1883 (nextProp != null || lastProp != null)
1884 ) {
1885 switch (propKey) {
1886 case 'selected': {
1887 if (nextProp !== lastProp) {
1888 trackHostMutation();
1889 }
1890 // TODO: Remove support for selected on option.
1891 (domElement as any).selected =
1892 nextProp &&
1893 typeof nextProp !== 'function' &&
1894 typeof nextProp !== 'symbol';
1895 break;
1896 }
1897 default: {
1898 setProp(domElement, tag, propKey, nextProp, nextProps, lastProp);
1899 }
1900 }
1901 }
1902 }
1903 return;
1904 }
1905 case 'img':
1906 case 'link':
1907 case 'area':
1908 case 'base':
1909 case 'br':
1910 case 'col':
1911 case 'embed':
1912 case 'hr':
1913 case 'keygen':
1914 case 'meta':
1915 case 'param':
1916 case 'source':
1917 case 'track':
1918 case 'wbr':
1919 case 'menuitem': {
1920 // Void elements
1921 for (const propKey in lastProps) {
1922 const lastProp = lastProps[propKey];
1923 if (
1924 lastProps.hasOwnProperty(propKey) &&
1925 lastProp != null &&
1926 !nextProps.hasOwnProperty(propKey)
1927 ) {
1928 setProp(domElement, tag, propKey, null, nextProps, lastProp);
1929 }
1930 }
1931 for (const propKey in nextProps) {
1932 const nextProp = nextProps[propKey];
1933 const lastProp = lastProps[propKey];
1934 if (
1935 nextProps.hasOwnProperty(propKey) &&
1936 nextProp !== lastProp &&
1937 (nextProp != null || lastProp != null)
1938 ) {
1939 switch (propKey) {
1940 case 'children':
1941 case 'dangerouslySetInnerHTML': {
1942 if (nextProp != null) {
1943 // TODO: Can we make this a DEV warning to avoid this deny list?
1944 throw new Error(
1945 `${tag} is a void element tag and must neither have \`children\` nor ` +
1946 'use `dangerouslySetInnerHTML`.',
1947 );
1948 }
1949 break;
1950 }
1951 // defaultChecked and defaultValue are ignored by setProp
1952 default: {
1953 setProp(domElement, tag, propKey, nextProp, nextProps, lastProp);
1954 }
1955 }
1956 }
1957 }
1958 return;
1959 }
1960 default: {
1961 if (isCustomElement(tag, nextProps)) {
1962 for (const propKey in lastProps) {
1963 const lastProp = lastProps[propKey];
1964 if (
1965 lastProps.hasOwnProperty(propKey) &&
1966 lastProp !== undefined &&
1967 !nextProps.hasOwnProperty(propKey)
1968 ) {
1969 setPropOnCustomElement(
1970 domElement,
1971 tag,
1972 propKey,
1973 undefined,
1974 nextProps,
1975 lastProp,
1976 );
1977 }
1978 }
1979 for (const propKey in nextProps) {
1980 const nextProp = nextProps[propKey];
1981 const lastProp = lastProps[propKey];
1982 if (
1983 nextProps.hasOwnProperty(propKey) &&
1984 nextProp !== lastProp &&
1985 (nextProp !== undefined || lastProp !== undefined)
1986 ) {
1987 setPropOnCustomElement(
1988 domElement,
1989 tag,
1990 propKey,
1991 nextProp,
1992 nextProps,
1993 lastProp,
1994 );
1995 }
1996 }
1997 return;
1998 }
1999 }
2000 }
2001
2002 for (const propKey in lastProps) {
2003 const lastProp = lastProps[propKey];
2004 if (
2005 lastProps.hasOwnProperty(propKey) &&
2006 lastProp != null &&
2007 !nextProps.hasOwnProperty(propKey)
2008 ) {
2009 setProp(domElement, tag, propKey, null, nextProps, lastProp);
2010 }
2011 }
2012 for (const propKey in nextProps) {
2013 const nextProp = nextProps[propKey];
2014 const lastProp = lastProps[propKey];
2015 if (
2016 nextProps.hasOwnProperty(propKey) &&
2017 nextProp !== lastProp &&
2018 (nextProp != null || lastProp != null)
2019 ) {
2020 setProp(domElement, tag, propKey, nextProp, nextProps, lastProp);
2021 }
2022 }
2023 }
2024
2025 function getPossibleStandardName(propName: string): string | null {
2026 if (__DEV__) {
2027 const lowerCasedName = propName.toLowerCase();
2028 if (!possibleStandardNames.hasOwnProperty(lowerCasedName)) {
2029 return null;
2030 }
2031 return possibleStandardNames[lowerCasedName] || null;
2032 }
2033 return null;
2034 }
2035
2036 function getPropNameFromAttributeName(attrName: string): string {
2037 switch (attrName) {
2038 case 'class':
2039 return 'className';
2040 case 'for':
2041 return 'htmlFor';
2042 // TODO: The rest of the aliases.
2043 default:
2044 return attrName;
2045 }
2046 }
2047
2048 export function getPropsFromElement(domElement: Element): Object {
2049 const serverDifferences: {[propName: string]: mixed} = {};
2050 const attributes = domElement.attributes;
2051 for (let i = 0; i < attributes.length; i++) {
2052 const attr = attributes[i];
2053 serverDifferences[getPropNameFromAttributeName(attr.name)] =
2054 attr.name.toLowerCase() === 'style'
2055 ? getStylesObjectFromElement(domElement)
2056 : attr.value;
2057 }
2058 return serverDifferences;
2059 }
2060
2061 function getStylesObjectFromElement(domElement: Element): {
2062 [styleName: string]: string,
2063 } {
2064 const serverValueInObjectForm: {[prop: string]: string} = {};
2065 const htmlElement: HTMLElement = domElement as any;
2066 const style = htmlElement.style;
2067 for (let i = 0; i < style.length; i++) {
2068 const styleName: string = style[i];
2069 // TODO: We should use the original prop value here if it is equivalent.
2070 // TODO: We could use the original client capitalization if the equivalent
2071 // other capitalization exists in the DOM.
2072 if (
2073 styleName === 'view-transition-name' &&
2074 isExpectedViewTransitionName(htmlElement)
2075 ) {
2076 // This is a view transition name added by the Fizz runtime, not the user's props.
2077 } else {
2078 serverValueInObjectForm[styleName] = style.getPropertyValue(styleName);
2079 }
2080 }
2081 return serverValueInObjectForm;
2082 }
2083
2084 function diffHydratedStyles(
2085 domElement: Element,
2086 value: mixed,
2087 serverDifferences: {[propName: string]: mixed},
2088 ): void {
2089 if (value != null && typeof value !== 'object') {
2090 if (__DEV__) {
2091 console.error(
2092 'The `style` prop expects a mapping from style properties to values, ' +
2093 "not a string. For example, style={{marginRight: spacing + 'em'}} when " +
2094 'using JSX.',
2095 );
2096 }
2097 return;
2098 }
2099 // First we compare the string form and see if it's equivalent.
2100 // This lets us bail out on anything that used to pass in this form.
2101 // It also lets us compare anything that's not parsed by this browser.
2102 const clientValue = createDangerousStringForStyles(value);
2103 const serverValue = domElement.getAttribute('style');
2104
2105 if (serverValue === clientValue) {
2106 return;
2107 }
2108 const normalizedClientValue = normalizeMarkupForTextOrAttribute(clientValue);
2109 const normalizedServerValue = normalizeMarkupForTextOrAttribute(serverValue);
2110 if (normalizedServerValue === normalizedClientValue) {
2111 return;
2112 }
2113
2114 if (
2115 // Trailing semi-colon means this was regenerated.
2116 normalizedServerValue[normalizedServerValue.length - 1] === ';' &&
2117 // TODO: Should we just ignore any style if the style as been manipulated?
2118 hasViewTransition(domElement as any)
2119 ) {
2120 // If this had a view transition we might have applied a view transition
2121 // name/class and removed it. If that happens, the style attribute gets
2122 // regenerated from the style object. This means we've lost the format
2123 // that we sent from the server and is unable to diff it. We just treat
2124 // it as passing even if it should be a mismatch in this edge case.
2125 return;
2126 }
2127
2128 // Otherwise, we create the object from the DOM for the diff view.
2129 serverDifferences.style = getStylesObjectFromElement(domElement);
2130 }
2131
2132 function hydrateAttribute(
2133 domElement: Element,
2134 propKey: string,
2135 attributeName: string,
2136 value: any,
2137 extraAttributes: Set<string>,
2138 serverDifferences: {[propName: string]: mixed},
2139 ): void {
2140 extraAttributes.delete(attributeName);
2141 const serverValue = domElement.getAttribute(attributeName);
2142 if (serverValue === null) {
2143 switch (typeof value) {
2144 case 'undefined':
2145 case 'function':
2146 case 'symbol':
2147 case 'boolean':
2148 return;
2149 }
2150 } else {
2151 if (value == null) {
2152 // We had an attribute but shouldn't have had one, so read it
2153 // for the error message.
2154 } else {
2155 switch (typeof value) {
2156 case 'function':
2157 case 'symbol':
2158 case 'boolean':
2159 break;
2160 default: {
2161 if (__DEV__) {
2162 checkAttributeStringCoercion(value, propKey);
2163 }
2164 // $FlowFixMe[invalid-compare]
2165 if (serverValue === '' + value) {
2166 return;
2167 }
2168 }
2169 }
2170 }
2171 }
2172 warnForPropDifference(propKey, serverValue, value, serverDifferences);
2173 }
2174
2175 function hydrateBooleanAttribute(
2176 domElement: Element,
2177 propKey: string,
2178 attributeName: string,
2179 value: any,
2180 extraAttributes: Set<string>,
2181 serverDifferences: {[propName: string]: mixed},
2182 ): void {
2183 extraAttributes.delete(attributeName);
2184 const serverValue = domElement.getAttribute(attributeName);
2185 if (serverValue === null) {
2186 switch (typeof value) {
2187 case 'function':
2188 case 'symbol':
2189 return;
2190 }
2191 if (!value) {
2192 return;
2193 }
2194 } else {
2195 switch (typeof value) {
2196 case 'function':
2197 case 'symbol':
2198 break;
2199 default: {
2200 if (value) {
2201 // If this was a boolean, it doesn't matter what the value is
2202 // the fact that we have it is the same as the expected.
2203 // As long as it's positive.
2204 return;
2205 }
2206 }
2207 }
2208 }
2209 warnForPropDifference(propKey, serverValue, value, serverDifferences);
2210 }
2211
2212 function hydrateOverloadedBooleanAttribute(
2213 domElement: Element,
2214 propKey: string,
2215 attributeName: string,
2216 value: any,
2217 extraAttributes: Set<string>,
2218 serverDifferences: {[propName: string]: mixed},
2219 ): void {
2220 extraAttributes.delete(attributeName);
2221 const serverValue = domElement.getAttribute(attributeName);
2222 if (serverValue === null) {
2223 switch (typeof value) {
2224 case 'undefined':
2225 case 'function':
2226 case 'symbol':
2227 return;
2228 default:
2229 // $FlowFixMe[invalid-compare]
2230 if (value === false) {
2231 return;
2232 }
2233 }
2234 } else {
2235 if (value == null) {
2236 // We had an attribute but shouldn't have had one, so read it
2237 // for the error message.
2238 } else {
2239 switch (typeof value) {
2240 case 'function':
2241 case 'symbol':
2242 break;
2243 case 'boolean':
2244 // $FlowFixMe[invalid-compare]
2245 if (value === true && serverValue === '') {
2246 return;
2247 }
2248 break;
2249 default: {
2250 if (__DEV__) {
2251 checkAttributeStringCoercion(value, propKey);
2252 }
2253 // $FlowFixMe[invalid-compare]
2254 if (serverValue === '' + value) {
2255 return;
2256 }
2257 }
2258 }
2259 }
2260 }
2261 warnForPropDifference(propKey, serverValue, value, serverDifferences);
2262 }
2263
2264 function hydrateBooleanishAttribute(
2265 domElement: Element,
2266 propKey: string,
2267 attributeName: string,
2268 value: any,
2269 extraAttributes: Set<string>,
2270 serverDifferences: {[propName: string]: mixed},
2271 ): void {
2272 extraAttributes.delete(attributeName);
2273 const serverValue = domElement.getAttribute(attributeName);
2274 if (serverValue === null) {
2275 switch (typeof value) {
2276 case 'undefined':
2277 case 'function':
2278 case 'symbol':
2279 return;
2280 }
2281 } else {
2282 if (value == null) {
2283 // We had an attribute but shouldn't have had one, so read it
2284 // for the error message.
2285 } else {
2286 switch (typeof value) {
2287 case 'function':
2288 case 'symbol':
2289 break;
2290 default: {
2291 if (__DEV__) {
2292 checkAttributeStringCoercion(value, attributeName);
2293 }
2294 if (serverValue === '' + (value as any)) {
2295 return;
2296 }
2297 }
2298 }
2299 }
2300 }
2301 warnForPropDifference(propKey, serverValue, value, serverDifferences);
2302 }
2303
2304 function hydrateNumericAttribute(
2305 domElement: Element,
2306 propKey: string,
2307 attributeName: string,
2308 value: any,
2309 extraAttributes: Set<string>,
2310 serverDifferences: {[propName: string]: mixed},
2311 ): void {
2312 extraAttributes.delete(attributeName);
2313 const serverValue = domElement.getAttribute(attributeName);
2314 if (serverValue === null) {
2315 switch (typeof value) {
2316 case 'undefined':
2317 case 'function':
2318 case 'symbol':
2319 case 'boolean':
2320 return;
2321 default:
2322 if (isNaN(value)) {
2323 return;
2324 }
2325 }
2326 } else {
2327 if (value == null) {
2328 // We had an attribute but shouldn't have had one, so read it
2329 // for the error message.
2330 } else {
2331 switch (typeof value) {
2332 case 'function':
2333 case 'symbol':
2334 case 'boolean':
2335 break;
2336 default: {
2337 if (isNaN(value)) {
2338 // We had an attribute but shouldn't have had one, so read it
2339 // for the error message.
2340 break;
2341 }
2342 if (__DEV__) {
2343 checkAttributeStringCoercion(value, propKey);
2344 }
2345 // $FlowFixMe[invalid-compare]
2346 if (serverValue === '' + value) {
2347 return;
2348 }
2349 }
2350 }
2351 }
2352 }
2353 warnForPropDifference(propKey, serverValue, value, serverDifferences);
2354 }
2355
2356 function hydratePositiveNumericAttribute(
2357 domElement: Element,
2358 propKey: string,
2359 attributeName: string,
2360 value: any,
2361 extraAttributes: Set<string>,
2362 serverDifferences: {[propName: string]: mixed},
2363 ): void {
2364 extraAttributes.delete(attributeName);
2365 const serverValue = domElement.getAttribute(attributeName);
2366 if (serverValue === null) {
2367 switch (typeof value) {
2368 case 'undefined':
2369 case 'function':
2370 case 'symbol':
2371 case 'boolean':
2372 return;
2373 default:
2374 if (isNaN(value) || value < 1) {
2375 return;
2376 }
2377 }
2378 } else {
2379 if (value == null) {
2380 // We had an attribute but shouldn't have had one, so read it
2381 // for the error message.
2382 } else {
2383 switch (typeof value) {
2384 case 'function':
2385 case 'symbol':
2386 case 'boolean':
2387 break;
2388 default: {
2389 if (isNaN(value) || value < 1) {
2390 // We had an attribute but shouldn't have had one, so read it
2391 // for the error message.
2392 break;
2393 }
2394 if (__DEV__) {
2395 checkAttributeStringCoercion(value, propKey);
2396 }
2397 // $FlowFixMe[invalid-compare]
2398 if (serverValue === '' + value) {
2399 return;
2400 }
2401 }
2402 }
2403 }
2404 }
2405 warnForPropDifference(propKey, serverValue, value, serverDifferences);
2406 }
2407
2408 function hydrateSanitizedAttribute(
2409 domElement: Element,
2410 propKey: string,
2411 attributeName: string,
2412 value: any,
2413 extraAttributes: Set<string>,
2414 serverDifferences: {[propName: string]: mixed},
2415 ): void {
2416 extraAttributes.delete(attributeName);
2417 const serverValue = domElement.getAttribute(attributeName);
2418 if (serverValue === null) {
2419 switch (typeof value) {
2420 case 'undefined':
2421 case 'function':
2422 case 'symbol':
2423 case 'boolean':
2424 return;
2425 }
2426 } else {
2427 if (value == null) {
2428 // We had an attribute but shouldn't have had one, so read it
2429 // for the error message.
2430 } else {
2431 switch (typeof value) {
2432 case 'function':
2433 case 'symbol':
2434 case 'boolean':
2435 break;
2436 default: {
2437 if (__DEV__) {
2438 checkAttributeStringCoercion(value, propKey);
2439 }
2440 const sanitizedValue = sanitizeURL('' + value);
2441 if (serverValue === sanitizedValue) {
2442 return;
2443 }
2444 }
2445 }
2446 }
2447 }
2448 warnForPropDifference(propKey, serverValue, value, serverDifferences);
2449 }
2450
2451 function hydrateSrcObjectAttribute(
2452 domElement: Element,
2453 value: Blob,
2454 extraAttributes: Set<string>,
2455 serverDifferences: {[propName: string]: mixed},
2456 ): void {
2457 const attributeName = 'src';
2458 extraAttributes.delete(attributeName);
2459 const serverValue = domElement.getAttribute(attributeName);
2460 if (serverValue != null && value != null) {
2461 const size = value.size;
2462 const type = value.type;
2463 if (typeof size === 'number' && typeof type === 'string') {
2464 if (serverValue.indexOf('data:' + type + ';base64,') === 0) {
2465 // For Blobs we don't bother reading the actual data but just diff by checking if
2466 // the byte length size of the Blob maches the length of the data url.
2467 const prefixLength = 5 + type.length + 8;
2468 let byteLength = ((serverValue.length - prefixLength) / 4) * 3;
2469 if (serverValue[serverValue.length - 1] === '=') {
2470 byteLength--;
2471 }
2472 if (serverValue[serverValue.length - 2] === '=') {
2473 byteLength--;
2474 }
2475 if (byteLength === size) {
2476 return;
2477 }
2478 }
2479 }
2480 }
2481 warnForPropDifference('src', serverValue, value, serverDifferences);
2482 }
2483
2484 function diffHydratedCustomComponent(
2485 domElement: Element,
2486 tag: string,
2487 props: Object,
2488 hostContext: HostContext,
2489 extraAttributes: Set<string>,
2490 serverDifferences: {[propName: string]: mixed},
2491 ) {
2492 for (const propKey in props) {
2493 if (!props.hasOwnProperty(propKey)) {
2494 continue;
2495 }
2496 const value = props[propKey];
2497 if (value == null) {
2498 continue;
2499 }
2500 if (registrationNameDependencies.hasOwnProperty(propKey)) {
2501 if (typeof value !== 'function') {
2502 warnForInvalidEventListener(propKey, value);
2503 }
2504 continue;
2505 }
2506 if (props.suppressHydrationWarning === true) {
2507 // Don't bother comparing. We're ignoring all these warnings.
2508 continue;
2509 }
2510 // Validate that the properties correspond to their expected values.
2511 switch (propKey) {
2512 case 'children': {
2513 if (typeof value === 'string' || typeof value === 'number') {
2514 warnForPropDifference(
2515 'children',
2516 domElement.textContent,
2517 value,
2518 serverDifferences,
2519 );
2520 }
2521 continue;
2522 }
2523 // Checked above already
2524 case 'suppressContentEditableWarning':
2525 case 'suppressHydrationWarning':
2526 case 'defaultValue':
2527 case 'defaultChecked':
2528 case 'innerHTML':
2529 case 'ref':
2530 // Noop
2531 continue;
2532 case 'dangerouslySetInnerHTML':
2533 const serverHTML = domElement.innerHTML;
2534 const nextHtml = value ? value.__html : undefined;
2535 if (nextHtml != null) {
2536 const expectedHTML = normalizeHTML(domElement, nextHtml);
2537 warnForPropDifference(
2538 propKey,
2539 serverHTML,
2540 expectedHTML,
2541 serverDifferences,
2542 );
2543 }
2544 continue;
2545 case 'style':
2546 extraAttributes.delete(propKey);
2547 diffHydratedStyles(domElement, value, serverDifferences);
2548 continue;
2549 case 'offsetParent':
2550 case 'offsetTop':
2551 case 'offsetLeft':
2552 case 'offsetWidth':
2553 case 'offsetHeight':
2554 case 'isContentEditable':
2555 case 'outerText':
2556 case 'outerHTML':
2557 extraAttributes.delete(propKey.toLowerCase());
2558 if (__DEV__) {
2559 console.error(
2560 'Assignment to read-only property will result in a no-op: `%s`',
2561 propKey,
2562 );
2563 }
2564 continue;
2565 // Fall through
2566 case 'className':
2567 // className is a special cased property on the server to render as an attribute.
2568 extraAttributes.delete('class');
2569 const serverValue = getValueForAttributeOnCustomComponent(
2570 domElement,
2571 'class',
2572 value,
2573 );
2574 warnForPropDifference(
2575 'className',
2576 serverValue,
2577 value,
2578 serverDifferences,
2579 );
2580 continue;
2581 default: {
2582 // This is a DEV-only path
2583 const hostContextDev: HostContextDev = hostContext as any;
2584 const hostContextProd = hostContextDev.context;
2585 if (
2586 hostContextProd === HostContextNamespaceNone &&
2587 tag !== 'svg' &&
2588 tag !== 'math'
2589 ) {
2590 extraAttributes.delete(propKey.toLowerCase());
2591 } else {
2592 extraAttributes.delete(propKey);
2593 }
2594 const valueOnCustomComponent = getValueForAttributeOnCustomComponent(
2595 domElement,
2596 propKey,
2597 value,
2598 );
2599 warnForPropDifference(
2600 propKey,
2601 valueOnCustomComponent,
2602 value,
2603 serverDifferences,
2604 );
2605 }
2606 }
2607 }
2608 }
2609
2610 // This is the exact URL string we expect that Fizz renders if we provide a function action.
2611 // We use this for hydration warnings. It needs to be in sync with Fizz. Maybe makes sense
2612 // as a shared module for that reason.
2613 const EXPECTED_FORM_ACTION_URL =
2614 // eslint-disable-next-line no-script-url
2615 "javascript:throw new Error('React form unexpectedly submitted.')";
2616
2617 function diffHydratedGenericElement(
2618 domElement: Element,
2619 tag: string,
2620 props: Object,
2621 hostContext: HostContext,
2622 extraAttributes: Set<string>,
2623 serverDifferences: {[propName: string]: mixed},
2624 ) {
2625 for (const propKey in props) {
2626 if (!props.hasOwnProperty(propKey)) {
2627 continue;
2628 }
2629 const value = props[propKey];
2630 if (value == null) {
2631 continue;
2632 }
2633 if (registrationNameDependencies.hasOwnProperty(propKey)) {
2634 if (typeof value !== 'function') {
2635 warnForInvalidEventListener(propKey, value);
2636 }
2637 continue;
2638 }
2639 if (props.suppressHydrationWarning === true) {
2640 // Don't bother comparing. We're ignoring all these warnings.
2641 continue;
2642 }
2643 // Validate that the properties correspond to their expected values.
2644 switch (propKey) {
2645 case 'children': {
2646 if (typeof value === 'string' || typeof value === 'number') {
2647 warnForPropDifference(
2648 'children',
2649 domElement.textContent,
2650 value,
2651 serverDifferences,
2652 );
2653 }
2654 continue;
2655 }
2656 // Checked above already
2657 case 'suppressContentEditableWarning':
2658 case 'suppressHydrationWarning':
2659 case 'value': // Controlled attributes are not validated
2660 case 'checked': // TODO: Only ignore them on controlled tags.
2661 case 'selected':
2662 case 'defaultValue':
2663 case 'defaultChecked':
2664 case 'innerHTML':
2665 case 'ref':
2666 // Noop
2667 continue;
2668 case 'dangerouslySetInnerHTML':
2669 const serverHTML = domElement.innerHTML;
2670 const nextHtml = value ? value.__html : undefined;
2671 if (nextHtml != null) {
2672 const expectedHTML = normalizeHTML(domElement, nextHtml);
2673 if (serverHTML !== expectedHTML) {
2674 serverDifferences[propKey] = {
2675 __html: serverHTML,
2676 };
2677 }
2678 }
2679 continue;
2680 case 'className':
2681 hydrateAttribute(
2682 domElement,
2683 propKey,
2684 'class',
2685 value,
2686 extraAttributes,
2687 serverDifferences,
2688 );
2689 continue;
2690 case 'tabIndex':
2691 hydrateAttribute(
2692 domElement,
2693 propKey,
2694 'tabindex',
2695 value,
2696 extraAttributes,
2697 serverDifferences,
2698 );
2699 continue;
2700 case 'style':
2701 extraAttributes.delete(propKey);
2702 diffHydratedStyles(domElement, value, serverDifferences);
2703 continue;
2704 case 'multiple': {
2705 extraAttributes.delete(propKey);
2706 const serverValue = (domElement as any).multiple;
2707 warnForPropDifference(propKey, serverValue, value, serverDifferences);
2708 continue;
2709 }
2710 case 'muted': {
2711 extraAttributes.delete(propKey);
2712 const serverValue = (domElement as any).muted;
2713 warnForPropDifference(propKey, serverValue, value, serverDifferences);
2714 continue;
2715 }
2716 case 'autoFocus': {
2717 extraAttributes.delete('autofocus');
2718 const serverValue = (domElement as any).autofocus;
2719 warnForPropDifference(propKey, serverValue, value, serverDifferences);
2720 continue;
2721 }
2722 case 'data':
2723 if (tag !== 'object') {
2724 extraAttributes.delete(propKey);
2725 const serverValue = (domElement as any).getAttribute('data');
2726 warnForPropDifference(propKey, serverValue, value, serverDifferences);
2727 continue;
2728 }
2729 // fallthrough
2730 case 'src': {
2731 if (enableSrcObject && typeof value === 'object' && value !== null) {
2732 // Some tags support object sources like Blob, File, MediaSource and MediaStream.
2733 if (tag === 'img' || tag === 'video' || tag === 'audio') {
2734 try {
2735 // Test if this is a compatible object
2736 URL.revokeObjectURL(URL.createObjectURL(value as any));
2737 hydrateSrcObjectAttribute(
2738 domElement,
2739 value,
2740 extraAttributes,
2741 serverDifferences,
2742 );
2743 continue;
2744 } catch (x) {
2745 // If not, just fall through to the normal toString flow.
2746 }
2747 } else {
2748 if (__DEV__) {
2749 try {
2750 // This should always error.
2751 URL.revokeObjectURL(URL.createObjectURL(value as any));
2752 if (tag === 'source') {
2753 console.error(
2754 'Passing Blob, MediaSource or MediaStream to <source src> is not supported. ' +
2755 'Pass it directly to <img src>, <video src> or <audio src> instead.',
2756 );
2757 } else {
2758 console.error(
2759 'Passing Blob, MediaSource or MediaStream to <%s src> is not supported.',
2760 tag,
2761 );
2762 }
2763 } catch (x) {}
2764 }
2765 }
2766 }
2767 // Fallthrough
2768 }
2769 case 'href':
2770 if (
2771 value === '' &&
2772 // <a href=""> is fine for "reload" links.
2773 !(tag === 'a' && propKey === 'href') &&
2774 !(tag === 'object' && propKey === 'data')
2775 ) {
2776 if (__DEV__) {
2777 if (propKey === 'src') {
2778 console.error(
2779 'An empty string ("") was passed to the %s attribute. ' +
2780 'This may cause the browser to download the whole page again over the network. ' +
2781 'To fix this, either do not render the element at all ' +
2782 'or pass null to %s instead of an empty string.',
2783 propKey,
2784 propKey,
2785 );
2786 } else {
2787 console.error(
2788 'An empty string ("") was passed to the %s attribute. ' +
2789 'To fix this, either do not render the element at all ' +
2790 'or pass null to %s instead of an empty string.',
2791 propKey,
2792 propKey,
2793 );
2794 }
2795 }
2796 continue;
2797 }
2798 hydrateSanitizedAttribute(
2799 domElement,
2800 propKey,
2801 propKey,
2802 value,
2803 extraAttributes,
2804 serverDifferences,
2805 );
2806 continue;
2807 case 'action':
2808 case 'formAction': {
2809 const serverValue = domElement.getAttribute(propKey);
2810 if (typeof value === 'function') {
2811 extraAttributes.delete(propKey.toLowerCase());
2812 // The server can set these extra properties to implement actions.
2813 // So we remove them from the extra attributes warnings.
2814 if (propKey === 'formAction') {
2815 extraAttributes.delete('name');
2816 extraAttributes.delete('formenctype');
2817 extraAttributes.delete('formmethod');
2818 extraAttributes.delete('formtarget');
2819 } else {
2820 extraAttributes.delete('enctype');
2821 extraAttributes.delete('method');
2822 extraAttributes.delete('target');
2823 }
2824 // Ideally we should be able to warn if the server value was not a function
2825 // however since the function can return any of these attributes any way it
2826 // wants as a custom progressive enhancement, there's nothing to compare to.
2827 // We can check if the function has the $FORM_ACTION property on the client
2828 // and if it's not, warn, but that's an unnecessary constraint that they
2829 // have to have the extra extension that doesn't do anything on the client.
2830 continue;
2831 } else if (serverValue === EXPECTED_FORM_ACTION_URL) {
2832 extraAttributes.delete(propKey.toLowerCase());
2833 warnForPropDifference(propKey, 'function', value, serverDifferences);
2834 continue;
2835 }
2836 hydrateSanitizedAttribute(
2837 domElement,
2838 propKey,
2839 propKey.toLowerCase(),
2840 value,
2841 extraAttributes,
2842 serverDifferences,
2843 );
2844 continue;
2845 }
2846 case 'xlinkHref':
2847 hydrateSanitizedAttribute(
2848 domElement,
2849 propKey,
2850 'xlink:href',
2851 value,
2852 extraAttributes,
2853 serverDifferences,
2854 );
2855 continue;
2856 case 'contentEditable': {
2857 // Lower-case Booleanish String
2858 hydrateBooleanishAttribute(
2859 domElement,
2860 propKey,
2861 'contenteditable',
2862 value,
2863 extraAttributes,
2864 serverDifferences,
2865 );
2866 continue;
2867 }
2868 case 'spellCheck': {
2869 // Lower-case Booleanish String
2870 hydrateBooleanishAttribute(
2871 domElement,
2872 propKey,
2873 'spellcheck',
2874 value,
2875 extraAttributes,
2876 serverDifferences,
2877 );
2878 continue;
2879 }
2880 case 'draggable':
2881 case 'autoReverse':
2882 case 'externalResourcesRequired':
2883 case 'focusable':
2884 case 'preserveAlpha': {
2885 // Case-sensitive Booleanish String
2886 hydrateBooleanishAttribute(
2887 domElement,
2888 propKey,
2889 propKey,
2890 value,
2891 extraAttributes,
2892 serverDifferences,
2893 );
2894 continue;
2895 }
2896 case 'allowFullScreen':
2897 case 'async':
2898 case 'autoPlay':
2899 case 'controls':
2900 case 'credentialless':
2901 case 'default':
2902 case 'defer':
2903 case 'disabled':
2904 case 'disablePictureInPicture':
2905 case 'disableRemotePlayback':
2906 case 'formNoValidate':
2907 case 'hidden':
2908 case 'loop':
2909 case 'noModule':
2910 case 'noValidate':
2911 case 'open':
2912 case 'playsInline':
2913 case 'readOnly':
2914 case 'required':
2915 case 'reversed':
2916 case 'scoped':
2917 case 'seamless':
2918 case 'itemScope': {
2919 // Some of these need to be lower case to remove them from the extraAttributes list.
2920 hydrateBooleanAttribute(
2921 domElement,
2922 propKey,
2923 propKey.toLowerCase(),
2924 value,
2925 extraAttributes,
2926 serverDifferences,
2927 );
2928 continue;
2929 }
2930 case 'capture':
2931 case 'download': {
2932 hydrateOverloadedBooleanAttribute(
2933 domElement,
2934 propKey,
2935 propKey,
2936 value,
2937 extraAttributes,
2938 serverDifferences,
2939 );
2940 continue;
2941 }
2942 case 'cols':
2943 case 'rows':
2944 case 'size':
2945 case 'span': {
2946 hydratePositiveNumericAttribute(
2947 domElement,
2948 propKey,
2949 propKey,
2950 value,
2951 extraAttributes,
2952 serverDifferences,
2953 );
2954 continue;
2955 }
2956 case 'rowSpan': {
2957 hydrateNumericAttribute(
2958 domElement,
2959 propKey,
2960 'rowspan',
2961 value,
2962 extraAttributes,
2963 serverDifferences,
2964 );
2965 continue;
2966 }
2967 case 'start': {
2968 hydrateNumericAttribute(
2969 domElement,
2970 propKey,
2971 propKey,
2972 value,
2973 extraAttributes,
2974 serverDifferences,
2975 );
2976 continue;
2977 }
2978 case 'xHeight':
2979 hydrateAttribute(
2980 domElement,
2981 propKey,
2982 'x-height',
2983 value,
2984 extraAttributes,
2985 serverDifferences,
2986 );
2987 continue;
2988 case 'xlinkActuate':
2989 hydrateAttribute(
2990 domElement,
2991 propKey,
2992 'xlink:actuate',
2993 value,
2994 extraAttributes,
2995 serverDifferences,
2996 );
2997 continue;
2998 case 'xlinkArcrole':
2999 hydrateAttribute(
3000 domElement,
3001 propKey,
3002 'xlink:arcrole',
3003 value,
3004 extraAttributes,
3005 serverDifferences,
3006 );
3007 continue;
3008 case 'xlinkRole':
3009 hydrateAttribute(
3010 domElement,
3011 propKey,
3012 'xlink:role',
3013 value,
3014 extraAttributes,
3015 serverDifferences,
3016 );
3017 continue;
3018 case 'xlinkShow':
3019 hydrateAttribute(
3020 domElement,
3021 propKey,
3022 'xlink:show',
3023 value,
3024 extraAttributes,
3025 serverDifferences,
3026 );
3027 continue;
3028 case 'xlinkTitle':
3029 hydrateAttribute(
3030 domElement,
3031 propKey,
3032 'xlink:title',
3033 value,
3034 extraAttributes,
3035 serverDifferences,
3036 );
3037 continue;
3038 case 'xlinkType':
3039 hydrateAttribute(
3040 domElement,
3041 propKey,
3042 'xlink:type',
3043 value,
3044 extraAttributes,
3045 serverDifferences,
3046 );
3047 continue;
3048 case 'xmlBase':
3049 hydrateAttribute(
3050 domElement,
3051 propKey,
3052 'xml:base',
3053 value,
3054 extraAttributes,
3055 serverDifferences,
3056 );
3057 continue;
3058 case 'xmlLang':
3059 hydrateAttribute(
3060 domElement,
3061 propKey,
3062 'xml:lang',
3063 value,
3064 extraAttributes,
3065 serverDifferences,
3066 );
3067 continue;
3068 case 'xmlSpace':
3069 hydrateAttribute(
3070 domElement,
3071 propKey,
3072 'xml:space',
3073 value,
3074 extraAttributes,
3075 serverDifferences,
3076 );
3077 continue;
3078 case 'inert':
3079 if (__DEV__) {
3080 if (
3081 value === '' &&
3082 !didWarnForNewBooleanPropsWithEmptyValue[propKey]
3083 ) {
3084 didWarnForNewBooleanPropsWithEmptyValue[propKey] = true;
3085 console.error(
3086 'Received an empty string for a boolean attribute `%s`. ' +
3087 'This will treat the attribute as if it were false. ' +
3088 'Either pass `false` to silence this warning, or ' +
3089 'pass `true` if you used an empty string in earlier versions of React to indicate this attribute is true.',
3090 propKey,
3091 );
3092 }
3093 }
3094 hydrateBooleanAttribute(
3095 domElement,
3096 propKey,
3097 propKey,
3098 value,
3099 extraAttributes,
3100 serverDifferences,
3101 );
3102 continue;
3103 default: {
3104 if (
3105 // shouldIgnoreAttribute
3106 // We have already filtered out null/undefined and reserved words.
3107 propKey.length > 2 &&
3108 (propKey[0] === 'o' || propKey[0] === 'O') &&
3109 (propKey[1] === 'n' || propKey[1] === 'N')
3110 ) {
3111 continue;
3112 }
3113 const attributeName = getAttributeAlias(propKey);
3114 let isMismatchDueToBadCasing = false;
3115
3116 // This is a DEV-only path
3117 const hostContextDev: HostContextDev = hostContext as any;
3118 const hostContextProd = hostContextDev.context;
3119
3120 if (
3121 hostContextProd === HostContextNamespaceNone &&
3122 tag !== 'svg' &&
3123 tag !== 'math'
3124 ) {
3125 extraAttributes.delete(attributeName.toLowerCase());
3126 } else {
3127 const standardName = getPossibleStandardName(propKey);
3128 if (standardName !== null && standardName !== propKey) {
3129 // If an SVG prop is supplied with bad casing, it will
3130 // be successfully parsed from HTML, but will produce a mismatch
3131 // (and would be incorrectly rendered on the client).
3132 // However, we already warn about bad casing elsewhere.
3133 // So we'll skip the misleading extra mismatch warning in this case.
3134 isMismatchDueToBadCasing = true;
3135 extraAttributes.delete(standardName);
3136 }
3137 extraAttributes.delete(attributeName);
3138 }
3139 const serverValue = getValueForAttribute(
3140 domElement,
3141 attributeName,
3142 value,
3143 );
3144 if (!isMismatchDueToBadCasing) {
3145 warnForPropDifference(propKey, serverValue, value, serverDifferences);
3146 }
3147 }
3148 }
3149 }
3150 }
3151
3152 export function hydrateProperties(
3153 domElement: Element,
3154 tag: string,
3155 props: Object,
3156 hostContext: HostContext,
3157 ): boolean {
3158 if (__DEV__) {
3159 validatePropertiesInDevelopment(tag, props);
3160 }
3161
3162 // TODO: Make sure that we check isMounted before firing any of these events.
3163 switch (tag) {
3164 case 'dialog':
3165 listenToNonDelegatedEvent('cancel', domElement);
3166 listenToNonDelegatedEvent('close', domElement);
3167 break;
3168 case 'iframe':
3169 case 'object':
3170 case 'embed':
3171 // We listen to this event in case to ensure emulated bubble
3172 // listeners still fire for the load event.
3173 listenToNonDelegatedEvent('load', domElement);
3174 break;
3175 case 'video':
3176 case 'audio':
3177 // We listen to these events in case to ensure emulated bubble
3178 // listeners still fire for all the media events.
3179 for (let i = 0; i < mediaEventTypes.length; i++) {
3180 listenToNonDelegatedEvent(mediaEventTypes[i], domElement);
3181 }
3182 break;
3183 case 'source':
3184 // We listen to this event in case to ensure emulated bubble
3185 // listeners still fire for the error event.
3186 listenToNonDelegatedEvent('error', domElement);
3187 break;
3188 case 'img':
3189 case 'image':
3190 case 'link':
3191 // We listen to these events in case to ensure emulated bubble
3192 // listeners still fire for error and load events.
3193 listenToNonDelegatedEvent('error', domElement);
3194 listenToNonDelegatedEvent('load', domElement);
3195 break;
3196 case 'details':
3197 // We listen to this event in case to ensure emulated bubble
3198 // listeners still fire for the toggle event.
3199 listenToNonDelegatedEvent('toggle', domElement);
3200 break;
3201 case 'input':
3202 if (__DEV__) {
3203 checkControlledValueProps('input', props);
3204 }
3205 // We listen to this event in case to ensure emulated bubble
3206 // listeners still fire for the invalid event.
3207 listenToNonDelegatedEvent('invalid', domElement);
3208 // TODO: Make sure we check if this is still unmounted or do any clean
3209 // up necessary since we never stop tracking anymore.
3210 validateInputProps(domElement, props);
3211 // For input and textarea we current always set the value property at
3212 // post mount to force it to diverge from attributes. However, for
3213 // option and select we don't quite do the same thing and select
3214 // is not resilient to the DOM state changing so we don't do that here.
3215 // TODO: Consider not doing this for input and textarea.
3216 if (!enableHydrationChangeEvent) {
3217 initInput(
3218 domElement,
3219 props.value,
3220 props.defaultValue,
3221 props.checked,
3222 props.defaultChecked,
3223 props.type,
3224 props.name,
3225 true,
3226 );
3227 }
3228 break;
3229 case 'option':
3230 validateOptionProps(domElement, props);
3231 break;
3232 case 'select':
3233 if (__DEV__) {
3234 checkControlledValueProps('select', props);
3235 }
3236 // We listen to this event in case to ensure emulated bubble
3237 // listeners still fire for the invalid event.
3238 listenToNonDelegatedEvent('invalid', domElement);
3239 validateSelectProps(domElement, props);
3240 break;
3241 case 'textarea':
3242 if (__DEV__) {
3243 checkControlledValueProps('textarea', props);
3244 }
3245 // We listen to this event in case to ensure emulated bubble
3246 // listeners still fire for the invalid event.
3247 listenToNonDelegatedEvent('invalid', domElement);
3248 // TODO: Make sure we check if this is still unmounted or do any clean
3249 // up necessary since we never stop tracking anymore.
3250 validateTextareaProps(domElement, props);
3251 if (!enableHydrationChangeEvent) {
3252 initTextarea(
3253 domElement,
3254 props.value,
3255 props.defaultValue,
3256 props.children,
3257 );
3258 }
3259 break;
3260 }
3261
3262 const children = props.children;
3263 // For text content children we compare against textContent. This
3264 // might match additional HTML that is hidden when we read it using
3265 // textContent. E.g. "foo" will match "f<span>oo</span>" but that still
3266 // satisfies our requirement. Our requirement is not to produce perfect
3267 // HTML and attributes. Ideally we should preserve structure but it's
3268 // ok not to if the visible content is still enough to indicate what
3269 // even listeners these nodes might be wired up to.
3270 // TODO: Warn if there is more than a single textNode as a child.
3271 // TODO: Should we use domElement.firstChild.nodeValue to compare?
3272 if (
3273 typeof children === 'string' ||
3274 typeof children === 'number' ||
3275 typeof children === 'bigint'
3276 ) {
3277 if (
3278 // $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint
3279 domElement.textContent !== '' + children &&
3280 props.suppressHydrationWarning !== true &&
3281 !checkForUnmatchedText(domElement.textContent, children)
3282 ) {
3283 return false;
3284 }
3285 }
3286
3287 if (props.popover != null) {
3288 // We listen to this event in case to ensure emulated bubble
3289 // listeners still fire for the toggle event.
3290 listenToNonDelegatedEvent('beforetoggle', domElement);
3291 listenToNonDelegatedEvent('toggle', domElement);
3292 }
3293
3294 if (props.onScroll != null) {
3295 listenToNonDelegatedEvent('scroll', domElement);
3296 }
3297
3298 if (props.onScrollEnd != null) {
3299 listenToNonDelegatedEvent('scrollend', domElement);
3300 if (enableScrollEndPolyfill) {
3301 // For use by the polyfill.
3302 listenToNonDelegatedEvent('scroll', domElement);
3303 }
3304 }
3305
3306 if (props.onClick != null) {
3307 // TODO: This cast may not be sound for SVG, MathML or custom elements.
3308 trapClickOnNonInteractiveElement(domElement as any as HTMLElement);
3309 }
3310
3311 return true;
3312 }
3313
3314 export function diffHydratedProperties(
3315 domElement: Element,
3316 tag: string,
3317 props: Object,
3318 hostContext: HostContext,
3319 ): null | Object {
3320 const serverDifferences: {[propName: string]: mixed} = {};
3321 if (__DEV__) {
3322 const extraAttributes: Set<string> = new Set();
3323 const attributes = domElement.attributes;
3324 for (let i = 0; i < attributes.length; i++) {
3325 const name = attributes[i].name.toLowerCase();
3326 switch (name) {
3327 // Controlled attributes are not validated
3328 // TODO: Only ignore them on controlled tags.
3329 case 'value':
3330 break;
3331 case 'checked':
3332 break;
3333 case 'selected':
3334 break;
3335 case 'vt-name':
3336 case 'vt-update':
3337 case 'vt-enter':
3338 case 'vt-exit':
3339 case 'vt-share':
3340 case 'vt-parent-enter':
3341 case 'vt-parent-exit':
3342 if (enableViewTransition) {
3343 // View Transition annotations are expected from the Server Runtime.
3344 // However, if they're also specified on the client and don't match
3345 // that's an error.
3346 break;
3347 }
3348 // Fallthrough
3349 default:
3350 // Intentionally use the original name.
3351 // See discussion in https://github.com/facebook/react/pull/10676.
3352 extraAttributes.add(attributes[i].name);
3353 }
3354 }
3355 if (isCustomElement(tag, props)) {
3356 diffHydratedCustomComponent(
3357 domElement,
3358 tag,
3359 props,
3360 hostContext,
3361 extraAttributes,
3362 serverDifferences,
3363 );
3364 } else {
3365 diffHydratedGenericElement(
3366 domElement,
3367 tag,
3368 props,
3369 hostContext,
3370 extraAttributes,
3371 serverDifferences,
3372 );
3373 }
3374 if (extraAttributes.size > 0 && props.suppressHydrationWarning !== true) {
3375 warnForExtraAttributes(domElement, extraAttributes, serverDifferences);
3376 }
3377 }
3378 if (Object.keys(serverDifferences).length === 0) {
3379 return null;
3380 }
3381 return serverDifferences;
3382 }
3383
3384 export function hydrateText(
3385 textNode: Text,
3386 text: string,
3387 parentProps: null | Object,
3388 ): boolean {
3389 const isDifferent = textNode.nodeValue !== text;
3390 if (
3391 isDifferent &&
3392 (parentProps === null || parentProps.suppressHydrationWarning !== true) &&
3393 !checkForUnmatchedText(textNode.nodeValue, text)
3394 ) {
3395 return false;
3396 }
3397 return true;
3398 }
3399
3400 export function diffHydratedText(textNode: Text, text: string): null | string {
3401 if (textNode.nodeValue === text) {
3402 return null;
3403 }
3404 const normalizedClientText = normalizeMarkupForTextOrAttribute(text);
3405 const normalizedServerText = normalizeMarkupForTextOrAttribute(
3406 textNode.nodeValue,
3407 );
3408 if (normalizedServerText === normalizedClientText) {
3409 return null;
3410 }
3411 return textNode.nodeValue;
3412 }
3413
3414 export function restoreControlledState(
3415 domElement: Element,
3416 tag: string,
3417 props: Object,
3418 ): void {
3419 switch (tag) {
3420 case 'input':
3421 restoreControlledInputState(domElement, props);
3422 return;
3423 case 'textarea':
3424 restoreControlledTextareaState(domElement, props);
3425 return;
3426 case 'select':
3427 restoreControlledSelectState(domElement, props);
3428 return;
3429 }
3430 }