mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
3131 lines
93 KiB
JavaScript
3131 lines
93 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
import type {HostContext, HostContextDev} from './ReactFiberConfigDOM';
|
|
|
|
import {HostContextNamespaceNone} from './ReactFiberConfigDOM';
|
|
|
|
import {
|
|
registrationNameDependencies,
|
|
possibleRegistrationNames,
|
|
} from '../events/EventRegistry';
|
|
|
|
import {checkHtmlStringCoercion} from 'shared/CheckStringCoercion';
|
|
import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
|
|
import {checkControlledValueProps} from '../shared/ReactControlledValuePropTypes';
|
|
|
|
import {
|
|
getValueForAttribute,
|
|
getValueForAttributeOnCustomComponent,
|
|
setValueForPropertyOnCustomComponent,
|
|
setValueForKnownAttribute,
|
|
setValueForAttribute,
|
|
setValueForNamespacedAttribute,
|
|
} from './DOMPropertyOperations';
|
|
import {
|
|
validateInputProps,
|
|
initInput,
|
|
updateInput,
|
|
restoreControlledInputState,
|
|
} from './ReactDOMInput';
|
|
import {validateOptionProps} from './ReactDOMOption';
|
|
import {
|
|
validateSelectProps,
|
|
initSelect,
|
|
restoreControlledSelectState,
|
|
updateSelect,
|
|
} from './ReactDOMSelect';
|
|
import {
|
|
validateTextareaProps,
|
|
initTextarea,
|
|
updateTextarea,
|
|
restoreControlledTextareaState,
|
|
} from './ReactDOMTextarea';
|
|
import {validateTextNesting} from './validateDOMNesting';
|
|
import {track} from './inputValueTracking';
|
|
import setTextContent from './setTextContent';
|
|
import {
|
|
createDangerousStringForStyles,
|
|
setValueForStyles,
|
|
} from './CSSPropertyOperations';
|
|
import {SVG_NAMESPACE, MATH_NAMESPACE} from './DOMNamespaces';
|
|
import isCustomElement from '../shared/isCustomElement';
|
|
import getAttributeAlias from '../shared/getAttributeAlias';
|
|
import possibleStandardNames from '../shared/possibleStandardNames';
|
|
import {validateProperties as validateARIAProperties} from '../shared/ReactDOMInvalidARIAHook';
|
|
import {validateProperties as validateInputProperties} from '../shared/ReactDOMNullInputValuePropHook';
|
|
import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook';
|
|
import sanitizeURL from '../shared/sanitizeURL';
|
|
|
|
import {enableTrustedTypesIntegration} from 'shared/ReactFeatureFlags';
|
|
import {
|
|
mediaEventTypes,
|
|
listenToNonDelegatedEvent,
|
|
} from '../events/DOMPluginEventSystem';
|
|
|
|
let didWarnControlledToUncontrolled = false;
|
|
let didWarnUncontrolledToControlled = false;
|
|
let didWarnFormActionType = false;
|
|
let didWarnFormActionName = false;
|
|
let didWarnFormActionTarget = false;
|
|
let didWarnFormActionMethod = false;
|
|
let didWarnForNewBooleanPropsWithEmptyValue: {[string]: boolean};
|
|
let didWarnPopoverTargetObject = false;
|
|
if (__DEV__) {
|
|
didWarnForNewBooleanPropsWithEmptyValue = {};
|
|
}
|
|
|
|
function validatePropertiesInDevelopment(type: string, props: any) {
|
|
if (__DEV__) {
|
|
validateARIAProperties(type, props);
|
|
validateInputProperties(type, props);
|
|
validateUnknownProperties(type, props, {
|
|
registrationNameDependencies,
|
|
possibleRegistrationNames,
|
|
});
|
|
if (
|
|
props.contentEditable &&
|
|
!props.suppressContentEditableWarning &&
|
|
props.children != null
|
|
) {
|
|
console.error(
|
|
'A component is `contentEditable` and contains `children` managed by ' +
|
|
'React. It is now your responsibility to guarantee that none of ' +
|
|
'those nodes are unexpectedly modified or duplicated. This is ' +
|
|
'probably not intentional.',
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function validateFormActionInDevelopment(
|
|
tag: string,
|
|
key: string,
|
|
value: mixed,
|
|
props: any,
|
|
) {
|
|
if (__DEV__) {
|
|
if (value == null) {
|
|
return;
|
|
}
|
|
if (tag === 'form') {
|
|
if (key === 'formAction') {
|
|
console.error(
|
|
'You can only pass the formAction prop to <input> or <button>. Use the action prop on <form>.',
|
|
);
|
|
} else if (typeof value === 'function') {
|
|
if (
|
|
(props.encType != null || props.method != null) &&
|
|
!didWarnFormActionMethod
|
|
) {
|
|
didWarnFormActionMethod = true;
|
|
console.error(
|
|
'Cannot specify a encType or method for a form that specifies a ' +
|
|
'function as the action. React provides those automatically. ' +
|
|
'They will get overridden.',
|
|
);
|
|
}
|
|
if (props.target != null && !didWarnFormActionTarget) {
|
|
didWarnFormActionTarget = true;
|
|
console.error(
|
|
'Cannot specify a target for a form that specifies a function as the action. ' +
|
|
'The function will always be executed in the same window.',
|
|
);
|
|
}
|
|
}
|
|
} else if (tag === 'input' || tag === 'button') {
|
|
if (key === 'action') {
|
|
console.error(
|
|
'You can only pass the action prop to <form>. Use the formAction prop on <input> or <button>.',
|
|
);
|
|
} else if (
|
|
tag === 'input' &&
|
|
props.type !== 'submit' &&
|
|
props.type !== 'image' &&
|
|
!didWarnFormActionType
|
|
) {
|
|
didWarnFormActionType = true;
|
|
console.error(
|
|
'An input can only specify a formAction along with type="submit" or type="image".',
|
|
);
|
|
} else if (
|
|
tag === 'button' &&
|
|
props.type != null &&
|
|
props.type !== 'submit' &&
|
|
!didWarnFormActionType
|
|
) {
|
|
didWarnFormActionType = true;
|
|
console.error(
|
|
'A button can only specify a formAction along with type="submit" or no type.',
|
|
);
|
|
} else if (typeof value === 'function') {
|
|
// Function form actions cannot control the form properties
|
|
if (props.name != null && !didWarnFormActionName) {
|
|
didWarnFormActionName = true;
|
|
console.error(
|
|
'Cannot specify a "name" prop for a button that specifies a function as a formAction. ' +
|
|
'React needs it to encode which action should be invoked. It will get overridden.',
|
|
);
|
|
}
|
|
if (
|
|
(props.formEncType != null || props.formMethod != null) &&
|
|
!didWarnFormActionMethod
|
|
) {
|
|
didWarnFormActionMethod = true;
|
|
console.error(
|
|
'Cannot specify a formEncType or formMethod for a button that specifies a ' +
|
|
'function as a formAction. React provides those automatically. They will get overridden.',
|
|
);
|
|
}
|
|
if (props.formTarget != null && !didWarnFormActionTarget) {
|
|
didWarnFormActionTarget = true;
|
|
console.error(
|
|
'Cannot specify a formTarget for a button that specifies a function as a formAction. ' +
|
|
'The function will always be executed in the same window.',
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
if (key === 'action') {
|
|
console.error('You can only pass the action prop to <form>.');
|
|
} else {
|
|
console.error(
|
|
'You can only pass the formAction prop to <input> or <button>.',
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function warnForPropDifference(
|
|
propName: string,
|
|
serverValue: mixed,
|
|
clientValue: mixed,
|
|
serverDifferences: {[propName: string]: mixed},
|
|
): void {
|
|
if (__DEV__) {
|
|
if (serverValue === clientValue) {
|
|
return;
|
|
}
|
|
const normalizedClientValue =
|
|
normalizeMarkupForTextOrAttribute(clientValue);
|
|
const normalizedServerValue =
|
|
normalizeMarkupForTextOrAttribute(serverValue);
|
|
if (normalizedServerValue === normalizedClientValue) {
|
|
return;
|
|
}
|
|
|
|
serverDifferences[propName] = serverValue;
|
|
}
|
|
}
|
|
|
|
function warnForExtraAttributes(
|
|
domElement: Element,
|
|
attributeNames: Set<string>,
|
|
serverDifferences: {[propName: string]: mixed},
|
|
) {
|
|
if (__DEV__) {
|
|
attributeNames.forEach(function (attributeName) {
|
|
serverDifferences[getPropNameFromAttributeName(attributeName)] =
|
|
attributeName === 'style'
|
|
? getStylesObjectFromElement(domElement)
|
|
: domElement.getAttribute(attributeName);
|
|
});
|
|
}
|
|
}
|
|
|
|
function warnForInvalidEventListener(registrationName: string, listener: any) {
|
|
if (__DEV__) {
|
|
if (listener === false) {
|
|
console.error(
|
|
'Expected `%s` listener to be a function, instead got `false`.\n\n' +
|
|
'If you used to conditionally omit it with %s={condition && value}, ' +
|
|
'pass %s={condition ? value : undefined} instead.',
|
|
registrationName,
|
|
registrationName,
|
|
registrationName,
|
|
);
|
|
} else {
|
|
console.error(
|
|
'Expected `%s` listener to be a function, instead got a value of `%s` type.',
|
|
registrationName,
|
|
typeof listener,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse the HTML and read it back to normalize the HTML string so that it
|
|
// can be used for comparison.
|
|
function normalizeHTML(parent: Element, html: string) {
|
|
if (__DEV__) {
|
|
// We could have created a separate document here to avoid
|
|
// re-initializing custom elements if they exist. But this breaks
|
|
// how <noscript> is being handled. So we use the same document.
|
|
// See the discussion in https://github.com/facebook/react/pull/11157.
|
|
const testElement =
|
|
parent.namespaceURI === MATH_NAMESPACE ||
|
|
parent.namespaceURI === SVG_NAMESPACE
|
|
? parent.ownerDocument.createElementNS(
|
|
(parent.namespaceURI: any),
|
|
parent.tagName,
|
|
)
|
|
: parent.ownerDocument.createElement(parent.tagName);
|
|
testElement.innerHTML = html;
|
|
return testElement.innerHTML;
|
|
}
|
|
}
|
|
|
|
// HTML parsing normalizes CR and CRLF to LF.
|
|
// It also can turn \u0000 into \uFFFD inside attributes.
|
|
// https://www.w3.org/TR/html5/single-page.html#preprocessing-the-input-stream
|
|
// If we have a mismatch, it might be caused by that.
|
|
// We will still patch up in this case but not fire the warning.
|
|
const NORMALIZE_NEWLINES_REGEX = /\r\n?/g;
|
|
const NORMALIZE_NULL_AND_REPLACEMENT_REGEX = /\u0000|\uFFFD/g;
|
|
|
|
function normalizeMarkupForTextOrAttribute(markup: mixed): string {
|
|
if (__DEV__) {
|
|
checkHtmlStringCoercion(markup);
|
|
}
|
|
const markupString = typeof markup === 'string' ? markup : '' + (markup: any);
|
|
return markupString
|
|
.replace(NORMALIZE_NEWLINES_REGEX, '\n')
|
|
.replace(NORMALIZE_NULL_AND_REPLACEMENT_REGEX, '');
|
|
}
|
|
|
|
function checkForUnmatchedText(
|
|
serverText: string,
|
|
clientText: string | number | bigint,
|
|
) {
|
|
const normalizedClientText = normalizeMarkupForTextOrAttribute(clientText);
|
|
const normalizedServerText = normalizeMarkupForTextOrAttribute(serverText);
|
|
if (normalizedServerText === normalizedClientText) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function noop() {}
|
|
|
|
export function trapClickOnNonInteractiveElement(node: HTMLElement) {
|
|
// Mobile Safari does not fire properly bubble click events on
|
|
// non-interactive elements, which means delegated click listeners do not
|
|
// fire. The workaround for this bug involves attaching an empty click
|
|
// listener on the target node.
|
|
// https://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
|
|
// Just set it using the onclick property so that we don't have to manage any
|
|
// bookkeeping for it. Not sure if we need to clear it when the listener is
|
|
// removed.
|
|
// TODO: Only do this for the relevant Safaris maybe?
|
|
node.onclick = noop;
|
|
}
|
|
|
|
const xlinkNamespace = 'http://www.w3.org/1999/xlink';
|
|
const xmlNamespace = 'http://www.w3.org/XML/1998/namespace';
|
|
|
|
function setProp(
|
|
domElement: Element,
|
|
tag: string,
|
|
key: string,
|
|
value: mixed,
|
|
props: any,
|
|
prevValue: mixed,
|
|
): void {
|
|
switch (key) {
|
|
case 'children': {
|
|
if (typeof value === 'string') {
|
|
if (__DEV__) {
|
|
validateTextNesting(value, tag);
|
|
}
|
|
// Avoid setting initial textContent when the text is empty. In IE11 setting
|
|
// textContent on a <textarea> will cause the placeholder to not
|
|
// show within the <textarea> until it has been focused and blurred again.
|
|
// https://github.com/facebook/react/issues/6731#issuecomment-254874553
|
|
const canSetTextContent =
|
|
tag !== 'body' && (tag !== 'textarea' || value !== '');
|
|
if (canSetTextContent) {
|
|
setTextContent(domElement, value);
|
|
}
|
|
} else if (typeof value === 'number' || typeof value === 'bigint') {
|
|
if (__DEV__) {
|
|
// $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint
|
|
validateTextNesting('' + value, tag);
|
|
}
|
|
const canSetTextContent = tag !== 'body';
|
|
if (canSetTextContent) {
|
|
// $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint
|
|
setTextContent(domElement, '' + value);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
// These are very common props and therefore are in the beginning of the switch.
|
|
// TODO: aria-label is a very common prop but allows booleans so is not like the others
|
|
// but should ideally go in this list too.
|
|
case 'className':
|
|
setValueForKnownAttribute(domElement, 'class', value);
|
|
break;
|
|
case 'tabIndex':
|
|
// This has to be case sensitive in SVG.
|
|
setValueForKnownAttribute(domElement, 'tabindex', value);
|
|
break;
|
|
case 'dir':
|
|
case 'role':
|
|
case 'viewBox':
|
|
case 'width':
|
|
case 'height': {
|
|
setValueForKnownAttribute(domElement, key, value);
|
|
break;
|
|
}
|
|
case 'style': {
|
|
setValueForStyles(domElement, value, prevValue);
|
|
break;
|
|
}
|
|
// These attributes accept URLs. These must not allow javascript: URLS.
|
|
case 'data':
|
|
if (tag !== 'object') {
|
|
setValueForKnownAttribute(domElement, 'data', value);
|
|
break;
|
|
}
|
|
// fallthrough
|
|
case 'src':
|
|
case 'href': {
|
|
if (
|
|
value === '' &&
|
|
// <a href=""> is fine for "reload" links.
|
|
!(tag === 'a' && key === 'href')
|
|
) {
|
|
if (__DEV__) {
|
|
if (key === 'src') {
|
|
console.error(
|
|
'An empty string ("") was passed to the %s attribute. ' +
|
|
'This may cause the browser to download the whole page again over the network. ' +
|
|
'To fix this, either do not render the element at all ' +
|
|
'or pass null to %s instead of an empty string.',
|
|
key,
|
|
key,
|
|
);
|
|
} else {
|
|
console.error(
|
|
'An empty string ("") was passed to the %s attribute. ' +
|
|
'To fix this, either do not render the element at all ' +
|
|
'or pass null to %s instead of an empty string.',
|
|
key,
|
|
key,
|
|
);
|
|
}
|
|
}
|
|
domElement.removeAttribute(key);
|
|
break;
|
|
}
|
|
if (
|
|
value == null ||
|
|
typeof value === 'function' ||
|
|
typeof value === 'symbol' ||
|
|
typeof value === 'boolean'
|
|
) {
|
|
domElement.removeAttribute(key);
|
|
break;
|
|
}
|
|
// `setAttribute` with objects becomes only `[object]` in IE8/9,
|
|
// ('' + value) makes it output the correct toString()-value.
|
|
if (__DEV__) {
|
|
checkAttributeStringCoercion(value, key);
|
|
}
|
|
const sanitizedValue = (sanitizeURL(
|
|
enableTrustedTypesIntegration ? value : '' + (value: any),
|
|
): any);
|
|
domElement.setAttribute(key, sanitizedValue);
|
|
break;
|
|
}
|
|
case 'action':
|
|
case 'formAction': {
|
|
// TODO: Consider moving these special cases to the form, input and button tags.
|
|
if (__DEV__) {
|
|
validateFormActionInDevelopment(tag, key, value, props);
|
|
}
|
|
if (typeof value === 'function') {
|
|
// Set a javascript URL that doesn't do anything. We don't expect this to be invoked
|
|
// because we'll preventDefault, but it can happen if a form is manually submitted or
|
|
// if someone calls stopPropagation before React gets the event.
|
|
// If CSP is used to block javascript: URLs that's fine too. It just won't show this
|
|
// error message but the URL will be logged.
|
|
domElement.setAttribute(
|
|
key,
|
|
// eslint-disable-next-line no-script-url
|
|
"javascript:throw new Error('" +
|
|
'A React form was unexpectedly submitted. If you called form.submit() manually, ' +
|
|
"consider using form.requestSubmit() instead. If you\\'re trying to use " +
|
|
'event.stopPropagation() in a submit event handler, consider also calling ' +
|
|
'event.preventDefault().' +
|
|
"')",
|
|
);
|
|
break;
|
|
} else if (typeof prevValue === 'function') {
|
|
// When we're switching off a Server Action that was originally hydrated.
|
|
// The server control these fields during SSR that are now trailing.
|
|
// The regular diffing doesn't apply since we compare against the previous props.
|
|
// Instead, we need to force them to be set to whatever they should be now.
|
|
// This would be a lot cleaner if we did this whole fork in the per-tag approach.
|
|
if (key === 'formAction') {
|
|
if (tag !== 'input') {
|
|
// Setting the name here isn't completely safe for inputs if this is switching
|
|
// to become a radio button. In that case we let the tag based override take
|
|
// control.
|
|
setProp(domElement, tag, 'name', props.name, props, null);
|
|
}
|
|
setProp(
|
|
domElement,
|
|
tag,
|
|
'formEncType',
|
|
props.formEncType,
|
|
props,
|
|
null,
|
|
);
|
|
setProp(domElement, tag, 'formMethod', props.formMethod, props, null);
|
|
setProp(domElement, tag, 'formTarget', props.formTarget, props, null);
|
|
} else {
|
|
setProp(domElement, tag, 'encType', props.encType, props, null);
|
|
setProp(domElement, tag, 'method', props.method, props, null);
|
|
setProp(domElement, tag, 'target', props.target, props, null);
|
|
}
|
|
}
|
|
if (
|
|
value == null ||
|
|
typeof value === 'symbol' ||
|
|
typeof value === 'boolean'
|
|
) {
|
|
domElement.removeAttribute(key);
|
|
break;
|
|
}
|
|
// `setAttribute` with objects becomes only `[object]` in IE8/9,
|
|
// ('' + value) makes it output the correct toString()-value.
|
|
if (__DEV__) {
|
|
checkAttributeStringCoercion(value, key);
|
|
}
|
|
const sanitizedValue = (sanitizeURL(
|
|
enableTrustedTypesIntegration ? value : '' + (value: any),
|
|
): any);
|
|
domElement.setAttribute(key, sanitizedValue);
|
|
break;
|
|
}
|
|
case 'onClick': {
|
|
// TODO: This cast may not be sound for SVG, MathML or custom elements.
|
|
if (value != null) {
|
|
if (__DEV__ && typeof value !== 'function') {
|
|
warnForInvalidEventListener(key, value);
|
|
}
|
|
trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
|
|
}
|
|
break;
|
|
}
|
|
case 'onScroll': {
|
|
if (value != null) {
|
|
if (__DEV__ && typeof value !== 'function') {
|
|
warnForInvalidEventListener(key, value);
|
|
}
|
|
listenToNonDelegatedEvent('scroll', domElement);
|
|
}
|
|
break;
|
|
}
|
|
case 'onScrollEnd': {
|
|
if (value != null) {
|
|
if (__DEV__ && typeof value !== 'function') {
|
|
warnForInvalidEventListener(key, value);
|
|
}
|
|
listenToNonDelegatedEvent('scrollend', domElement);
|
|
}
|
|
break;
|
|
}
|
|
case 'dangerouslySetInnerHTML': {
|
|
if (value != null) {
|
|
if (typeof value !== 'object' || !('__html' in value)) {
|
|
throw new Error(
|
|
'`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +
|
|
'Please visit https://react.dev/link/dangerously-set-inner-html ' +
|
|
'for more information.',
|
|
);
|
|
}
|
|
const nextHtml: any = value.__html;
|
|
if (nextHtml != null) {
|
|
if (props.children != null) {
|
|
throw new Error(
|
|
'Can only set one of `children` or `props.dangerouslySetInnerHTML`.',
|
|
);
|
|
}
|
|
domElement.innerHTML = nextHtml;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
// Note: `option.selected` is not updated if `select.multiple` is
|
|
// disabled with `removeAttribute`. We have special logic for handling this.
|
|
case 'multiple': {
|
|
(domElement: any).multiple =
|
|
value && typeof value !== 'function' && typeof value !== 'symbol';
|
|
break;
|
|
}
|
|
case 'muted': {
|
|
(domElement: any).muted =
|
|
value && typeof value !== 'function' && typeof value !== 'symbol';
|
|
break;
|
|
}
|
|
case 'suppressContentEditableWarning':
|
|
case 'suppressHydrationWarning':
|
|
case 'defaultValue': // Reserved
|
|
case 'defaultChecked':
|
|
case 'innerHTML':
|
|
case 'ref': {
|
|
// TODO: `ref` is pretty common, should we move it up?
|
|
// Noop
|
|
break;
|
|
}
|
|
case 'autoFocus': {
|
|
// We polyfill it separately on the client during commit.
|
|
// We could have excluded it in the property list instead of
|
|
// adding a special case here, but then it wouldn't be emitted
|
|
// on server rendering (but we *do* want to emit it in SSR).
|
|
break;
|
|
}
|
|
case 'xlinkHref': {
|
|
if (
|
|
value == null ||
|
|
typeof value === 'function' ||
|
|
typeof value === 'boolean' ||
|
|
typeof value === 'symbol'
|
|
) {
|
|
domElement.removeAttribute('xlink:href');
|
|
break;
|
|
}
|
|
// `setAttribute` with objects becomes only `[object]` in IE8/9,
|
|
// ('' + value) makes it output the correct toString()-value.
|
|
if (__DEV__) {
|
|
checkAttributeStringCoercion(value, key);
|
|
}
|
|
const sanitizedValue = (sanitizeURL(
|
|
enableTrustedTypesIntegration ? value : '' + (value: any),
|
|
): any);
|
|
domElement.setAttributeNS(xlinkNamespace, 'xlink:href', sanitizedValue);
|
|
break;
|
|
}
|
|
case 'contentEditable':
|
|
case 'spellCheck':
|
|
case 'draggable':
|
|
case 'value':
|
|
case 'autoReverse':
|
|
case 'externalResourcesRequired':
|
|
case 'focusable':
|
|
case 'preserveAlpha': {
|
|
// Booleanish String
|
|
// These are "enumerated" attributes that accept "true" and "false".
|
|
// In React, we let users pass `true` and `false` even though technically
|
|
// these aren't boolean attributes (they are coerced to strings).
|
|
// The SVG attributes are case-sensitive. Since the HTML attributes are
|
|
// insensitive they also work even though we canonically use lower case.
|
|
if (
|
|
value != null &&
|
|
typeof value !== 'function' &&
|
|
typeof value !== 'symbol'
|
|
) {
|
|
if (__DEV__) {
|
|
checkAttributeStringCoercion(value, key);
|
|
}
|
|
domElement.setAttribute(
|
|
key,
|
|
enableTrustedTypesIntegration ? (value: any) : '' + (value: any),
|
|
);
|
|
} else {
|
|
domElement.removeAttribute(key);
|
|
}
|
|
break;
|
|
}
|
|
// Boolean
|
|
case 'inert': {
|
|
if (__DEV__) {
|
|
if (value === '' && !didWarnForNewBooleanPropsWithEmptyValue[key]) {
|
|
didWarnForNewBooleanPropsWithEmptyValue[key] = true;
|
|
console.error(
|
|
'Received an empty string for a boolean attribute `%s`. ' +
|
|
'This will treat the attribute as if it were false. ' +
|
|
'Either pass `false` to silence this warning, or ' +
|
|
'pass `true` if you used an empty string in earlier versions of React to indicate this attribute is true.',
|
|
key,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
// Fallthrough for boolean props that don't have a warning for empty strings.
|
|
case 'allowFullScreen':
|
|
case 'async':
|
|
case 'autoPlay':
|
|
case 'controls':
|
|
case 'default':
|
|
case 'defer':
|
|
case 'disabled':
|
|
case 'disablePictureInPicture':
|
|
case 'disableRemotePlayback':
|
|
case 'formNoValidate':
|
|
case 'hidden':
|
|
case 'loop':
|
|
case 'noModule':
|
|
case 'noValidate':
|
|
case 'open':
|
|
case 'playsInline':
|
|
case 'readOnly':
|
|
case 'required':
|
|
case 'reversed':
|
|
case 'scoped':
|
|
case 'seamless':
|
|
case 'itemScope': {
|
|
if (value && typeof value !== 'function' && typeof value !== 'symbol') {
|
|
domElement.setAttribute(key, '');
|
|
} else {
|
|
domElement.removeAttribute(key);
|
|
}
|
|
break;
|
|
}
|
|
// Overloaded Boolean
|
|
case 'capture':
|
|
case 'download': {
|
|
// An attribute that can be used as a flag as well as with a value.
|
|
// When true, it should be present (set either to an empty string or its name).
|
|
// When false, it should be omitted.
|
|
// For any other value, should be present with that value.
|
|
if (value === true) {
|
|
domElement.setAttribute(key, '');
|
|
} else if (
|
|
value !== false &&
|
|
value != null &&
|
|
typeof value !== 'function' &&
|
|
typeof value !== 'symbol'
|
|
) {
|
|
if (__DEV__) {
|
|
checkAttributeStringCoercion(value, key);
|
|
}
|
|
domElement.setAttribute(key, (value: any));
|
|
} else {
|
|
domElement.removeAttribute(key);
|
|
}
|
|
break;
|
|
}
|
|
case 'cols':
|
|
case 'rows':
|
|
case 'size':
|
|
case 'span': {
|
|
// These are HTML attributes that must be positive numbers.
|
|
if (
|
|
value != null &&
|
|
typeof value !== 'function' &&
|
|
typeof value !== 'symbol' &&
|
|
!isNaN(value) &&
|
|
(value: any) >= 1
|
|
) {
|
|
if (__DEV__) {
|
|
checkAttributeStringCoercion(value, key);
|
|
}
|
|
domElement.setAttribute(key, (value: any));
|
|
} else {
|
|
domElement.removeAttribute(key);
|
|
}
|
|
break;
|
|
}
|
|
case 'rowSpan':
|
|
case 'start': {
|
|
// These are HTML attributes that must be numbers.
|
|
if (
|
|
value != null &&
|
|
typeof value !== 'function' &&
|
|
typeof value !== 'symbol' &&
|
|
!isNaN(value)
|
|
) {
|
|
if (__DEV__) {
|
|
checkAttributeStringCoercion(value, key);
|
|
}
|
|
domElement.setAttribute(key, (value: any));
|
|
} else {
|
|
domElement.removeAttribute(key);
|
|
}
|
|
break;
|
|
}
|
|
case 'popover':
|
|
listenToNonDelegatedEvent('beforetoggle', domElement);
|
|
listenToNonDelegatedEvent('toggle', domElement);
|
|
setValueForAttribute(domElement, 'popover', value);
|
|
break;
|
|
case 'xlinkActuate':
|
|
setValueForNamespacedAttribute(
|
|
domElement,
|
|
xlinkNamespace,
|
|
'xlink:actuate',
|
|
value,
|
|
);
|
|
break;
|
|
case 'xlinkArcrole':
|
|
setValueForNamespacedAttribute(
|
|
domElement,
|
|
xlinkNamespace,
|
|
'xlink:arcrole',
|
|
value,
|
|
);
|
|
break;
|
|
case 'xlinkRole':
|
|
setValueForNamespacedAttribute(
|
|
domElement,
|
|
xlinkNamespace,
|
|
'xlink:role',
|
|
value,
|
|
);
|
|
break;
|
|
case 'xlinkShow':
|
|
setValueForNamespacedAttribute(
|
|
domElement,
|
|
xlinkNamespace,
|
|
'xlink:show',
|
|
value,
|
|
);
|
|
break;
|
|
case 'xlinkTitle':
|
|
setValueForNamespacedAttribute(
|
|
domElement,
|
|
xlinkNamespace,
|
|
'xlink:title',
|
|
value,
|
|
);
|
|
break;
|
|
case 'xlinkType':
|
|
setValueForNamespacedAttribute(
|
|
domElement,
|
|
xlinkNamespace,
|
|
'xlink:type',
|
|
value,
|
|
);
|
|
break;
|
|
case 'xmlBase':
|
|
setValueForNamespacedAttribute(
|
|
domElement,
|
|
xmlNamespace,
|
|
'xml:base',
|
|
value,
|
|
);
|
|
break;
|
|
case 'xmlLang':
|
|
setValueForNamespacedAttribute(
|
|
domElement,
|
|
xmlNamespace,
|
|
'xml:lang',
|
|
value,
|
|
);
|
|
break;
|
|
case 'xmlSpace':
|
|
setValueForNamespacedAttribute(
|
|
domElement,
|
|
xmlNamespace,
|
|
'xml:space',
|
|
value,
|
|
);
|
|
break;
|
|
// Properties that should not be allowed on custom elements.
|
|
case 'is': {
|
|
if (__DEV__) {
|
|
if (prevValue != null) {
|
|
console.error(
|
|
'Cannot update the "is" prop after it has been initialized.',
|
|
);
|
|
}
|
|
}
|
|
// TODO: We shouldn't actually set this attribute, because we've already
|
|
// passed it to createElement. We don't also need the attribute.
|
|
// However, our tests currently query for it so it's plausible someone
|
|
// else does too so it's break.
|
|
setValueForAttribute(domElement, 'is', value);
|
|
break;
|
|
}
|
|
case 'innerText':
|
|
case 'textContent':
|
|
break;
|
|
case 'popoverTarget':
|
|
if (__DEV__) {
|
|
if (
|
|
!didWarnPopoverTargetObject &&
|
|
value != null &&
|
|
typeof value === 'object'
|
|
) {
|
|
didWarnPopoverTargetObject = true;
|
|
console.error(
|
|
'The `popoverTarget` prop expects the ID of an Element as a string. Received %s instead.',
|
|
value,
|
|
);
|
|
}
|
|
}
|
|
// Fall through
|
|
default: {
|
|
if (
|
|
key.length > 2 &&
|
|
(key[0] === 'o' || key[0] === 'O') &&
|
|
(key[1] === 'n' || key[1] === 'N')
|
|
) {
|
|
if (
|
|
__DEV__ &&
|
|
registrationNameDependencies.hasOwnProperty(key) &&
|
|
value != null &&
|
|
typeof value !== 'function'
|
|
) {
|
|
warnForInvalidEventListener(key, value);
|
|
}
|
|
} else {
|
|
const attributeName = getAttributeAlias(key);
|
|
setValueForAttribute(domElement, attributeName, value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function setPropOnCustomElement(
|
|
domElement: Element,
|
|
tag: string,
|
|
key: string,
|
|
value: mixed,
|
|
props: any,
|
|
prevValue: mixed,
|
|
): void {
|
|
switch (key) {
|
|
case 'style': {
|
|
setValueForStyles(domElement, value, prevValue);
|
|
break;
|
|
}
|
|
case 'dangerouslySetInnerHTML': {
|
|
if (value != null) {
|
|
if (typeof value !== 'object' || !('__html' in value)) {
|
|
throw new Error(
|
|
'`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +
|
|
'Please visit https://react.dev/link/dangerously-set-inner-html ' +
|
|
'for more information.',
|
|
);
|
|
}
|
|
const nextHtml: any = value.__html;
|
|
if (nextHtml != null) {
|
|
if (props.children != null) {
|
|
throw new Error(
|
|
'Can only set one of `children` or `props.dangerouslySetInnerHTML`.',
|
|
);
|
|
}
|
|
domElement.innerHTML = nextHtml;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 'children': {
|
|
if (typeof value === 'string') {
|
|
setTextContent(domElement, value);
|
|
} else if (typeof value === 'number' || typeof value === 'bigint') {
|
|
// $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint
|
|
setTextContent(domElement, '' + value);
|
|
}
|
|
break;
|
|
}
|
|
case 'onScroll': {
|
|
if (value != null) {
|
|
if (__DEV__ && typeof value !== 'function') {
|
|
warnForInvalidEventListener(key, value);
|
|
}
|
|
listenToNonDelegatedEvent('scroll', domElement);
|
|
}
|
|
break;
|
|
}
|
|
case 'onScrollEnd': {
|
|
if (value != null) {
|
|
if (__DEV__ && typeof value !== 'function') {
|
|
warnForInvalidEventListener(key, value);
|
|
}
|
|
listenToNonDelegatedEvent('scrollend', domElement);
|
|
}
|
|
break;
|
|
}
|
|
case 'onClick': {
|
|
// TODO: This cast may not be sound for SVG, MathML or custom elements.
|
|
if (value != null) {
|
|
if (__DEV__ && typeof value !== 'function') {
|
|
warnForInvalidEventListener(key, value);
|
|
}
|
|
trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
|
|
}
|
|
break;
|
|
}
|
|
case 'suppressContentEditableWarning':
|
|
case 'suppressHydrationWarning':
|
|
case 'innerHTML':
|
|
case 'ref': {
|
|
// Noop
|
|
break;
|
|
}
|
|
case 'innerText': // Properties
|
|
case 'textContent':
|
|
break;
|
|
// Fall through
|
|
default: {
|
|
if (registrationNameDependencies.hasOwnProperty(key)) {
|
|
if (__DEV__ && value != null && typeof value !== 'function') {
|
|
warnForInvalidEventListener(key, value);
|
|
}
|
|
} else {
|
|
setValueForPropertyOnCustomComponent(domElement, key, value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export function setInitialProperties(
|
|
domElement: Element,
|
|
tag: string,
|
|
props: Object,
|
|
): void {
|
|
if (__DEV__) {
|
|
validatePropertiesInDevelopment(tag, props);
|
|
}
|
|
|
|
// TODO: Make sure that we check isMounted before firing any of these events.
|
|
|
|
switch (tag) {
|
|
case 'div':
|
|
case 'span':
|
|
case 'svg':
|
|
case 'path':
|
|
case 'a':
|
|
case 'g':
|
|
case 'p':
|
|
case 'li': {
|
|
// Fast track the most common tag types
|
|
break;
|
|
}
|
|
// img tags previously were implemented as void elements with non delegated events however Safari (and possibly Firefox)
|
|
// begin fetching the image as soon as the `src` or `srcSet` property is set and if we set these before other properties
|
|
// that can modify the request (such as crossorigin) or the resource fetch (such as sizes) then the browser will load
|
|
// the wrong thing or load more than one thing. This implementation ensures src and srcSet are set on the instance last
|
|
case 'img': {
|
|
listenToNonDelegatedEvent('error', domElement);
|
|
listenToNonDelegatedEvent('load', domElement);
|
|
// Mostly a port of Void Element logic with special casing to ensure srcset and src are set last
|
|
let hasSrc = false;
|
|
let hasSrcSet = false;
|
|
for (const propKey in props) {
|
|
if (!props.hasOwnProperty(propKey)) {
|
|
continue;
|
|
}
|
|
const propValue = props[propKey];
|
|
if (propValue == null) {
|
|
continue;
|
|
}
|
|
switch (propKey) {
|
|
case 'src':
|
|
hasSrc = true;
|
|
break;
|
|
case 'srcSet':
|
|
hasSrcSet = true;
|
|
break;
|
|
case 'children':
|
|
case 'dangerouslySetInnerHTML': {
|
|
// TODO: Can we make this a DEV warning to avoid this deny list?
|
|
throw new Error(
|
|
`${tag} is a void element tag and must neither have \`children\` nor ` +
|
|
'use `dangerouslySetInnerHTML`.',
|
|
);
|
|
}
|
|
// defaultChecked and defaultValue are ignored by setProp
|
|
default: {
|
|
setProp(domElement, tag, propKey, propValue, props, null);
|
|
}
|
|
}
|
|
}
|
|
if (hasSrcSet) {
|
|
setProp(domElement, tag, 'srcSet', props.srcSet, props, null);
|
|
}
|
|
if (hasSrc) {
|
|
setProp(domElement, tag, 'src', props.src, props, null);
|
|
}
|
|
return;
|
|
}
|
|
case 'input': {
|
|
if (__DEV__) {
|
|
checkControlledValueProps('input', props);
|
|
}
|
|
// We listen to this event in case to ensure emulated bubble
|
|
// listeners still fire for the invalid event.
|
|
listenToNonDelegatedEvent('invalid', domElement);
|
|
|
|
let name = null;
|
|
let type = null;
|
|
let value = null;
|
|
let defaultValue = null;
|
|
let checked = null;
|
|
let defaultChecked = null;
|
|
for (const propKey in props) {
|
|
if (!props.hasOwnProperty(propKey)) {
|
|
continue;
|
|
}
|
|
const propValue = props[propKey];
|
|
if (propValue == null) {
|
|
continue;
|
|
}
|
|
switch (propKey) {
|
|
case 'name': {
|
|
name = propValue;
|
|
break;
|
|
}
|
|
case 'type': {
|
|
type = propValue;
|
|
break;
|
|
}
|
|
case 'checked': {
|
|
checked = propValue;
|
|
break;
|
|
}
|
|
case 'defaultChecked': {
|
|
defaultChecked = propValue;
|
|
break;
|
|
}
|
|
case 'value': {
|
|
value = propValue;
|
|
break;
|
|
}
|
|
case 'defaultValue': {
|
|
defaultValue = propValue;
|
|
break;
|
|
}
|
|
case 'children':
|
|
case 'dangerouslySetInnerHTML': {
|
|
if (propValue != null) {
|
|
throw new Error(
|
|
`${tag} is a void element tag and must neither have \`children\` nor ` +
|
|
'use `dangerouslySetInnerHTML`.',
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
default: {
|
|
setProp(domElement, tag, propKey, propValue, props, null);
|
|
}
|
|
}
|
|
}
|
|
// TODO: Make sure we check if this is still unmounted or do any clean
|
|
// up necessary since we never stop tracking anymore.
|
|
validateInputProps(domElement, props);
|
|
initInput(
|
|
domElement,
|
|
value,
|
|
defaultValue,
|
|
checked,
|
|
defaultChecked,
|
|
type,
|
|
name,
|
|
false,
|
|
);
|
|
track((domElement: any));
|
|
return;
|
|
}
|
|
case 'select': {
|
|
if (__DEV__) {
|
|
checkControlledValueProps('select', props);
|
|
}
|
|
// We listen to this event in case to ensure emulated bubble
|
|
// listeners still fire for the invalid event.
|
|
listenToNonDelegatedEvent('invalid', domElement);
|
|
let value = null;
|
|
let defaultValue = null;
|
|
let multiple = null;
|
|
for (const propKey in props) {
|
|
if (!props.hasOwnProperty(propKey)) {
|
|
continue;
|
|
}
|
|
const propValue = props[propKey];
|
|
if (propValue == null) {
|
|
continue;
|
|
}
|
|
switch (propKey) {
|
|
case 'value': {
|
|
value = propValue;
|
|
// This is handled by initSelect below.
|
|
break;
|
|
}
|
|
case 'defaultValue': {
|
|
defaultValue = propValue;
|
|
// This is handled by initSelect below.
|
|
break;
|
|
}
|
|
case 'multiple': {
|
|
multiple = propValue;
|
|
// TODO: We don't actually have to fall through here because we set it
|
|
// in initSelect anyway. We can remove the special case in setProp.
|
|
}
|
|
// Fallthrough
|
|
default: {
|
|
setProp(domElement, tag, propKey, propValue, props, null);
|
|
}
|
|
}
|
|
}
|
|
validateSelectProps(domElement, props);
|
|
initSelect(domElement, value, defaultValue, multiple);
|
|
return;
|
|
}
|
|
case 'textarea': {
|
|
if (__DEV__) {
|
|
checkControlledValueProps('textarea', props);
|
|
}
|
|
// We listen to this event in case to ensure emulated bubble
|
|
// listeners still fire for the invalid event.
|
|
listenToNonDelegatedEvent('invalid', domElement);
|
|
let value = null;
|
|
let defaultValue = null;
|
|
let children = null;
|
|
for (const propKey in props) {
|
|
if (!props.hasOwnProperty(propKey)) {
|
|
continue;
|
|
}
|
|
const propValue = props[propKey];
|
|
if (propValue == null) {
|
|
continue;
|
|
}
|
|
switch (propKey) {
|
|
case 'value': {
|
|
value = propValue;
|
|
// This is handled by initTextarea below.
|
|
break;
|
|
}
|
|
case 'defaultValue': {
|
|
defaultValue = propValue;
|
|
break;
|
|
}
|
|
case 'children': {
|
|
children = propValue;
|
|
// Handled by initTextarea above.
|
|
break;
|
|
}
|
|
case 'dangerouslySetInnerHTML': {
|
|
if (propValue != null) {
|
|
// TODO: Do we really need a special error message for this. It's also pretty blunt.
|
|
throw new Error(
|
|
'`dangerouslySetInnerHTML` does not make sense on <textarea>.',
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
default: {
|
|
setProp(domElement, tag, propKey, propValue, props, null);
|
|
}
|
|
}
|
|
}
|
|
// TODO: Make sure we check if this is still unmounted or do any clean
|
|
// up necessary since we never stop tracking anymore.
|
|
validateTextareaProps(domElement, props);
|
|
initTextarea(domElement, value, defaultValue, children);
|
|
track((domElement: any));
|
|
return;
|
|
}
|
|
case 'option': {
|
|
validateOptionProps(domElement, props);
|
|
for (const propKey in props) {
|
|
if (!props.hasOwnProperty(propKey)) {
|
|
continue;
|
|
}
|
|
const propValue = props[propKey];
|
|
if (propValue == null) {
|
|
continue;
|
|
}
|
|
switch (propKey) {
|
|
case 'selected': {
|
|
// TODO: Remove support for selected on option.
|
|
(domElement: any).selected =
|
|
propValue &&
|
|
typeof propValue !== 'function' &&
|
|
typeof propValue !== 'symbol';
|
|
break;
|
|
}
|
|
default: {
|
|
setProp(domElement, tag, propKey, propValue, props, null);
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
case 'dialog': {
|
|
listenToNonDelegatedEvent('cancel', domElement);
|
|
listenToNonDelegatedEvent('close', domElement);
|
|
break;
|
|
}
|
|
case 'iframe':
|
|
case 'object': {
|
|
// We listen to this event in case to ensure emulated bubble
|
|
// listeners still fire for the load event.
|
|
listenToNonDelegatedEvent('load', domElement);
|
|
break;
|
|
}
|
|
case 'video':
|
|
case 'audio': {
|
|
// We listen to these events in case to ensure emulated bubble
|
|
// listeners still fire for all the media events.
|
|
for (let i = 0; i < mediaEventTypes.length; i++) {
|
|
listenToNonDelegatedEvent(mediaEventTypes[i], domElement);
|
|
}
|
|
break;
|
|
}
|
|
case 'image': {
|
|
// We listen to these events in case to ensure emulated bubble
|
|
// listeners still fire for error and load events.
|
|
listenToNonDelegatedEvent('error', domElement);
|
|
listenToNonDelegatedEvent('load', domElement);
|
|
break;
|
|
}
|
|
case 'details': {
|
|
// We listen to this event in case to ensure emulated bubble
|
|
// listeners still fire for the toggle event.
|
|
listenToNonDelegatedEvent('toggle', domElement);
|
|
break;
|
|
}
|
|
case 'embed':
|
|
case 'source':
|
|
case 'link': {
|
|
// These are void elements that also need delegated events.
|
|
listenToNonDelegatedEvent('error', domElement);
|
|
listenToNonDelegatedEvent('load', domElement);
|
|
// We fallthrough to the return of the void elements
|
|
}
|
|
case 'area':
|
|
case 'base':
|
|
case 'br':
|
|
case 'col':
|
|
case 'hr':
|
|
case 'keygen':
|
|
case 'meta':
|
|
case 'param':
|
|
case 'track':
|
|
case 'wbr':
|
|
case 'menuitem': {
|
|
// Void elements
|
|
for (const propKey in props) {
|
|
if (!props.hasOwnProperty(propKey)) {
|
|
continue;
|
|
}
|
|
const propValue = props[propKey];
|
|
if (propValue == null) {
|
|
continue;
|
|
}
|
|
switch (propKey) {
|
|
case 'children':
|
|
case 'dangerouslySetInnerHTML': {
|
|
// TODO: Can we make this a DEV warning to avoid this deny list?
|
|
throw new Error(
|
|
`${tag} is a void element tag and must neither have \`children\` nor ` +
|
|
'use `dangerouslySetInnerHTML`.',
|
|
);
|
|
}
|
|
// defaultChecked and defaultValue are ignored by setProp
|
|
default: {
|
|
setProp(domElement, tag, propKey, propValue, props, null);
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
default: {
|
|
if (isCustomElement(tag, props)) {
|
|
for (const propKey in props) {
|
|
if (!props.hasOwnProperty(propKey)) {
|
|
continue;
|
|
}
|
|
const propValue = props[propKey];
|
|
if (propValue === undefined) {
|
|
continue;
|
|
}
|
|
setPropOnCustomElement(
|
|
domElement,
|
|
tag,
|
|
propKey,
|
|
propValue,
|
|
props,
|
|
undefined,
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const propKey in props) {
|
|
if (!props.hasOwnProperty(propKey)) {
|
|
continue;
|
|
}
|
|
const propValue = props[propKey];
|
|
if (propValue == null) {
|
|
continue;
|
|
}
|
|
setProp(domElement, tag, propKey, propValue, props, null);
|
|
}
|
|
}
|
|
|
|
export function updateProperties(
|
|
domElement: Element,
|
|
tag: string,
|
|
lastProps: Object,
|
|
nextProps: Object,
|
|
): void {
|
|
if (__DEV__) {
|
|
validatePropertiesInDevelopment(tag, nextProps);
|
|
}
|
|
|
|
switch (tag) {
|
|
case 'div':
|
|
case 'span':
|
|
case 'svg':
|
|
case 'path':
|
|
case 'a':
|
|
case 'g':
|
|
case 'p':
|
|
case 'li': {
|
|
// Fast track the most common tag types
|
|
break;
|
|
}
|
|
case 'input': {
|
|
let name = null;
|
|
let type = null;
|
|
let value = null;
|
|
let defaultValue = null;
|
|
let lastDefaultValue = null;
|
|
let checked = null;
|
|
let defaultChecked = null;
|
|
for (const propKey in lastProps) {
|
|
const lastProp = lastProps[propKey];
|
|
if (lastProps.hasOwnProperty(propKey) && lastProp != null) {
|
|
switch (propKey) {
|
|
case 'checked': {
|
|
break;
|
|
}
|
|
case 'value': {
|
|
// This is handled by updateWrapper below.
|
|
break;
|
|
}
|
|
case 'defaultValue': {
|
|
lastDefaultValue = lastProp;
|
|
}
|
|
// defaultChecked and defaultValue are ignored by setProp
|
|
// Fallthrough
|
|
default: {
|
|
if (!nextProps.hasOwnProperty(propKey))
|
|
setProp(domElement, tag, propKey, null, nextProps, lastProp);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for (const propKey in nextProps) {
|
|
const nextProp = nextProps[propKey];
|
|
const lastProp = lastProps[propKey];
|
|
if (
|
|
nextProps.hasOwnProperty(propKey) &&
|
|
(nextProp != null || lastProp != null)
|
|
) {
|
|
switch (propKey) {
|
|
case 'type': {
|
|
type = nextProp;
|
|
break;
|
|
}
|
|
case 'name': {
|
|
name = nextProp;
|
|
break;
|
|
}
|
|
case 'checked': {
|
|
checked = nextProp;
|
|
break;
|
|
}
|
|
case 'defaultChecked': {
|
|
defaultChecked = nextProp;
|
|
break;
|
|
}
|
|
case 'value': {
|
|
value = nextProp;
|
|
break;
|
|
}
|
|
case 'defaultValue': {
|
|
defaultValue = nextProp;
|
|
break;
|
|
}
|
|
case 'children':
|
|
case 'dangerouslySetInnerHTML': {
|
|
if (nextProp != null) {
|
|
throw new Error(
|
|
`${tag} is a void element tag and must neither have \`children\` nor ` +
|
|
'use `dangerouslySetInnerHTML`.',
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
default: {
|
|
if (nextProp !== lastProp)
|
|
setProp(
|
|
domElement,
|
|
tag,
|
|
propKey,
|
|
nextProp,
|
|
nextProps,
|
|
lastProp,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (__DEV__) {
|
|
const wasControlled =
|
|
lastProps.type === 'checkbox' || lastProps.type === 'radio'
|
|
? lastProps.checked != null
|
|
: lastProps.value != null;
|
|
const isControlled =
|
|
nextProps.type === 'checkbox' || nextProps.type === 'radio'
|
|
? nextProps.checked != null
|
|
: nextProps.value != null;
|
|
|
|
if (
|
|
!wasControlled &&
|
|
isControlled &&
|
|
!didWarnUncontrolledToControlled
|
|
) {
|
|
console.error(
|
|
'A component is changing an uncontrolled input to be controlled. ' +
|
|
'This is likely caused by the value changing from undefined to ' +
|
|
'a defined value, which should not happen. ' +
|
|
'Decide between using a controlled or uncontrolled input ' +
|
|
'element for the lifetime of the component. More info: https://react.dev/link/controlled-components',
|
|
);
|
|
didWarnUncontrolledToControlled = true;
|
|
}
|
|
if (
|
|
wasControlled &&
|
|
!isControlled &&
|
|
!didWarnControlledToUncontrolled
|
|
) {
|
|
console.error(
|
|
'A component is changing a controlled input to be uncontrolled. ' +
|
|
'This is likely caused by the value changing from a defined to ' +
|
|
'undefined, which should not happen. ' +
|
|
'Decide between using a controlled or uncontrolled input ' +
|
|
'element for the lifetime of the component. More info: https://react.dev/link/controlled-components',
|
|
);
|
|
didWarnControlledToUncontrolled = true;
|
|
}
|
|
}
|
|
|
|
// Update the wrapper around inputs *after* updating props. This has to
|
|
// happen after updating the rest of props. Otherwise HTML5 input validations
|
|
// raise warnings and prevent the new value from being assigned.
|
|
updateInput(
|
|
domElement,
|
|
value,
|
|
defaultValue,
|
|
lastDefaultValue,
|
|
checked,
|
|
defaultChecked,
|
|
type,
|
|
name,
|
|
);
|
|
return;
|
|
}
|
|
case 'select': {
|
|
let value = null;
|
|
let defaultValue = null;
|
|
let multiple = null;
|
|
let wasMultiple = null;
|
|
for (const propKey in lastProps) {
|
|
const lastProp = lastProps[propKey];
|
|
if (lastProps.hasOwnProperty(propKey) && lastProp != null) {
|
|
switch (propKey) {
|
|
case 'value': {
|
|
// This is handled by updateWrapper below.
|
|
break;
|
|
}
|
|
// defaultValue are ignored by setProp
|
|
case 'multiple': {
|
|
wasMultiple = lastProp;
|
|
// TODO: Move special case in here from setProp.
|
|
}
|
|
// Fallthrough
|
|
default: {
|
|
if (!nextProps.hasOwnProperty(propKey))
|
|
setProp(domElement, tag, propKey, null, nextProps, lastProp);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for (const propKey in nextProps) {
|
|
const nextProp = nextProps[propKey];
|
|
const lastProp = lastProps[propKey];
|
|
if (
|
|
nextProps.hasOwnProperty(propKey) &&
|
|
(nextProp != null || lastProp != null)
|
|
) {
|
|
switch (propKey) {
|
|
case 'value': {
|
|
value = nextProp;
|
|
// This is handled by updateSelect below.
|
|
break;
|
|
}
|
|
case 'defaultValue': {
|
|
defaultValue = nextProp;
|
|
break;
|
|
}
|
|
case 'multiple': {
|
|
multiple = nextProp;
|
|
// TODO: Just move the special case in here from setProp.
|
|
}
|
|
// Fallthrough
|
|
default: {
|
|
if (nextProp !== lastProp)
|
|
setProp(
|
|
domElement,
|
|
tag,
|
|
propKey,
|
|
nextProp,
|
|
nextProps,
|
|
lastProp,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// <select> value update needs to occur after <option> children
|
|
// reconciliation
|
|
updateSelect(domElement, value, defaultValue, multiple, wasMultiple);
|
|
return;
|
|
}
|
|
case 'textarea': {
|
|
let value = null;
|
|
let defaultValue = null;
|
|
for (const propKey in lastProps) {
|
|
const lastProp = lastProps[propKey];
|
|
if (
|
|
lastProps.hasOwnProperty(propKey) &&
|
|
lastProp != null &&
|
|
!nextProps.hasOwnProperty(propKey)
|
|
) {
|
|
switch (propKey) {
|
|
case 'value': {
|
|
// This is handled by updateTextarea below.
|
|
break;
|
|
}
|
|
case 'children': {
|
|
// TODO: This doesn't actually do anything if it updates.
|
|
break;
|
|
}
|
|
// defaultValue is ignored by setProp
|
|
default: {
|
|
setProp(domElement, tag, propKey, null, nextProps, lastProp);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for (const propKey in nextProps) {
|
|
const nextProp = nextProps[propKey];
|
|
const lastProp = lastProps[propKey];
|
|
if (
|
|
nextProps.hasOwnProperty(propKey) &&
|
|
(nextProp != null || lastProp != null)
|
|
) {
|
|
switch (propKey) {
|
|
case 'value': {
|
|
value = nextProp;
|
|
// This is handled by updateTextarea below.
|
|
break;
|
|
}
|
|
case 'defaultValue': {
|
|
defaultValue = nextProp;
|
|
break;
|
|
}
|
|
case 'children': {
|
|
// TODO: This doesn't actually do anything if it updates.
|
|
break;
|
|
}
|
|
case 'dangerouslySetInnerHTML': {
|
|
if (nextProp != null) {
|
|
// TODO: Do we really need a special error message for this. It's also pretty blunt.
|
|
throw new Error(
|
|
'`dangerouslySetInnerHTML` does not make sense on <textarea>.',
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
default: {
|
|
if (nextProp !== lastProp)
|
|
setProp(
|
|
domElement,
|
|
tag,
|
|
propKey,
|
|
nextProp,
|
|
nextProps,
|
|
lastProp,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
updateTextarea(domElement, value, defaultValue);
|
|
return;
|
|
}
|
|
case 'option': {
|
|
for (const propKey in lastProps) {
|
|
const lastProp = lastProps[propKey];
|
|
if (
|
|
lastProps.hasOwnProperty(propKey) &&
|
|
lastProp != null &&
|
|
!nextProps.hasOwnProperty(propKey)
|
|
) {
|
|
switch (propKey) {
|
|
case 'selected': {
|
|
// TODO: Remove support for selected on option.
|
|
(domElement: any).selected = false;
|
|
break;
|
|
}
|
|
default: {
|
|
setProp(domElement, tag, propKey, null, nextProps, lastProp);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for (const propKey in nextProps) {
|
|
const nextProp = nextProps[propKey];
|
|
const lastProp = lastProps[propKey];
|
|
if (
|
|
nextProps.hasOwnProperty(propKey) &&
|
|
nextProp !== lastProp &&
|
|
(nextProp != null || lastProp != null)
|
|
) {
|
|
switch (propKey) {
|
|
case 'selected': {
|
|
// TODO: Remove support for selected on option.
|
|
(domElement: any).selected =
|
|
nextProp &&
|
|
typeof nextProp !== 'function' &&
|
|
typeof nextProp !== 'symbol';
|
|
break;
|
|
}
|
|
default: {
|
|
setProp(domElement, tag, propKey, nextProp, nextProps, lastProp);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
case 'img':
|
|
case 'link':
|
|
case 'area':
|
|
case 'base':
|
|
case 'br':
|
|
case 'col':
|
|
case 'embed':
|
|
case 'hr':
|
|
case 'keygen':
|
|
case 'meta':
|
|
case 'param':
|
|
case 'source':
|
|
case 'track':
|
|
case 'wbr':
|
|
case 'menuitem': {
|
|
// Void elements
|
|
for (const propKey in lastProps) {
|
|
const lastProp = lastProps[propKey];
|
|
if (
|
|
lastProps.hasOwnProperty(propKey) &&
|
|
lastProp != null &&
|
|
!nextProps.hasOwnProperty(propKey)
|
|
) {
|
|
setProp(domElement, tag, propKey, null, nextProps, lastProp);
|
|
}
|
|
}
|
|
for (const propKey in nextProps) {
|
|
const nextProp = nextProps[propKey];
|
|
const lastProp = lastProps[propKey];
|
|
if (
|
|
nextProps.hasOwnProperty(propKey) &&
|
|
nextProp !== lastProp &&
|
|
(nextProp != null || lastProp != null)
|
|
) {
|
|
switch (propKey) {
|
|
case 'children':
|
|
case 'dangerouslySetInnerHTML': {
|
|
if (nextProp != null) {
|
|
// TODO: Can we make this a DEV warning to avoid this deny list?
|
|
throw new Error(
|
|
`${tag} is a void element tag and must neither have \`children\` nor ` +
|
|
'use `dangerouslySetInnerHTML`.',
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
// defaultChecked and defaultValue are ignored by setProp
|
|
default: {
|
|
setProp(domElement, tag, propKey, nextProp, nextProps, lastProp);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
default: {
|
|
if (isCustomElement(tag, nextProps)) {
|
|
for (const propKey in lastProps) {
|
|
const lastProp = lastProps[propKey];
|
|
if (
|
|
lastProps.hasOwnProperty(propKey) &&
|
|
lastProp !== undefined &&
|
|
!nextProps.hasOwnProperty(propKey)
|
|
) {
|
|
setPropOnCustomElement(
|
|
domElement,
|
|
tag,
|
|
propKey,
|
|
undefined,
|
|
nextProps,
|
|
lastProp,
|
|
);
|
|
}
|
|
}
|
|
for (const propKey in nextProps) {
|
|
const nextProp = nextProps[propKey];
|
|
const lastProp = lastProps[propKey];
|
|
if (
|
|
nextProps.hasOwnProperty(propKey) &&
|
|
nextProp !== lastProp &&
|
|
(nextProp !== undefined || lastProp !== undefined)
|
|
) {
|
|
setPropOnCustomElement(
|
|
domElement,
|
|
tag,
|
|
propKey,
|
|
nextProp,
|
|
nextProps,
|
|
lastProp,
|
|
);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const propKey in lastProps) {
|
|
const lastProp = lastProps[propKey];
|
|
if (
|
|
lastProps.hasOwnProperty(propKey) &&
|
|
lastProp != null &&
|
|
!nextProps.hasOwnProperty(propKey)
|
|
) {
|
|
setProp(domElement, tag, propKey, null, nextProps, lastProp);
|
|
}
|
|
}
|
|
for (const propKey in nextProps) {
|
|
const nextProp = nextProps[propKey];
|
|
const lastProp = lastProps[propKey];
|
|
if (
|
|
nextProps.hasOwnProperty(propKey) &&
|
|
nextProp !== lastProp &&
|
|
(nextProp != null || lastProp != null)
|
|
) {
|
|
setProp(domElement, tag, propKey, nextProp, nextProps, lastProp);
|
|
}
|
|
}
|
|
}
|
|
|
|
function getPossibleStandardName(propName: string): string | null {
|
|
if (__DEV__) {
|
|
const lowerCasedName = propName.toLowerCase();
|
|
if (!possibleStandardNames.hasOwnProperty(lowerCasedName)) {
|
|
return null;
|
|
}
|
|
return possibleStandardNames[lowerCasedName] || null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function getPropNameFromAttributeName(attrName: string): string {
|
|
switch (attrName) {
|
|
case 'class':
|
|
return 'className';
|
|
case 'for':
|
|
return 'htmlFor';
|
|
// TODO: The rest of the aliases.
|
|
default:
|
|
return attrName;
|
|
}
|
|
}
|
|
|
|
export function getPropsFromElement(domElement: Element): Object {
|
|
const serverDifferences: {[propName: string]: mixed} = {};
|
|
const attributes = domElement.attributes;
|
|
for (let i = 0; i < attributes.length; i++) {
|
|
const attr = attributes[i];
|
|
serverDifferences[getPropNameFromAttributeName(attr.name)] =
|
|
attr.name.toLowerCase() === 'style'
|
|
? getStylesObjectFromElement(domElement)
|
|
: attr.value;
|
|
}
|
|
return serverDifferences;
|
|
}
|
|
|
|
function getStylesObjectFromElement(domElement: Element): {
|
|
[styleName: string]: string,
|
|
} {
|
|
const serverValueInObjectForm: {[prop: string]: string} = {};
|
|
const style = ((domElement: any): HTMLElement).style;
|
|
for (let i = 0; i < style.length; i++) {
|
|
const styleName: string = style[i];
|
|
// TODO: We should use the original prop value here if it is equivalent.
|
|
// TODO: We could use the original client capitalization if the equivalent
|
|
// other capitalization exists in the DOM.
|
|
serverValueInObjectForm[styleName] = style.getPropertyValue(styleName);
|
|
}
|
|
return serverValueInObjectForm;
|
|
}
|
|
|
|
function diffHydratedStyles(
|
|
domElement: Element,
|
|
value: mixed,
|
|
serverDifferences: {[propName: string]: mixed},
|
|
): void {
|
|
if (value != null && typeof value !== 'object') {
|
|
if (__DEV__) {
|
|
console.error(
|
|
'The `style` prop expects a mapping from style properties to values, ' +
|
|
"not a string. For example, style={{marginRight: spacing + 'em'}} when " +
|
|
'using JSX.',
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
// First we compare the string form and see if it's equivalent.
|
|
// This lets us bail out on anything that used to pass in this form.
|
|
// It also lets us compare anything that's not parsed by this browser.
|
|
const clientValue = createDangerousStringForStyles(value);
|
|
const serverValue = domElement.getAttribute('style');
|
|
|
|
if (serverValue === clientValue) {
|
|
return;
|
|
}
|
|
const normalizedClientValue = normalizeMarkupForTextOrAttribute(clientValue);
|
|
const normalizedServerValue = normalizeMarkupForTextOrAttribute(serverValue);
|
|
if (normalizedServerValue === normalizedClientValue) {
|
|
return;
|
|
}
|
|
|
|
// Otherwise, we create the object from the DOM for the diff view.
|
|
serverDifferences.style = getStylesObjectFromElement(domElement);
|
|
}
|
|
|
|
function hydrateAttribute(
|
|
domElement: Element,
|
|
propKey: string,
|
|
attributeName: string,
|
|
value: any,
|
|
extraAttributes: Set<string>,
|
|
serverDifferences: {[propName: string]: mixed},
|
|
): void {
|
|
extraAttributes.delete(attributeName);
|
|
const serverValue = domElement.getAttribute(attributeName);
|
|
if (serverValue === null) {
|
|
switch (typeof value) {
|
|
case 'undefined':
|
|
case 'function':
|
|
case 'symbol':
|
|
case 'boolean':
|
|
return;
|
|
}
|
|
} else {
|
|
if (value == null) {
|
|
// We had an attribute but shouldn't have had one, so read it
|
|
// for the error message.
|
|
} else {
|
|
switch (typeof value) {
|
|
case 'function':
|
|
case 'symbol':
|
|
case 'boolean':
|
|
break;
|
|
default: {
|
|
if (__DEV__) {
|
|
checkAttributeStringCoercion(value, propKey);
|
|
}
|
|
if (serverValue === '' + value) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
warnForPropDifference(propKey, serverValue, value, serverDifferences);
|
|
}
|
|
|
|
function hydrateBooleanAttribute(
|
|
domElement: Element,
|
|
propKey: string,
|
|
attributeName: string,
|
|
value: any,
|
|
extraAttributes: Set<string>,
|
|
serverDifferences: {[propName: string]: mixed},
|
|
): void {
|
|
extraAttributes.delete(attributeName);
|
|
const serverValue = domElement.getAttribute(attributeName);
|
|
if (serverValue === null) {
|
|
switch (typeof value) {
|
|
case 'function':
|
|
case 'symbol':
|
|
return;
|
|
}
|
|
if (!value) {
|
|
return;
|
|
}
|
|
} else {
|
|
switch (typeof value) {
|
|
case 'function':
|
|
case 'symbol':
|
|
break;
|
|
default: {
|
|
if (value) {
|
|
// If this was a boolean, it doesn't matter what the value is
|
|
// the fact that we have it is the same as the expected.
|
|
// As long as it's positive.
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
warnForPropDifference(propKey, serverValue, value, serverDifferences);
|
|
}
|
|
|
|
function hydrateOverloadedBooleanAttribute(
|
|
domElement: Element,
|
|
propKey: string,
|
|
attributeName: string,
|
|
value: any,
|
|
extraAttributes: Set<string>,
|
|
serverDifferences: {[propName: string]: mixed},
|
|
): void {
|
|
extraAttributes.delete(attributeName);
|
|
const serverValue = domElement.getAttribute(attributeName);
|
|
if (serverValue === null) {
|
|
switch (typeof value) {
|
|
case 'undefined':
|
|
case 'function':
|
|
case 'symbol':
|
|
return;
|
|
default:
|
|
if (value === false) {
|
|
return;
|
|
}
|
|
}
|
|
} else {
|
|
if (value == null) {
|
|
// We had an attribute but shouldn't have had one, so read it
|
|
// for the error message.
|
|
} else {
|
|
switch (typeof value) {
|
|
case 'function':
|
|
case 'symbol':
|
|
break;
|
|
case 'boolean':
|
|
if (value === true && serverValue === '') {
|
|
return;
|
|
}
|
|
break;
|
|
default: {
|
|
if (__DEV__) {
|
|
checkAttributeStringCoercion(value, propKey);
|
|
}
|
|
if (serverValue === '' + value) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
warnForPropDifference(propKey, serverValue, value, serverDifferences);
|
|
}
|
|
|
|
function hydrateBooleanishAttribute(
|
|
domElement: Element,
|
|
propKey: string,
|
|
attributeName: string,
|
|
value: any,
|
|
extraAttributes: Set<string>,
|
|
serverDifferences: {[propName: string]: mixed},
|
|
): void {
|
|
extraAttributes.delete(attributeName);
|
|
const serverValue = domElement.getAttribute(attributeName);
|
|
if (serverValue === null) {
|
|
switch (typeof value) {
|
|
case 'undefined':
|
|
case 'function':
|
|
case 'symbol':
|
|
return;
|
|
}
|
|
} else {
|
|
if (value == null) {
|
|
// We had an attribute but shouldn't have had one, so read it
|
|
// for the error message.
|
|
} else {
|
|
switch (typeof value) {
|
|
case 'function':
|
|
case 'symbol':
|
|
break;
|
|
default: {
|
|
if (__DEV__) {
|
|
checkAttributeStringCoercion(value, attributeName);
|
|
}
|
|
if (serverValue === '' + (value: any)) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
warnForPropDifference(propKey, serverValue, value, serverDifferences);
|
|
}
|
|
|
|
function hydrateNumericAttribute(
|
|
domElement: Element,
|
|
propKey: string,
|
|
attributeName: string,
|
|
value: any,
|
|
extraAttributes: Set<string>,
|
|
serverDifferences: {[propName: string]: mixed},
|
|
): void {
|
|
extraAttributes.delete(attributeName);
|
|
const serverValue = domElement.getAttribute(attributeName);
|
|
if (serverValue === null) {
|
|
switch (typeof value) {
|
|
case 'undefined':
|
|
case 'function':
|
|
case 'symbol':
|
|
case 'boolean':
|
|
return;
|
|
default:
|
|
if (isNaN(value)) {
|
|
return;
|
|
}
|
|
}
|
|
} else {
|
|
if (value == null) {
|
|
// We had an attribute but shouldn't have had one, so read it
|
|
// for the error message.
|
|
} else {
|
|
switch (typeof value) {
|
|
case 'function':
|
|
case 'symbol':
|
|
case 'boolean':
|
|
break;
|
|
default: {
|
|
if (isNaN(value)) {
|
|
// We had an attribute but shouldn't have had one, so read it
|
|
// for the error message.
|
|
break;
|
|
}
|
|
if (__DEV__) {
|
|
checkAttributeStringCoercion(value, propKey);
|
|
}
|
|
if (serverValue === '' + value) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
warnForPropDifference(propKey, serverValue, value, serverDifferences);
|
|
}
|
|
|
|
function hydratePositiveNumericAttribute(
|
|
domElement: Element,
|
|
propKey: string,
|
|
attributeName: string,
|
|
value: any,
|
|
extraAttributes: Set<string>,
|
|
serverDifferences: {[propName: string]: mixed},
|
|
): void {
|
|
extraAttributes.delete(attributeName);
|
|
const serverValue = domElement.getAttribute(attributeName);
|
|
if (serverValue === null) {
|
|
switch (typeof value) {
|
|
case 'undefined':
|
|
case 'function':
|
|
case 'symbol':
|
|
case 'boolean':
|
|
return;
|
|
default:
|
|
if (isNaN(value) || value < 1) {
|
|
return;
|
|
}
|
|
}
|
|
} else {
|
|
if (value == null) {
|
|
// We had an attribute but shouldn't have had one, so read it
|
|
// for the error message.
|
|
} else {
|
|
switch (typeof value) {
|
|
case 'function':
|
|
case 'symbol':
|
|
case 'boolean':
|
|
break;
|
|
default: {
|
|
if (isNaN(value) || value < 1) {
|
|
// We had an attribute but shouldn't have had one, so read it
|
|
// for the error message.
|
|
break;
|
|
}
|
|
if (__DEV__) {
|
|
checkAttributeStringCoercion(value, propKey);
|
|
}
|
|
if (serverValue === '' + value) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
warnForPropDifference(propKey, serverValue, value, serverDifferences);
|
|
}
|
|
|
|
function hydrateSanitizedAttribute(
|
|
domElement: Element,
|
|
propKey: string,
|
|
attributeName: string,
|
|
value: any,
|
|
extraAttributes: Set<string>,
|
|
serverDifferences: {[propName: string]: mixed},
|
|
): void {
|
|
extraAttributes.delete(attributeName);
|
|
const serverValue = domElement.getAttribute(attributeName);
|
|
if (serverValue === null) {
|
|
switch (typeof value) {
|
|
case 'undefined':
|
|
case 'function':
|
|
case 'symbol':
|
|
case 'boolean':
|
|
return;
|
|
}
|
|
} else {
|
|
if (value == null) {
|
|
// We had an attribute but shouldn't have had one, so read it
|
|
// for the error message.
|
|
} else {
|
|
switch (typeof value) {
|
|
case 'function':
|
|
case 'symbol':
|
|
case 'boolean':
|
|
break;
|
|
default: {
|
|
if (__DEV__) {
|
|
checkAttributeStringCoercion(value, propKey);
|
|
}
|
|
const sanitizedValue = sanitizeURL('' + value);
|
|
if (serverValue === sanitizedValue) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
warnForPropDifference(propKey, serverValue, value, serverDifferences);
|
|
}
|
|
|
|
function diffHydratedCustomComponent(
|
|
domElement: Element,
|
|
tag: string,
|
|
props: Object,
|
|
hostContext: HostContext,
|
|
extraAttributes: Set<string>,
|
|
serverDifferences: {[propName: string]: mixed},
|
|
) {
|
|
for (const propKey in props) {
|
|
if (!props.hasOwnProperty(propKey)) {
|
|
continue;
|
|
}
|
|
const value = props[propKey];
|
|
if (value == null) {
|
|
continue;
|
|
}
|
|
if (registrationNameDependencies.hasOwnProperty(propKey)) {
|
|
if (typeof value !== 'function') {
|
|
warnForInvalidEventListener(propKey, value);
|
|
}
|
|
continue;
|
|
}
|
|
if (props.suppressHydrationWarning === true) {
|
|
// Don't bother comparing. We're ignoring all these warnings.
|
|
continue;
|
|
}
|
|
// Validate that the properties correspond to their expected values.
|
|
switch (propKey) {
|
|
case 'children': {
|
|
if (typeof value === 'string' || typeof value === 'number') {
|
|
warnForPropDifference(
|
|
'children',
|
|
domElement.textContent,
|
|
value,
|
|
serverDifferences,
|
|
);
|
|
}
|
|
continue;
|
|
}
|
|
// Checked above already
|
|
case 'suppressContentEditableWarning':
|
|
case 'suppressHydrationWarning':
|
|
case 'defaultValue':
|
|
case 'defaultChecked':
|
|
case 'innerHTML':
|
|
case 'ref':
|
|
// Noop
|
|
continue;
|
|
case 'dangerouslySetInnerHTML':
|
|
const serverHTML = domElement.innerHTML;
|
|
const nextHtml = value ? value.__html : undefined;
|
|
if (nextHtml != null) {
|
|
const expectedHTML = normalizeHTML(domElement, nextHtml);
|
|
warnForPropDifference(
|
|
propKey,
|
|
serverHTML,
|
|
expectedHTML,
|
|
serverDifferences,
|
|
);
|
|
}
|
|
continue;
|
|
case 'style':
|
|
extraAttributes.delete(propKey);
|
|
diffHydratedStyles(domElement, value, serverDifferences);
|
|
continue;
|
|
case 'offsetParent':
|
|
case 'offsetTop':
|
|
case 'offsetLeft':
|
|
case 'offsetWidth':
|
|
case 'offsetHeight':
|
|
case 'isContentEditable':
|
|
case 'outerText':
|
|
case 'outerHTML':
|
|
extraAttributes.delete(propKey.toLowerCase());
|
|
if (__DEV__) {
|
|
console.error(
|
|
'Assignment to read-only property will result in a no-op: `%s`',
|
|
propKey,
|
|
);
|
|
}
|
|
continue;
|
|
// Fall through
|
|
case 'className':
|
|
// className is a special cased property on the server to render as an attribute.
|
|
extraAttributes.delete('class');
|
|
const serverValue = getValueForAttributeOnCustomComponent(
|
|
domElement,
|
|
'class',
|
|
value,
|
|
);
|
|
warnForPropDifference(
|
|
'className',
|
|
serverValue,
|
|
value,
|
|
serverDifferences,
|
|
);
|
|
continue;
|
|
default: {
|
|
// This is a DEV-only path
|
|
const hostContextDev: HostContextDev = (hostContext: any);
|
|
const hostContextProd = hostContextDev.context;
|
|
if (
|
|
hostContextProd === HostContextNamespaceNone &&
|
|
tag !== 'svg' &&
|
|
tag !== 'math'
|
|
) {
|
|
extraAttributes.delete(propKey.toLowerCase());
|
|
} else {
|
|
extraAttributes.delete(propKey);
|
|
}
|
|
const valueOnCustomComponent = getValueForAttributeOnCustomComponent(
|
|
domElement,
|
|
propKey,
|
|
value,
|
|
);
|
|
warnForPropDifference(
|
|
propKey,
|
|
valueOnCustomComponent,
|
|
value,
|
|
serverDifferences,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// This is the exact URL string we expect that Fizz renders if we provide a function action.
|
|
// We use this for hydration warnings. It needs to be in sync with Fizz. Maybe makes sense
|
|
// as a shared module for that reason.
|
|
const EXPECTED_FORM_ACTION_URL =
|
|
// eslint-disable-next-line no-script-url
|
|
"javascript:throw new Error('React form unexpectedly submitted.')";
|
|
|
|
function diffHydratedGenericElement(
|
|
domElement: Element,
|
|
tag: string,
|
|
props: Object,
|
|
hostContext: HostContext,
|
|
extraAttributes: Set<string>,
|
|
serverDifferences: {[propName: string]: mixed},
|
|
) {
|
|
for (const propKey in props) {
|
|
if (!props.hasOwnProperty(propKey)) {
|
|
continue;
|
|
}
|
|
const value = props[propKey];
|
|
if (value == null) {
|
|
continue;
|
|
}
|
|
if (registrationNameDependencies.hasOwnProperty(propKey)) {
|
|
if (typeof value !== 'function') {
|
|
warnForInvalidEventListener(propKey, value);
|
|
}
|
|
continue;
|
|
}
|
|
if (props.suppressHydrationWarning === true) {
|
|
// Don't bother comparing. We're ignoring all these warnings.
|
|
continue;
|
|
}
|
|
// Validate that the properties correspond to their expected values.
|
|
switch (propKey) {
|
|
case 'children': {
|
|
if (typeof value === 'string' || typeof value === 'number') {
|
|
warnForPropDifference(
|
|
'children',
|
|
domElement.textContent,
|
|
value,
|
|
serverDifferences,
|
|
);
|
|
}
|
|
continue;
|
|
}
|
|
// Checked above already
|
|
case 'suppressContentEditableWarning':
|
|
case 'suppressHydrationWarning':
|
|
case 'value': // Controlled attributes are not validated
|
|
case 'checked': // TODO: Only ignore them on controlled tags.
|
|
case 'selected':
|
|
case 'defaultValue':
|
|
case 'defaultChecked':
|
|
case 'innerHTML':
|
|
case 'ref':
|
|
// Noop
|
|
continue;
|
|
case 'dangerouslySetInnerHTML':
|
|
const serverHTML = domElement.innerHTML;
|
|
const nextHtml = value ? value.__html : undefined;
|
|
if (nextHtml != null) {
|
|
const expectedHTML = normalizeHTML(domElement, nextHtml);
|
|
if (serverHTML !== expectedHTML) {
|
|
serverDifferences[propKey] = {
|
|
__html: serverHTML,
|
|
};
|
|
}
|
|
}
|
|
continue;
|
|
case 'className':
|
|
hydrateAttribute(
|
|
domElement,
|
|
propKey,
|
|
'class',
|
|
value,
|
|
extraAttributes,
|
|
serverDifferences,
|
|
);
|
|
continue;
|
|
case 'tabIndex':
|
|
hydrateAttribute(
|
|
domElement,
|
|
propKey,
|
|
'tabindex',
|
|
value,
|
|
extraAttributes,
|
|
serverDifferences,
|
|
);
|
|
continue;
|
|
case 'style':
|
|
extraAttributes.delete(propKey);
|
|
diffHydratedStyles(domElement, value, serverDifferences);
|
|
continue;
|
|
case 'multiple': {
|
|
extraAttributes.delete(propKey);
|
|
const serverValue = (domElement: any).multiple;
|
|
warnForPropDifference(propKey, serverValue, value, serverDifferences);
|
|
continue;
|
|
}
|
|
case 'muted': {
|
|
extraAttributes.delete(propKey);
|
|
const serverValue = (domElement: any).muted;
|
|
warnForPropDifference(propKey, serverValue, value, serverDifferences);
|
|
continue;
|
|
}
|
|
case 'autoFocus': {
|
|
extraAttributes.delete('autofocus');
|
|
const serverValue = (domElement: any).autofocus;
|
|
warnForPropDifference(propKey, serverValue, value, serverDifferences);
|
|
continue;
|
|
}
|
|
case 'data':
|
|
if (tag !== 'object') {
|
|
extraAttributes.delete(propKey);
|
|
const serverValue = (domElement: any).getAttribute('data');
|
|
warnForPropDifference(propKey, serverValue, value, serverDifferences);
|
|
continue;
|
|
}
|
|
// fallthrough
|
|
case 'src':
|
|
case 'href':
|
|
if (
|
|
value === '' &&
|
|
// <a href=""> is fine for "reload" links.
|
|
!(tag === 'a' && propKey === 'href') &&
|
|
!(tag === 'object' && propKey === 'data')
|
|
) {
|
|
if (__DEV__) {
|
|
if (propKey === 'src') {
|
|
console.error(
|
|
'An empty string ("") was passed to the %s attribute. ' +
|
|
'This may cause the browser to download the whole page again over the network. ' +
|
|
'To fix this, either do not render the element at all ' +
|
|
'or pass null to %s instead of an empty string.',
|
|
propKey,
|
|
propKey,
|
|
);
|
|
} else {
|
|
console.error(
|
|
'An empty string ("") was passed to the %s attribute. ' +
|
|
'To fix this, either do not render the element at all ' +
|
|
'or pass null to %s instead of an empty string.',
|
|
propKey,
|
|
propKey,
|
|
);
|
|
}
|
|
}
|
|
hydrateSanitizedAttribute(
|
|
domElement,
|
|
propKey,
|
|
propKey,
|
|
null,
|
|
extraAttributes,
|
|
serverDifferences,
|
|
);
|
|
continue;
|
|
} else {
|
|
hydrateSanitizedAttribute(
|
|
domElement,
|
|
propKey,
|
|
propKey,
|
|
value,
|
|
extraAttributes,
|
|
serverDifferences,
|
|
);
|
|
continue;
|
|
}
|
|
case 'action':
|
|
case 'formAction': {
|
|
const serverValue = domElement.getAttribute(propKey);
|
|
if (typeof value === 'function') {
|
|
extraAttributes.delete(propKey.toLowerCase());
|
|
// The server can set these extra properties to implement actions.
|
|
// So we remove them from the extra attributes warnings.
|
|
if (propKey === 'formAction') {
|
|
extraAttributes.delete('name');
|
|
extraAttributes.delete('formenctype');
|
|
extraAttributes.delete('formmethod');
|
|
extraAttributes.delete('formtarget');
|
|
} else {
|
|
extraAttributes.delete('enctype');
|
|
extraAttributes.delete('method');
|
|
extraAttributes.delete('target');
|
|
}
|
|
// Ideally we should be able to warn if the server value was not a function
|
|
// however since the function can return any of these attributes any way it
|
|
// wants as a custom progressive enhancement, there's nothing to compare to.
|
|
// We can check if the function has the $FORM_ACTION property on the client
|
|
// and if it's not, warn, but that's an unnecessary constraint that they
|
|
// have to have the extra extension that doesn't do anything on the client.
|
|
continue;
|
|
} else if (serverValue === EXPECTED_FORM_ACTION_URL) {
|
|
extraAttributes.delete(propKey.toLowerCase());
|
|
warnForPropDifference(propKey, 'function', value, serverDifferences);
|
|
continue;
|
|
}
|
|
hydrateSanitizedAttribute(
|
|
domElement,
|
|
propKey,
|
|
propKey.toLowerCase(),
|
|
value,
|
|
extraAttributes,
|
|
serverDifferences,
|
|
);
|
|
continue;
|
|
}
|
|
case 'xlinkHref':
|
|
hydrateSanitizedAttribute(
|
|
domElement,
|
|
propKey,
|
|
'xlink:href',
|
|
value,
|
|
extraAttributes,
|
|
serverDifferences,
|
|
);
|
|
continue;
|
|
case 'contentEditable': {
|
|
// Lower-case Booleanish String
|
|
hydrateBooleanishAttribute(
|
|
domElement,
|
|
propKey,
|
|
'contenteditable',
|
|
value,
|
|
extraAttributes,
|
|
serverDifferences,
|
|
);
|
|
continue;
|
|
}
|
|
case 'spellCheck': {
|
|
// Lower-case Booleanish String
|
|
hydrateBooleanishAttribute(
|
|
domElement,
|
|
propKey,
|
|
'spellcheck',
|
|
value,
|
|
extraAttributes,
|
|
serverDifferences,
|
|
);
|
|
continue;
|
|
}
|
|
case 'draggable':
|
|
case 'autoReverse':
|
|
case 'externalResourcesRequired':
|
|
case 'focusable':
|
|
case 'preserveAlpha': {
|
|
// Case-sensitive Booleanish String
|
|
hydrateBooleanishAttribute(
|
|
domElement,
|
|
propKey,
|
|
propKey,
|
|
value,
|
|
extraAttributes,
|
|
serverDifferences,
|
|
);
|
|
continue;
|
|
}
|
|
case 'allowFullScreen':
|
|
case 'async':
|
|
case 'autoPlay':
|
|
case 'controls':
|
|
case 'default':
|
|
case 'defer':
|
|
case 'disabled':
|
|
case 'disablePictureInPicture':
|
|
case 'disableRemotePlayback':
|
|
case 'formNoValidate':
|
|
case 'hidden':
|
|
case 'loop':
|
|
case 'noModule':
|
|
case 'noValidate':
|
|
case 'open':
|
|
case 'playsInline':
|
|
case 'readOnly':
|
|
case 'required':
|
|
case 'reversed':
|
|
case 'scoped':
|
|
case 'seamless':
|
|
case 'itemScope': {
|
|
// Some of these need to be lower case to remove them from the extraAttributes list.
|
|
hydrateBooleanAttribute(
|
|
domElement,
|
|
propKey,
|
|
propKey.toLowerCase(),
|
|
value,
|
|
extraAttributes,
|
|
serverDifferences,
|
|
);
|
|
continue;
|
|
}
|
|
case 'capture':
|
|
case 'download': {
|
|
hydrateOverloadedBooleanAttribute(
|
|
domElement,
|
|
propKey,
|
|
propKey,
|
|
value,
|
|
extraAttributes,
|
|
serverDifferences,
|
|
);
|
|
continue;
|
|
}
|
|
case 'cols':
|
|
case 'rows':
|
|
case 'size':
|
|
case 'span': {
|
|
hydratePositiveNumericAttribute(
|
|
domElement,
|
|
propKey,
|
|
propKey,
|
|
value,
|
|
extraAttributes,
|
|
serverDifferences,
|
|
);
|
|
continue;
|
|
}
|
|
case 'rowSpan': {
|
|
hydrateNumericAttribute(
|
|
domElement,
|
|
propKey,
|
|
'rowspan',
|
|
value,
|
|
extraAttributes,
|
|
serverDifferences,
|
|
);
|
|
continue;
|
|
}
|
|
case 'start': {
|
|
hydrateNumericAttribute(
|
|
domElement,
|
|
propKey,
|
|
propKey,
|
|
value,
|
|
extraAttributes,
|
|
serverDifferences,
|
|
);
|
|
continue;
|
|
}
|
|
case 'xHeight':
|
|
hydrateAttribute(
|
|
domElement,
|
|
propKey,
|
|
'x-height',
|
|
value,
|
|
extraAttributes,
|
|
serverDifferences,
|
|
);
|
|
continue;
|
|
case 'xlinkActuate':
|
|
hydrateAttribute(
|
|
domElement,
|
|
propKey,
|
|
'xlink:actuate',
|
|
value,
|
|
extraAttributes,
|
|
serverDifferences,
|
|
);
|
|
continue;
|
|
case 'xlinkArcrole':
|
|
hydrateAttribute(
|
|
domElement,
|
|
propKey,
|
|
'xlink:arcrole',
|
|
value,
|
|
extraAttributes,
|
|
serverDifferences,
|
|
);
|
|
continue;
|
|
case 'xlinkRole':
|
|
hydrateAttribute(
|
|
domElement,
|
|
propKey,
|
|
'xlink:role',
|
|
value,
|
|
extraAttributes,
|
|
serverDifferences,
|
|
);
|
|
continue;
|
|
case 'xlinkShow':
|
|
hydrateAttribute(
|
|
domElement,
|
|
propKey,
|
|
'xlink:show',
|
|
value,
|
|
extraAttributes,
|
|
serverDifferences,
|
|
);
|
|
continue;
|
|
case 'xlinkTitle':
|
|
hydrateAttribute(
|
|
domElement,
|
|
propKey,
|
|
'xlink:title',
|
|
value,
|
|
extraAttributes,
|
|
serverDifferences,
|
|
);
|
|
continue;
|
|
case 'xlinkType':
|
|
hydrateAttribute(
|
|
domElement,
|
|
propKey,
|
|
'xlink:type',
|
|
value,
|
|
extraAttributes,
|
|
serverDifferences,
|
|
);
|
|
continue;
|
|
case 'xmlBase':
|
|
hydrateAttribute(
|
|
domElement,
|
|
propKey,
|
|
'xml:base',
|
|
value,
|
|
extraAttributes,
|
|
serverDifferences,
|
|
);
|
|
continue;
|
|
case 'xmlLang':
|
|
hydrateAttribute(
|
|
domElement,
|
|
propKey,
|
|
'xml:lang',
|
|
value,
|
|
extraAttributes,
|
|
serverDifferences,
|
|
);
|
|
continue;
|
|
case 'xmlSpace':
|
|
hydrateAttribute(
|
|
domElement,
|
|
propKey,
|
|
'xml:space',
|
|
value,
|
|
extraAttributes,
|
|
serverDifferences,
|
|
);
|
|
continue;
|
|
case 'inert':
|
|
if (__DEV__) {
|
|
if (
|
|
value === '' &&
|
|
!didWarnForNewBooleanPropsWithEmptyValue[propKey]
|
|
) {
|
|
didWarnForNewBooleanPropsWithEmptyValue[propKey] = true;
|
|
console.error(
|
|
'Received an empty string for a boolean attribute `%s`. ' +
|
|
'This will treat the attribute as if it were false. ' +
|
|
'Either pass `false` to silence this warning, or ' +
|
|
'pass `true` if you used an empty string in earlier versions of React to indicate this attribute is true.',
|
|
propKey,
|
|
);
|
|
}
|
|
}
|
|
hydrateBooleanAttribute(
|
|
domElement,
|
|
propKey,
|
|
propKey,
|
|
value,
|
|
extraAttributes,
|
|
serverDifferences,
|
|
);
|
|
continue;
|
|
default: {
|
|
if (
|
|
// shouldIgnoreAttribute
|
|
// We have already filtered out null/undefined and reserved words.
|
|
propKey.length > 2 &&
|
|
(propKey[0] === 'o' || propKey[0] === 'O') &&
|
|
(propKey[1] === 'n' || propKey[1] === 'N')
|
|
) {
|
|
continue;
|
|
}
|
|
const attributeName = getAttributeAlias(propKey);
|
|
let isMismatchDueToBadCasing = false;
|
|
|
|
// This is a DEV-only path
|
|
const hostContextDev: HostContextDev = (hostContext: any);
|
|
const hostContextProd = hostContextDev.context;
|
|
|
|
if (
|
|
hostContextProd === HostContextNamespaceNone &&
|
|
tag !== 'svg' &&
|
|
tag !== 'math'
|
|
) {
|
|
extraAttributes.delete(attributeName.toLowerCase());
|
|
} else {
|
|
const standardName = getPossibleStandardName(propKey);
|
|
if (standardName !== null && standardName !== propKey) {
|
|
// If an SVG prop is supplied with bad casing, it will
|
|
// be successfully parsed from HTML, but will produce a mismatch
|
|
// (and would be incorrectly rendered on the client).
|
|
// However, we already warn about bad casing elsewhere.
|
|
// So we'll skip the misleading extra mismatch warning in this case.
|
|
isMismatchDueToBadCasing = true;
|
|
extraAttributes.delete(standardName);
|
|
}
|
|
extraAttributes.delete(attributeName);
|
|
}
|
|
const serverValue = getValueForAttribute(
|
|
domElement,
|
|
attributeName,
|
|
value,
|
|
);
|
|
if (!isMismatchDueToBadCasing) {
|
|
warnForPropDifference(propKey, serverValue, value, serverDifferences);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export function hydrateProperties(
|
|
domElement: Element,
|
|
tag: string,
|
|
props: Object,
|
|
hostContext: HostContext,
|
|
): boolean {
|
|
if (__DEV__) {
|
|
validatePropertiesInDevelopment(tag, props);
|
|
}
|
|
|
|
// TODO: Make sure that we check isMounted before firing any of these events.
|
|
switch (tag) {
|
|
case 'dialog':
|
|
listenToNonDelegatedEvent('cancel', domElement);
|
|
listenToNonDelegatedEvent('close', domElement);
|
|
break;
|
|
case 'iframe':
|
|
case 'object':
|
|
case 'embed':
|
|
// We listen to this event in case to ensure emulated bubble
|
|
// listeners still fire for the load event.
|
|
listenToNonDelegatedEvent('load', domElement);
|
|
break;
|
|
case 'video':
|
|
case 'audio':
|
|
// We listen to these events in case to ensure emulated bubble
|
|
// listeners still fire for all the media events.
|
|
for (let i = 0; i < mediaEventTypes.length; i++) {
|
|
listenToNonDelegatedEvent(mediaEventTypes[i], domElement);
|
|
}
|
|
break;
|
|
case 'source':
|
|
// We listen to this event in case to ensure emulated bubble
|
|
// listeners still fire for the error event.
|
|
listenToNonDelegatedEvent('error', domElement);
|
|
break;
|
|
case 'img':
|
|
case 'image':
|
|
case 'link':
|
|
// We listen to these events in case to ensure emulated bubble
|
|
// listeners still fire for error and load events.
|
|
listenToNonDelegatedEvent('error', domElement);
|
|
listenToNonDelegatedEvent('load', domElement);
|
|
break;
|
|
case 'details':
|
|
// We listen to this event in case to ensure emulated bubble
|
|
// listeners still fire for the toggle event.
|
|
listenToNonDelegatedEvent('toggle', domElement);
|
|
break;
|
|
case 'input':
|
|
if (__DEV__) {
|
|
checkControlledValueProps('input', props);
|
|
}
|
|
// We listen to this event in case to ensure emulated bubble
|
|
// listeners still fire for the invalid event.
|
|
listenToNonDelegatedEvent('invalid', domElement);
|
|
// TODO: Make sure we check if this is still unmounted or do any clean
|
|
// up necessary since we never stop tracking anymore.
|
|
validateInputProps(domElement, props);
|
|
// For input and textarea we current always set the value property at
|
|
// post mount to force it to diverge from attributes. However, for
|
|
// option and select we don't quite do the same thing and select
|
|
// is not resilient to the DOM state changing so we don't do that here.
|
|
// TODO: Consider not doing this for input and textarea.
|
|
initInput(
|
|
domElement,
|
|
props.value,
|
|
props.defaultValue,
|
|
props.checked,
|
|
props.defaultChecked,
|
|
props.type,
|
|
props.name,
|
|
true,
|
|
);
|
|
track((domElement: any));
|
|
break;
|
|
case 'option':
|
|
validateOptionProps(domElement, props);
|
|
break;
|
|
case 'select':
|
|
if (__DEV__) {
|
|
checkControlledValueProps('select', props);
|
|
}
|
|
// We listen to this event in case to ensure emulated bubble
|
|
// listeners still fire for the invalid event.
|
|
listenToNonDelegatedEvent('invalid', domElement);
|
|
validateSelectProps(domElement, props);
|
|
break;
|
|
case 'textarea':
|
|
if (__DEV__) {
|
|
checkControlledValueProps('textarea', props);
|
|
}
|
|
// We listen to this event in case to ensure emulated bubble
|
|
// listeners still fire for the invalid event.
|
|
listenToNonDelegatedEvent('invalid', domElement);
|
|
// TODO: Make sure we check if this is still unmounted or do any clean
|
|
// up necessary since we never stop tracking anymore.
|
|
validateTextareaProps(domElement, props);
|
|
initTextarea(domElement, props.value, props.defaultValue, props.children);
|
|
track((domElement: any));
|
|
break;
|
|
}
|
|
|
|
const children = props.children;
|
|
// For text content children we compare against textContent. This
|
|
// might match additional HTML that is hidden when we read it using
|
|
// textContent. E.g. "foo" will match "f<span>oo</span>" but that still
|
|
// satisfies our requirement. Our requirement is not to produce perfect
|
|
// HTML and attributes. Ideally we should preserve structure but it's
|
|
// ok not to if the visible content is still enough to indicate what
|
|
// even listeners these nodes might be wired up to.
|
|
// TODO: Warn if there is more than a single textNode as a child.
|
|
// TODO: Should we use domElement.firstChild.nodeValue to compare?
|
|
if (
|
|
typeof children === 'string' ||
|
|
typeof children === 'number' ||
|
|
typeof children === 'bigint'
|
|
) {
|
|
if (
|
|
// $FlowFixMe[unsafe-addition] Flow doesn't want us to use `+` operator with string and bigint
|
|
domElement.textContent !== '' + children &&
|
|
props.suppressHydrationWarning !== true &&
|
|
!checkForUnmatchedText(domElement.textContent, children)
|
|
) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (props.popover != null) {
|
|
// We listen to this event in case to ensure emulated bubble
|
|
// listeners still fire for the toggle event.
|
|
listenToNonDelegatedEvent('beforetoggle', domElement);
|
|
listenToNonDelegatedEvent('toggle', domElement);
|
|
}
|
|
|
|
if (props.onScroll != null) {
|
|
listenToNonDelegatedEvent('scroll', domElement);
|
|
}
|
|
|
|
if (props.onScrollEnd != null) {
|
|
listenToNonDelegatedEvent('scrollend', domElement);
|
|
}
|
|
|
|
if (props.onClick != null) {
|
|
// TODO: This cast may not be sound for SVG, MathML or custom elements.
|
|
trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
export function diffHydratedProperties(
|
|
domElement: Element,
|
|
tag: string,
|
|
props: Object,
|
|
hostContext: HostContext,
|
|
): null | Object {
|
|
const serverDifferences: {[propName: string]: mixed} = {};
|
|
if (__DEV__) {
|
|
const extraAttributes: Set<string> = new Set();
|
|
const attributes = domElement.attributes;
|
|
for (let i = 0; i < attributes.length; i++) {
|
|
const name = attributes[i].name.toLowerCase();
|
|
switch (name) {
|
|
// Controlled attributes are not validated
|
|
// TODO: Only ignore them on controlled tags.
|
|
case 'value':
|
|
break;
|
|
case 'checked':
|
|
break;
|
|
case 'selected':
|
|
break;
|
|
default:
|
|
// Intentionally use the original name.
|
|
// See discussion in https://github.com/facebook/react/pull/10676.
|
|
extraAttributes.add(attributes[i].name);
|
|
}
|
|
}
|
|
if (isCustomElement(tag, props)) {
|
|
diffHydratedCustomComponent(
|
|
domElement,
|
|
tag,
|
|
props,
|
|
hostContext,
|
|
extraAttributes,
|
|
serverDifferences,
|
|
);
|
|
} else {
|
|
diffHydratedGenericElement(
|
|
domElement,
|
|
tag,
|
|
props,
|
|
hostContext,
|
|
extraAttributes,
|
|
serverDifferences,
|
|
);
|
|
}
|
|
if (extraAttributes.size > 0 && props.suppressHydrationWarning !== true) {
|
|
warnForExtraAttributes(domElement, extraAttributes, serverDifferences);
|
|
}
|
|
}
|
|
if (Object.keys(serverDifferences).length === 0) {
|
|
return null;
|
|
}
|
|
return serverDifferences;
|
|
}
|
|
|
|
export function hydrateText(
|
|
textNode: Text,
|
|
text: string,
|
|
parentProps: null | Object,
|
|
): boolean {
|
|
const isDifferent = textNode.nodeValue !== text;
|
|
if (
|
|
isDifferent &&
|
|
(parentProps === null || parentProps.suppressHydrationWarning !== true) &&
|
|
!checkForUnmatchedText(textNode.nodeValue, text)
|
|
) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export function diffHydratedText(textNode: Text, text: string): null | string {
|
|
if (textNode.nodeValue === text) {
|
|
return null;
|
|
}
|
|
const normalizedClientText = normalizeMarkupForTextOrAttribute(text);
|
|
const normalizedServerText = normalizeMarkupForTextOrAttribute(
|
|
textNode.nodeValue,
|
|
);
|
|
if (normalizedServerText === normalizedClientText) {
|
|
return null;
|
|
}
|
|
return textNode.nodeValue;
|
|
}
|
|
|
|
export function restoreControlledState(
|
|
domElement: Element,
|
|
tag: string,
|
|
props: Object,
|
|
): void {
|
|
switch (tag) {
|
|
case 'input':
|
|
restoreControlledInputState(domElement, props);
|
|
return;
|
|
case 'textarea':
|
|
restoreControlledTextareaState(domElement, props);
|
|
return;
|
|
case 'select':
|
|
restoreControlledSelectState(domElement, props);
|
|
return;
|
|
}
|
|
}
|