diff --git a/packages/react-dom-bindings/src/client/CSSPropertyOperations.js b/packages/react-dom-bindings/src/client/CSSPropertyOperations.js index 3f101ae8282c6..9c07945aecff9 100644 --- a/packages/react-dom-bindings/src/client/CSSPropertyOperations.js +++ b/packages/react-dom-bindings/src/client/CSSPropertyOperations.js @@ -72,6 +72,21 @@ export function createDangerousStringForStyles(styles) { * @param {object} styles */ export function setValueForStyles(node, styles) { + if (styles != null && typeof styles !== 'object') { + throw new Error( + 'The `style` prop expects a mapping from style properties to values, ' + + "not a string. For example, style={{marginRight: spacing + 'em'}} when " + + 'using JSX.', + ); + } + if (__DEV__) { + if (styles) { + // Freeze the next style object so that we can assume it won't be + // mutated. We have already warned for this in the past. + Object.freeze(styles); + } + } + const style = node.style; for (const styleName in styles) { if (!styles.hasOwnProperty(styleName)) { diff --git a/packages/react-dom-bindings/src/client/DOMPropertyOperations.js b/packages/react-dom-bindings/src/client/DOMPropertyOperations.js index d89f40037ef2f..4066055e0197f 100644 --- a/packages/react-dom-bindings/src/client/DOMPropertyOperations.js +++ b/packages/react-dom-bindings/src/client/DOMPropertyOperations.js @@ -7,181 +7,14 @@ * @flow */ -import { - BOOLEAN, - OVERLOADED_BOOLEAN, - NUMERIC, - POSITIVE_NUMERIC, -} from '../shared/DOMProperty'; - import isAttributeNameSafe from '../shared/isAttributeNameSafe'; -import sanitizeURL from '../shared/sanitizeURL'; import { enableTrustedTypesIntegration, enableCustomElementPropertySupport, - enableFilterEmptyStringAttributesDOM, } from 'shared/ReactFeatureFlags'; import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion'; import {getFiberCurrentPropsFromNode} from './ReactDOMComponentTree'; -import type {PropertyInfo} from '../shared/DOMProperty'; - -/** - * Get the value for a property on a node. Only used in DEV for SSR validation. - * The "expected" argument is used as a hint of what the expected value is. - * Some properties have multiple equivalent values. - */ -export function getValueForProperty( - node: Element, - name: string, - expected: mixed, - propertyInfo: PropertyInfo, -): mixed { - if (__DEV__) { - const attributeName = propertyInfo.attributeName; - - if (!node.hasAttribute(attributeName)) { - // shouldRemoveAttribute - switch (typeof expected) { - case 'function': - case 'symbol': // eslint-disable-line - return expected; - case 'boolean': { - if (!propertyInfo.acceptsBooleans) { - return expected; - } - } - } - switch (propertyInfo.type) { - case BOOLEAN: { - if (!expected) { - return expected; - } - break; - } - case OVERLOADED_BOOLEAN: { - if (expected === false) { - return expected; - } - break; - } - case NUMERIC: { - if (isNaN(expected)) { - return expected; - } - break; - } - case POSITIVE_NUMERIC: { - if (isNaN(expected) || (expected: any) < 1) { - return expected; - } - break; - } - } - if (enableFilterEmptyStringAttributesDOM) { - if (propertyInfo.removeEmptyString && expected === '') { - if (__DEV__) { - if (name === '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.', - name, - name, - ); - } 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.', - name, - name, - ); - } - } - return expected; - } - } - return expected === undefined ? undefined : null; - } - - // Even if this property uses a namespace we use getAttribute - // because we assume its namespaced name is the same as our config. - // To use getAttributeNS we need the local name which we don't have - // in our config atm. - const value = node.getAttribute(attributeName); - - if (expected == null) { - // We had an attribute but shouldn't have had one, so read it - // for the error message. - return value; - } - - // shouldRemoveAttribute - switch (typeof expected) { - case 'function': - case 'symbol': // eslint-disable-line - return value; - } - switch (propertyInfo.type) { - case BOOLEAN: { - if (expected) { - // 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 expected; - } - return value; - } - case OVERLOADED_BOOLEAN: { - if (value === '') { - return true; - } - if (expected === false) { - // We had an attribute but shouldn't have had one, so read it - // for the error message. - return value; - } - break; - } - case NUMERIC: { - if (isNaN(expected)) { - // We had an attribute but shouldn't have had one, so read it - // for the error message. - return value; - } - break; - } - case POSITIVE_NUMERIC: { - if (isNaN(expected) || (expected: any) < 1) { - // We had an attribute but shouldn't have had one, so read it - // for the error message. - return value; - } - break; - } - } - if (__DEV__) { - checkAttributeStringCoercion(expected, name); - } - if (propertyInfo.sanitizeURL) { - // We have already verified this above. - // eslint-disable-next-line react-internal/safe-string-coercion - if (value === '' + (sanitizeURL(expected): any)) { - return expected; - } - return value; - } - // We have already verified this above. - // eslint-disable-next-line react-internal/safe-string-coercion - if (value === '' + (expected: any)) { - return expected; - } - return value; - } -} - /** * Get the value for a attribute on a node. Only used in DEV for SSR validation. * The third argument is used as a hint of what the expected value is. Some @@ -271,138 +104,6 @@ export function getValueForAttributeOnCustomComponent( } } -/** - * Sets the value for a property on a node. - * - * @param {DOMElement} node - * @param {string} name - * @param {*} value - */ -export function setValueForProperty( - node: Element, - propertyInfo: PropertyInfo, - value: mixed, -) { - const attributeName = propertyInfo.attributeName; - - if (value === null) { - node.removeAttribute(attributeName); - return; - } - - // shouldRemoveAttribute - switch (typeof value) { - case 'undefined': - case 'function': - case 'symbol': // eslint-disable-line - node.removeAttribute(attributeName); - return; - case 'boolean': { - if (!propertyInfo.acceptsBooleans) { - node.removeAttribute(attributeName); - return; - } - } - } - if (enableFilterEmptyStringAttributesDOM) { - if (propertyInfo.removeEmptyString && value === '') { - if (__DEV__) { - if (attributeName === '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.', - attributeName, - attributeName, - ); - } 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.', - attributeName, - attributeName, - ); - } - } - node.removeAttribute(attributeName); - return; - } - } - - switch (propertyInfo.type) { - case BOOLEAN: - if (value) { - node.setAttribute(attributeName, ''); - } else { - node.removeAttribute(attributeName); - return; - } - break; - case OVERLOADED_BOOLEAN: - if (value === true) { - node.setAttribute(attributeName, ''); - } else if (value === false) { - node.removeAttribute(attributeName); - } else { - if (__DEV__) { - checkAttributeStringCoercion(value, attributeName); - } - node.setAttribute(attributeName, (value: any)); - } - return; - case NUMERIC: - if (!isNaN(value)) { - if (__DEV__) { - checkAttributeStringCoercion(value, attributeName); - } - node.setAttribute(attributeName, (value: any)); - } else { - node.removeAttribute(attributeName); - } - break; - case POSITIVE_NUMERIC: - if (!isNaN(value) && (value: any) >= 1) { - if (__DEV__) { - checkAttributeStringCoercion(value, attributeName); - } - node.setAttribute(attributeName, (value: any)); - } else { - node.removeAttribute(attributeName); - } - break; - default: { - if (__DEV__) { - checkAttributeStringCoercion(value, attributeName); - } - let attributeValue; - // `setAttribute` with objects becomes only `[object]` in IE8/9, - // ('' + value) makes it output the correct toString()-value. - if (enableTrustedTypesIntegration) { - if (propertyInfo.sanitizeURL) { - attributeValue = (sanitizeURL(value): any); - } else { - attributeValue = (value: any); - } - } else { - // We have already verified this above. - // eslint-disable-next-line react-internal/safe-string-coercion - attributeValue = '' + (value: any); - if (propertyInfo.sanitizeURL) { - attributeValue = sanitizeURL(attributeValue); - } - } - const attributeNamespace = propertyInfo.attributeNamespace; - if (attributeNamespace) { - node.setAttributeNS(attributeNamespace, attributeName, attributeValue); - } else { - node.setAttribute(attributeName, attributeValue); - } - } - } -} - export function setValueForAttribute( node: Element, name: string, @@ -439,6 +140,35 @@ export function setValueForAttribute( } } +export function setValueForNamespacedAttribute( + node: Element, + namespace: string, + name: string, + value: mixed, +) { + if (value === null) { + node.removeAttribute(name); + return; + } + switch (typeof value) { + case 'undefined': + case 'function': + case 'symbol': + case 'boolean': { + node.removeAttribute(name); + return; + } + } + if (__DEV__) { + checkAttributeStringCoercion(value, name); + } + node.setAttributeNS( + namespace, + name, + enableTrustedTypesIntegration ? (value: any) : '' + (value: any), + ); +} + export function setValueForPropertyOnCustomComponent( node: Element, name: string, diff --git a/packages/react-dom-bindings/src/client/ReactDOMComponent.js b/packages/react-dom-bindings/src/client/ReactDOMComponent.js index e587d84707189..60776996e537a 100644 --- a/packages/react-dom-bindings/src/client/ReactDOMComponent.js +++ b/packages/react-dom-bindings/src/client/ReactDOMComponent.js @@ -16,14 +16,14 @@ import { import {canUseDOM} from 'shared/ExecutionEnvironment'; import {checkHtmlStringCoercion} from 'shared/CheckStringCoercion'; +import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion'; import { getValueForAttribute, getValueForAttributeOnCustomComponent, - getValueForProperty, - setValueForProperty, setValueForPropertyOnCustomComponent, setValueForAttribute, + setValueForNamespacedAttribute, } from './DOMPropertyOperations'; import { initWrapperState as ReactDOMInputInitWrapperState, @@ -57,18 +57,20 @@ import { validateShorthandPropertyCollisionInDev, } from './CSSPropertyOperations'; import {HTML_NAMESPACE, getIntrinsicNamespace} from './DOMNamespaces'; -import {getPropertyInfo} from '../shared/DOMProperty'; import isCustomElement from '../shared/isCustomElement'; 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 { enableCustomElementPropertySupport, enableClientRenderFallbackOnTextMismatch, enableHostSingletons, disableIEWorkarounds, + enableTrustedTypesIntegration, + enableFilterEmptyStringAttributesDOM, } from 'shared/ReactFeatureFlags'; import { mediaEventTypes, @@ -122,6 +124,9 @@ function warnForPropDifference( if (didWarnInvalidHydration) { return; } + if (serverValue === clientValue) { + return; + } const normalizedClientValue = normalizeMarkupForTextOrAttribute(clientValue); const normalizedServerValue = @@ -259,31 +264,18 @@ export function trapClickOnNonInteractiveElement(node: HTMLElement) { 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, - isCustomElementTag: boolean, props: any, ): void { switch (key) { case 'style': { - if (value != null && typeof value !== 'object') { - throw new Error( - 'The `style` prop expects a mapping from style properties to values, ' + - "not a string. For example, style={{marginRight: spacing + 'em'}} when " + - 'using JSX.', - ); - } - if (__DEV__) { - if (value) { - // Freeze the next style object so that we can assume it won't be - // mutated. We have already warned for this in the past. - Object.freeze(value); - } - } - // Relies on `updateStylesByID` not mutating `styleUpdates`. setValueForStyles(domElement, value); break; } @@ -378,6 +370,616 @@ function setProp( // on server rendering (but we *do* want to emit it in SSR). break; } + // These attributes accept URLs. These must not allow javascript: URLS. + case 'src': + case 'href': + case 'action': + if (enableFilterEmptyStringAttributesDOM) { + if (value === '') { + 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; + } + } + // Fall through to the last case which shouldn't remove empty strings. + // eslint-disable-next-line no-fallthrough + case 'formAction': { + 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 '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, (value: any)); + } else { + domElement.removeAttribute(key); + } + break; + } + // Boolean + 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; + } + // A few React string attributes have a different name. + // This is a mapping from React prop names to the attribute names. + case 'acceptCharset': + setValueForAttribute(domElement, 'accept-charset', value); + break; + case 'className': + setValueForAttribute(domElement, 'class', value); + break; + case 'htmlFor': + setValueForAttribute(domElement, 'for', value); + break; + case 'httpEquiv': + setValueForAttribute(domElement, 'http-equiv', value); + break; + // HTML and SVG attributes, but the SVG attribute is case sensitive. + case 'tabIndex': + setValueForAttribute(domElement, 'tabindex', value); + break; + case 'crossOrigin': + setValueForAttribute(domElement, 'crossorigin', value); + break; + // This is a list of all SVG attributes that need special casing. + // Regular attributes that just accept strings. + case 'accentHeight': + setValueForAttribute(domElement, 'accent-height', value); + break; + case 'alignmentBaseline': + setValueForAttribute(domElement, 'alignment-baseline', value); + break; + case 'arabicForm': + setValueForAttribute(domElement, 'arabic-form', value); + break; + case 'baselineShift': + setValueForAttribute(domElement, 'baseline-shift', value); + break; + case 'capHeight': + setValueForAttribute(domElement, 'cap-height', value); + break; + case 'clipPath': + setValueForAttribute(domElement, 'clip-path', value); + break; + case 'clipRule': + setValueForAttribute(domElement, 'clip-rule', value); + break; + case 'colorInterpolation': + setValueForAttribute(domElement, 'color-interpolation', value); + break; + case 'colorInterpolationFilters': + setValueForAttribute(domElement, 'color-interpolation-filters', value); + break; + case 'colorProfile': + setValueForAttribute(domElement, 'color-profile', value); + break; + case 'colorRendering': + setValueForAttribute(domElement, 'color-rendering', value); + break; + case 'dominantBaseline': + setValueForAttribute(domElement, 'dominant-baseline', value); + break; + case 'enableBackground': + setValueForAttribute(domElement, 'enable-background', value); + break; + case 'fillOpacity': + setValueForAttribute(domElement, 'fill-opacity', value); + break; + case 'fillRule': + setValueForAttribute(domElement, 'fill-rule', value); + break; + case 'floodColor': + setValueForAttribute(domElement, 'flood-color', value); + break; + case 'floodOpacity': + setValueForAttribute(domElement, 'flood-opacity', value); + break; + case 'fontFamily': + setValueForAttribute(domElement, 'font-family', value); + break; + case 'fontSize': + setValueForAttribute(domElement, 'font-size', value); + break; + case 'fontSizeAdjust': + setValueForAttribute(domElement, 'font-size-adjust', value); + break; + case 'fontStretch': + setValueForAttribute(domElement, 'font-stretch', value); + break; + case 'fontStyle': + setValueForAttribute(domElement, 'font-style', value); + break; + case 'fontVariant': + setValueForAttribute(domElement, 'font-variant', value); + break; + case 'fontWeight': + setValueForAttribute(domElement, 'font-weight', value); + break; + case 'glyphName': + setValueForAttribute(domElement, 'glyph-name', value); + break; + case 'glyphOrientationHorizontal': + setValueForAttribute(domElement, 'glyph-orientation-horizontal', value); + break; + case 'glyphOrientationVertical': + setValueForAttribute(domElement, 'glyph-orientation-vertical', value); + break; + case 'horizAdvX': + setValueForAttribute(domElement, 'horiz-adv-x', value); + break; + case 'horizOriginX': + setValueForAttribute(domElement, 'horiz-origin-x', value); + break; + case 'imageRendering': + setValueForAttribute(domElement, 'image-rendering', value); + break; + case 'letterSpacing': + setValueForAttribute(domElement, 'letter-spacing', value); + break; + case 'lightingColor': + setValueForAttribute(domElement, 'lighting-color', value); + break; + case 'markerEnd': + setValueForAttribute(domElement, 'marker-end', value); + break; + case 'markerMid': + setValueForAttribute(domElement, 'marker-mid', value); + break; + case 'markerStart': + setValueForAttribute(domElement, 'marker-start', value); + break; + case 'overlinePosition': + setValueForAttribute(domElement, 'overline-position', value); + break; + case 'overlineThickness': + setValueForAttribute(domElement, 'overline-thickness', value); + break; + case 'paintOrder': + setValueForAttribute(domElement, 'paint-order', value); + break; + case 'panose-1': + setValueForAttribute(domElement, 'panose-1', value); + break; + case 'pointerEvents': + setValueForAttribute(domElement, 'pointer-events', value); + break; + case 'renderingIntent': + setValueForAttribute(domElement, 'rendering-intent', value); + break; + case 'shapeRendering': + setValueForAttribute(domElement, 'shape-rendering', value); + break; + case 'stopColor': + setValueForAttribute(domElement, 'stop-color', value); + break; + case 'stopOpacity': + setValueForAttribute(domElement, 'stop-opacity', value); + break; + case 'strikethroughPosition': + setValueForAttribute(domElement, 'strikethrough-position', value); + break; + case 'strikethroughThickness': + setValueForAttribute(domElement, 'strikethrough-thickness', value); + break; + case 'strokeDasharray': + setValueForAttribute(domElement, 'stroke-dasharray', value); + break; + case 'strokeDashoffset': + setValueForAttribute(domElement, 'stroke-dashoffset', value); + break; + case 'strokeLinecap': + setValueForAttribute(domElement, 'stroke-linecap', value); + break; + case 'strokeLinejoin': + setValueForAttribute(domElement, 'stroke-linejoin', value); + break; + case 'strokeMiterlimit': + setValueForAttribute(domElement, 'stroke-miterlimit', value); + break; + case 'strokeOpacity': + setValueForAttribute(domElement, 'stroke-opacity', value); + break; + case 'strokeWidth': + setValueForAttribute(domElement, 'stroke-width', value); + break; + case 'textAnchor': + setValueForAttribute(domElement, 'text-anchor', value); + break; + case 'textDecoration': + setValueForAttribute(domElement, 'text-decoration', value); + break; + case 'textRendering': + setValueForAttribute(domElement, 'text-rendering', value); + break; + case 'transformOrigin': + setValueForAttribute(domElement, 'transform-origin', value); + break; + case 'underlinePosition': + setValueForAttribute(domElement, 'underline-position', value); + break; + case 'underlineThickness': + setValueForAttribute(domElement, 'underline-thickness', value); + break; + case 'unicodeBidi': + setValueForAttribute(domElement, 'unicode-bidi', value); + break; + case 'unicodeRange': + setValueForAttribute(domElement, 'unicode-range', value); + break; + case 'unitsPerEm': + setValueForAttribute(domElement, 'units-per-em', value); + break; + case 'vAlphabetic': + setValueForAttribute(domElement, 'v-alphabetic', value); + break; + case 'vHanging': + setValueForAttribute(domElement, 'v-hanging', value); + break; + case 'vIdeographic': + setValueForAttribute(domElement, 'v-ideographic', value); + break; + case 'vMathematical': + setValueForAttribute(domElement, 'v-mathematical', value); + break; + case 'vectorEffect': + setValueForAttribute(domElement, 'vector-effect', value); + break; + case 'vertAdvY': + setValueForAttribute(domElement, 'vert-adv-y', value); + break; + case 'vertOriginX': + setValueForAttribute(domElement, 'vert-origin-x', value); + break; + case 'vertOriginY': + setValueForAttribute(domElement, 'vert-origin-y', value); + break; + case 'wordSpacing': + setValueForAttribute(domElement, 'word-spacing', value); + break; + case 'writingMode': + setValueForAttribute(domElement, 'writing-mode', value); + break; + case 'xmlnsXlink': + setValueForAttribute(domElement, 'xmlns:xlink', value); + break; + case 'xHeight': + setValueForAttribute(domElement, 'x-height', 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 'innerText': + case 'textContent': + if (enableCustomElementPropertySupport) { + break; + } + // eslint-disable-next-line no-fallthrough + 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 { + setValueForAttribute(domElement, key, value); + } + } + } +} + +function setPropOnCustomElement( + domElement: Element, + tag: string, + key: string, + value: mixed, + props: any, +): void { + switch (key) { + case 'style': { + setValueForStyles(domElement, value); + 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://reactjs.org/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`.', + ); + } + if (disableIEWorkarounds) { + domElement.innerHTML = nextHtml; + } else { + setInnerHTML(domElement, nextHtml); + } + } + } + break; + } + case 'children': { + if (typeof value === 'string') { + setTextContent(domElement, value); + } else if (typeof value === 'number') { + setTextContent(domElement, '' + value); + } + break; + } + case 'onScroll': { + if (value != null) { + if (__DEV__ && typeof value !== 'function') { + warnForInvalidEventListener(key, value); + } + listenToNonDelegatedEvent('scroll', 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': { + // Noop + break; + } case 'innerText': // Properties case 'textContent': if (enableCustomElementPropertySupport) { @@ -390,33 +992,14 @@ function setProp( warnForInvalidEventListener(key, value); } } else { - if (isCustomElementTag) { - if (enableCustomElementPropertySupport) { - setValueForPropertyOnCustomComponent(domElement, key, value); - } else { - if (typeof value === 'boolean') { - // Special case before the new flag is on - value = '' + (value: any); - } - setValueForAttribute(domElement, key, value); - } + if (enableCustomElementPropertySupport) { + setValueForPropertyOnCustomComponent(domElement, key, value); } else { - if ( - // shouldIgnoreAttribute - // We have already filtered out reserved words. - key.length > 2 && - (key[0] === 'o' || key[0] === 'O') && - (key[1] === 'n' || key[1] === 'N') - ) { - return; - } - - const propertyInfo = getPropertyInfo(key); - if (propertyInfo !== null) { - setValueForProperty(domElement, propertyInfo, value); - } else { - setValueForAttribute(domElement, key, value); + if (typeof value === 'boolean') { + // Special case before the new flag is on + value = '' + (value: any); } + setValueForAttribute(domElement, key, value); } } } @@ -475,7 +1058,7 @@ export function setInitialProperties( } // defaultChecked and defaultValue are ignored by setProp default: { - setProp(domElement, tag, propKey, propValue, false, props); + setProp(domElement, tag, propKey, propValue, props); } } } @@ -505,7 +1088,7 @@ export function setInitialProperties( } // defaultValue are ignored by setProp default: { - setProp(domElement, tag, propKey, propValue, false, props); + setProp(domElement, tag, propKey, propValue, props); } } } @@ -545,7 +1128,7 @@ export function setInitialProperties( } // defaultValue is ignored by setProp default: { - setProp(domElement, tag, propKey, propValue, false, props); + setProp(domElement, tag, propKey, propValue, props); } } } @@ -575,7 +1158,7 @@ export function setInitialProperties( break; } default: { - setProp(domElement, tag, propKey, propValue, false, props); + setProp(domElement, tag, propKey, propValue, props); } } } @@ -657,7 +1240,7 @@ export function setInitialProperties( } // defaultChecked and defaultValue are ignored by setProp default: { - setProp(domElement, tag, propKey, propValue, false, props); + setProp(domElement, tag, propKey, propValue, props); } } } @@ -665,16 +1248,28 @@ export function setInitialProperties( } } - const isCustomElementTag = isCustomElement(tag, props); - for (const propKey in props) { - if (!props.hasOwnProperty(propKey)) { - continue; - } - const propValue = props[propKey]; - if (propValue == null) { - continue; + if (isCustomElement(tag, props)) { + for (const propKey in props) { + if (!props.hasOwnProperty(propKey)) { + continue; + } + const propValue = props[propKey]; + if (propValue == null) { + continue; + } + setPropOnCustomElement(domElement, tag, propKey, propValue, props); + } + } else { + for (const propKey in props) { + if (!props.hasOwnProperty(propKey)) { + continue; + } + const propValue = props[propKey]; + if (propValue == null) { + continue; + } + setProp(domElement, tag, propKey, propValue, props); } - setProp(domElement, tag, propKey, propValue, isCustomElementTag, props); } } @@ -837,7 +1432,7 @@ export function updateProperties( } // defaultChecked and defaultValue are ignored by setProp default: { - setProp(domElement, tag, propKey, propValue, false, nextProps); + setProp(domElement, tag, propKey, propValue, nextProps); } } } @@ -858,7 +1453,7 @@ export function updateProperties( } // defaultValue are ignored by setProp default: { - setProp(domElement, tag, propKey, propValue, false, nextProps); + setProp(domElement, tag, propKey, propValue, nextProps); } } } @@ -891,7 +1486,7 @@ export function updateProperties( } // defaultValue is ignored by setProp default: { - setProp(domElement, tag, propKey, propValue, false, nextProps); + setProp(domElement, tag, propKey, propValue, nextProps); } } } @@ -912,7 +1507,7 @@ export function updateProperties( break; } default: { - setProp(domElement, tag, propKey, propValue, false, nextProps); + setProp(domElement, tag, propKey, propValue, nextProps); } } } @@ -951,7 +1546,7 @@ export function updateProperties( } // defaultChecked and defaultValue are ignored by setProp default: { - setProp(domElement, tag, propKey, propValue, false, nextProps); + setProp(domElement, tag, propKey, propValue, nextProps); } } } @@ -959,12 +1554,19 @@ export function updateProperties( } } - const isCustomElementTag = isCustomElement(tag, nextProps); // Apply the diff. - for (let i = 0; i < updatePayload.length; i += 2) { - const propKey = updatePayload[i]; - const propValue = updatePayload[i + 1]; - setProp(domElement, tag, propKey, propValue, isCustomElementTag, nextProps); + if (isCustomElement(tag, nextProps)) { + for (let i = 0; i < updatePayload.length; i += 2) { + const propKey = updatePayload[i]; + const propValue = updatePayload[i + 1]; + setPropOnCustomElement(domElement, tag, propKey, propValue, nextProps); + } + } else { + for (let i = 0; i < updatePayload.length; i += 2) { + const propKey = updatePayload[i]; + const propValue = updatePayload[i + 1]; + setProp(domElement, tag, propKey, propValue, nextProps); + } } } @@ -990,10 +1592,314 @@ function diffHydratedStyles(domElement: Element, value: mixed) { if (canDiffStyleForHydrationWarning) { const expectedStyle = createDangerousStringForStyles(value); const serverValue = domElement.getAttribute('style'); - if (expectedStyle !== serverValue) { - warnForPropDifference('style', serverValue, expectedStyle); + warnForPropDifference('style', serverValue, expectedStyle); + } +} + +function hydrateAttribute( + domElement: Element, + propKey: string, + attributeName: string, + value: any, + extraAttributes: Set, +): 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); +} + +function hydrateBooleanAttribute( + domElement: Element, + propKey: string, + attributeName: string, + value: any, + extraAttributes: Set, +): 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); +} + +function hydrateOverloadedBooleanAttribute( + domElement: Element, + propKey: string, + attributeName: string, + value: any, + extraAttributes: Set, +): 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); +} + +function hydrateBooleanishAttribute( + domElement: Element, + propKey: string, + attributeName: string, + value: any, + extraAttributes: Set, +): 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); +} + +function hydrateNumericAttribute( + domElement: Element, + propKey: string, + attributeName: string, + value: any, + extraAttributes: Set, +): 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); +} + +function hydratePositiveNumericAttribute( + domElement: Element, + propKey: string, + attributeName: string, + value: any, + extraAttributes: Set, +): 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); +} + +function hydrateSanitizedAttribute( + domElement: Element, + propKey: string, + attributeName: string, + value: any, + extraAttributes: Set, +): 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); } function diffHydratedCustomComponent( @@ -1001,19 +1907,19 @@ function diffHydratedCustomComponent( tag: string, props: Object, parentNamespaceDev: string, - extraAttributeNames: Set, + extraAttributes: Set, ) { for (const propKey in props) { if (!props.hasOwnProperty(propKey)) { continue; } - const nextProp = props[propKey]; - if (nextProp == null) { + const value = props[propKey]; + if (value == null) { continue; } if (registrationNameDependencies.hasOwnProperty(propKey)) { - if (typeof nextProp !== 'function') { - warnForInvalidEventListener(propKey, nextProp); + if (typeof value !== 'function') { + warnForInvalidEventListener(propKey, value); } continue; } @@ -1033,17 +1939,15 @@ function diffHydratedCustomComponent( continue; case 'dangerouslySetInnerHTML': const serverHTML = domElement.innerHTML; - const nextHtml = nextProp ? nextProp.__html : undefined; + const nextHtml = value ? value.__html : undefined; if (nextHtml != null) { const expectedHTML = normalizeHTML(domElement, nextHtml); - if (expectedHTML !== serverHTML) { - warnForPropDifference(propKey, serverHTML, expectedHTML); - } + warnForPropDifference(propKey, serverHTML, expectedHTML); } continue; case 'style': - extraAttributeNames.delete(propKey); - diffHydratedStyles(domElement, nextProp); + extraAttributes.delete(propKey); + diffHydratedStyles(domElement, value); continue; case 'offsetParent': case 'offsetTop': @@ -1054,7 +1958,7 @@ function diffHydratedCustomComponent( case 'outerText': case 'outerHTML': if (enableCustomElementPropertySupport) { - extraAttributeNames.delete(propKey.toLowerCase()); + extraAttributes.delete(propKey.toLowerCase()); if (__DEV__) { console.error( 'Assignment to read-only property will result in a no-op: `%s`', @@ -1067,15 +1971,13 @@ function diffHydratedCustomComponent( case 'className': if (enableCustomElementPropertySupport) { // className is a special cased property on the server to render as an attribute. - extraAttributeNames.delete('class'); + extraAttributes.delete('class'); const serverValue = getValueForAttributeOnCustomComponent( domElement, 'class', - nextProp, + value, ); - if (nextProp !== serverValue) { - warnForPropDifference('className', serverValue, nextProp); - } + warnForPropDifference('className', serverValue, value); continue; } // eslint-disable-next-line no-fallthrough @@ -1085,18 +1987,16 @@ function diffHydratedCustomComponent( ownNamespaceDev = getIntrinsicNamespace(tag); } if (ownNamespaceDev === HTML_NAMESPACE) { - extraAttributeNames.delete(propKey.toLowerCase()); + extraAttributes.delete(propKey.toLowerCase()); } else { - extraAttributeNames.delete(propKey); + extraAttributes.delete(propKey); } const serverValue = getValueForAttributeOnCustomComponent( domElement, propKey, - nextProp, + value, ); - if (nextProp !== serverValue) { - warnForPropDifference(propKey, serverValue, nextProp); - } + warnForPropDifference(propKey, serverValue, value); } } } @@ -1107,19 +2007,19 @@ function diffHydratedGenericElement( tag: string, props: Object, parentNamespaceDev: string, - extraAttributeNames: Set, + extraAttributes: Set, ) { for (const propKey in props) { if (!props.hasOwnProperty(propKey)) { continue; } - const nextProp = props[propKey]; - if (nextProp == null) { + const value = props[propKey]; + if (value == null) { continue; } if (registrationNameDependencies.hasOwnProperty(propKey)) { - if (typeof nextProp !== 'function') { - warnForInvalidEventListener(propKey, nextProp); + if (typeof value !== 'function') { + warnForInvalidEventListener(propKey, value); } continue; } @@ -1142,157 +2042,1110 @@ function diffHydratedGenericElement( continue; case 'dangerouslySetInnerHTML': const serverHTML = domElement.innerHTML; - const nextHtml = nextProp ? nextProp.__html : undefined; + const nextHtml = value ? value.__html : undefined; if (nextHtml != null) { const expectedHTML = normalizeHTML(domElement, nextHtml); - if (expectedHTML !== serverHTML) { - warnForPropDifference(propKey, serverHTML, expectedHTML); - } + warnForPropDifference(propKey, serverHTML, expectedHTML); } continue; case 'style': - extraAttributeNames.delete(propKey); - diffHydratedStyles(domElement, nextProp); + extraAttributes.delete(propKey); + diffHydratedStyles(domElement, value); continue; case 'multiple': { - extraAttributeNames.delete(propKey); + extraAttributes.delete(propKey); const serverValue = (domElement: any).multiple; - if (nextProp !== serverValue) { - warnForPropDifference('multiple', serverValue, nextProp); - } + warnForPropDifference(propKey, serverValue, value); continue; } case 'muted': { - extraAttributeNames.delete(propKey); + extraAttributes.delete(propKey); const serverValue = (domElement: any).muted; - if (nextProp !== serverValue) { - warnForPropDifference('muted', serverValue, nextProp); - } + warnForPropDifference(propKey, serverValue, value); 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 propertyInfo = getPropertyInfo(propKey); - let isMismatchDueToBadCasing = false; - let serverValue; - if (propertyInfo !== null) { - extraAttributeNames.delete(propertyInfo.attributeName); - serverValue = getValueForProperty( - domElement, - propKey, - nextProp, - propertyInfo, - ); - } else { - let ownNamespaceDev = parentNamespaceDev; - if (ownNamespaceDev === HTML_NAMESPACE) { - ownNamespaceDev = getIntrinsicNamespace(tag); - } - if (ownNamespaceDev === HTML_NAMESPACE) { - extraAttributeNames.delete(propKey.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; - extraAttributeNames.delete(standardName); + case 'autoFocus': { + extraAttributes.delete('autofocus'); + const serverValue = (domElement: any).autofocus; + warnForPropDifference(propKey, serverValue, value); + continue; + } + case 'src': + case 'href': + case 'action': + if (enableFilterEmptyStringAttributesDOM) { + if (value === '') { + 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, + ); + } } - extraAttributeNames.delete(propKey); + hydrateSanitizedAttribute( + domElement, + propKey, + propKey, + null, + extraAttributes, + ); + continue; } - serverValue = getValueForAttribute(domElement, propKey, nextProp); - } - - if (nextProp !== serverValue && !isMismatchDueToBadCasing) { - warnForPropDifference(propKey, serverValue, nextProp); } - } - } -} - -export function diffHydratedProperties( - domElement: Element, - tag: string, - props: Object, - isConcurrentMode: boolean, - shouldWarnDev: boolean, - parentNamespaceDev: string, -): null | Array { - 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); + hydrateSanitizedAttribute( + domElement, + propKey, + propKey, + value, + extraAttributes, + ); + continue; + case 'formAction': + hydrateSanitizedAttribute( + domElement, + propKey, + 'formaction', + value, + extraAttributes, + ); + continue; + case 'xlinkHref': + hydrateSanitizedAttribute( + domElement, + propKey, + 'xlink:href', + value, + extraAttributes, + ); + continue; + case 'contentEditable': { + // Lower-case Booleanish String + hydrateBooleanishAttribute( + domElement, + propKey, + 'contenteditable', + value, + extraAttributes, + ); + continue; } - 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': - ReactDOMInputInitWrapperState(domElement, 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. - track((domElement: any)); - // 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. - ReactDOMInputPostMountWrapper(domElement, props, true); - break; - case 'option': - ReactDOMOptionValidateProps(domElement, props); - break; - case 'select': - ReactDOMSelectInitWrapperState(domElement, props); - // We listen to this event in case to ensure emulated bubble + case 'spellCheck': { + // Lower-case Booleanish String + hydrateBooleanishAttribute( + domElement, + propKey, + 'spellcheck', + value, + extraAttributes, + ); + continue; + } + case 'draggable': + case 'autoReverse': + case 'externalResourcesRequired': + case 'focusable': + case 'preserveAlpha': { + // Case-sensitive Booleanish String + hydrateBooleanishAttribute( + domElement, + propKey, + propKey, + value, + extraAttributes, + ); + 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, + ); + continue; + } + case 'capture': + case 'download': { + hydrateOverloadedBooleanAttribute( + domElement, + propKey, + propKey, + value, + extraAttributes, + ); + continue; + } + case 'cols': + case 'rows': + case 'size': + case 'span': { + hydratePositiveNumericAttribute( + domElement, + propKey, + propKey, + value, + extraAttributes, + ); + continue; + } + case 'rowSpan': { + hydrateNumericAttribute( + domElement, + propKey, + 'rowspan', + value, + extraAttributes, + ); + continue; + } + case 'start': { + hydrateNumericAttribute( + domElement, + propKey, + propKey, + value, + extraAttributes, + ); + continue; + } + // A few React string attributes have a different name. + // This is a mapping from React prop names to the attribute names. + case 'acceptCharset': + hydrateAttribute( + domElement, + propKey, + 'accept-charset', + value, + extraAttributes, + ); + continue; + case 'className': + hydrateAttribute(domElement, propKey, 'class', value, extraAttributes); + continue; + case 'htmlFor': + hydrateAttribute(domElement, propKey, 'for', value, extraAttributes); + continue; + case 'httpEquiv': + hydrateAttribute( + domElement, + propKey, + 'http-equiv', + value, + extraAttributes, + ); + continue; + case 'tabIndex': + hydrateAttribute( + domElement, + propKey, + 'tabindex', + value, + extraAttributes, + ); + continue; + case 'crossOrigin': + hydrateAttribute( + domElement, + propKey, + 'crossorigin', + value, + extraAttributes, + ); + continue; + case 'accentHeight': + hydrateAttribute( + domElement, + propKey, + 'accent-height', + value, + extraAttributes, + ); + continue; + case 'alignmentBaseline': + hydrateAttribute( + domElement, + propKey, + 'alignment-baseline', + value, + extraAttributes, + ); + continue; + case 'arabicForm': + hydrateAttribute( + domElement, + propKey, + 'arabic-form', + value, + extraAttributes, + ); + continue; + case 'baselineShift': + hydrateAttribute( + domElement, + propKey, + 'baseline-shift', + value, + extraAttributes, + ); + continue; + case 'capHeight': + hydrateAttribute( + domElement, + propKey, + 'cap-height', + value, + extraAttributes, + ); + continue; + case 'clipPath': + hydrateAttribute( + domElement, + propKey, + 'clip-path', + value, + extraAttributes, + ); + continue; + case 'clipRule': + hydrateAttribute( + domElement, + propKey, + 'clip-rule', + value, + extraAttributes, + ); + continue; + case 'colorInterpolation': + hydrateAttribute( + domElement, + propKey, + 'color-interpolation', + value, + extraAttributes, + ); + continue; + case 'colorInterpolationFilters': + hydrateAttribute( + domElement, + propKey, + 'color-interpolation-filters', + value, + extraAttributes, + ); + continue; + case 'colorProfile': + hydrateAttribute( + domElement, + propKey, + 'color-profile', + value, + extraAttributes, + ); + continue; + case 'colorRendering': + hydrateAttribute( + domElement, + propKey, + 'color-rendering', + value, + extraAttributes, + ); + continue; + case 'dominantBaseline': + hydrateAttribute( + domElement, + propKey, + 'dominant-baseline', + value, + extraAttributes, + ); + continue; + case 'enableBackground': + hydrateAttribute( + domElement, + propKey, + 'enable-background', + value, + extraAttributes, + ); + continue; + case 'fillOpacity': + hydrateAttribute( + domElement, + propKey, + 'fill-opacity', + value, + extraAttributes, + ); + continue; + case 'fillRule': + hydrateAttribute( + domElement, + propKey, + 'fill-rule', + value, + extraAttributes, + ); + continue; + case 'floodColor': + hydrateAttribute( + domElement, + propKey, + 'flood-color', + value, + extraAttributes, + ); + continue; + case 'floodOpacity': + hydrateAttribute( + domElement, + propKey, + 'flood-opacity', + value, + extraAttributes, + ); + continue; + case 'fontFamily': + hydrateAttribute( + domElement, + propKey, + 'font-family', + value, + extraAttributes, + ); + continue; + case 'fontSize': + hydrateAttribute( + domElement, + propKey, + 'font-size', + value, + extraAttributes, + ); + continue; + case 'fontSizeAdjust': + hydrateAttribute( + domElement, + propKey, + 'font-size-adjust', + value, + extraAttributes, + ); + continue; + case 'fontStretch': + hydrateAttribute( + domElement, + propKey, + 'font-stretch', + value, + extraAttributes, + ); + continue; + case 'fontStyle': + hydrateAttribute( + domElement, + propKey, + 'font-style', + value, + extraAttributes, + ); + continue; + case 'fontVariant': + hydrateAttribute( + domElement, + propKey, + 'font-variant', + value, + extraAttributes, + ); + continue; + case 'fontWeight': + hydrateAttribute( + domElement, + propKey, + 'font-weight', + value, + extraAttributes, + ); + continue; + case 'glyphName': + hydrateAttribute( + domElement, + propKey, + 'glyph-name', + value, + extraAttributes, + ); + continue; + case 'glyphOrientationHorizontal': + hydrateAttribute( + domElement, + propKey, + 'glyph-orientation-horizontal', + value, + extraAttributes, + ); + continue; + case 'glyphOrientationVertical': + hydrateAttribute( + domElement, + propKey, + 'glyph-orientation-vertical', + value, + extraAttributes, + ); + continue; + case 'horizAdvX': + hydrateAttribute( + domElement, + propKey, + 'horiz-adv-x', + value, + extraAttributes, + ); + continue; + case 'horizOriginX': + hydrateAttribute( + domElement, + propKey, + 'horiz-origin-x', + value, + extraAttributes, + ); + continue; + case 'imageRendering': + hydrateAttribute( + domElement, + propKey, + 'image-rendering', + value, + extraAttributes, + ); + continue; + case 'letterSpacing': + hydrateAttribute( + domElement, + propKey, + 'letter-spacing', + value, + extraAttributes, + ); + continue; + case 'lightingColor': + hydrateAttribute( + domElement, + propKey, + 'lighting-color', + value, + extraAttributes, + ); + continue; + case 'markerEnd': + hydrateAttribute( + domElement, + propKey, + 'marker-end', + value, + extraAttributes, + ); + continue; + case 'markerMid': + hydrateAttribute( + domElement, + propKey, + 'marker-mid', + value, + extraAttributes, + ); + continue; + case 'markerStart': + hydrateAttribute( + domElement, + propKey, + 'marker-start', + value, + extraAttributes, + ); + continue; + case 'overlinePosition': + hydrateAttribute( + domElement, + propKey, + 'overline-position', + value, + extraAttributes, + ); + continue; + case 'overlineThickness': + hydrateAttribute( + domElement, + propKey, + 'overline-thickness', + value, + extraAttributes, + ); + continue; + case 'paintOrder': + hydrateAttribute( + domElement, + propKey, + 'paint-order', + value, + extraAttributes, + ); + continue; + case 'panose-1': + hydrateAttribute( + domElement, + propKey, + 'panose-1', + value, + extraAttributes, + ); + continue; + case 'pointerEvents': + hydrateAttribute( + domElement, + propKey, + 'pointer-events', + value, + extraAttributes, + ); + continue; + case 'renderingIntent': + hydrateAttribute( + domElement, + propKey, + 'rendering-intent', + value, + extraAttributes, + ); + continue; + case 'shapeRendering': + hydrateAttribute( + domElement, + propKey, + 'shape-rendering', + value, + extraAttributes, + ); + continue; + case 'stopColor': + hydrateAttribute( + domElement, + propKey, + 'stop-color', + value, + extraAttributes, + ); + continue; + case 'stopOpacity': + hydrateAttribute( + domElement, + propKey, + 'stop-opacity', + value, + extraAttributes, + ); + continue; + case 'strikethroughPosition': + hydrateAttribute( + domElement, + propKey, + 'strikethrough-position', + value, + extraAttributes, + ); + continue; + case 'strikethroughThickness': + hydrateAttribute( + domElement, + propKey, + 'strikethrough-thickness', + value, + extraAttributes, + ); + continue; + case 'strokeDasharray': + hydrateAttribute( + domElement, + propKey, + 'stroke-dasharray', + value, + extraAttributes, + ); + continue; + case 'strokeDashoffset': + hydrateAttribute( + domElement, + propKey, + 'stroke-dashoffset', + value, + extraAttributes, + ); + continue; + case 'strokeLinecap': + hydrateAttribute( + domElement, + propKey, + 'stroke-linecap', + value, + extraAttributes, + ); + continue; + case 'strokeLinejoin': + hydrateAttribute( + domElement, + propKey, + 'stroke-linejoin', + value, + extraAttributes, + ); + continue; + case 'strokeMiterlimit': + hydrateAttribute( + domElement, + propKey, + 'stroke-miterlimit', + value, + extraAttributes, + ); + continue; + case 'strokeOpacity': + hydrateAttribute( + domElement, + propKey, + 'stroke-opacity', + value, + extraAttributes, + ); + continue; + case 'strokeWidth': + hydrateAttribute( + domElement, + propKey, + 'stroke-width', + value, + extraAttributes, + ); + continue; + case 'textAnchor': + hydrateAttribute( + domElement, + propKey, + 'text-anchor', + value, + extraAttributes, + ); + continue; + case 'textDecoration': + hydrateAttribute( + domElement, + propKey, + 'text-decoration', + value, + extraAttributes, + ); + continue; + case 'textRendering': + hydrateAttribute( + domElement, + propKey, + 'text-rendering', + value, + extraAttributes, + ); + continue; + case 'transformOrigin': + hydrateAttribute( + domElement, + propKey, + 'transform-origin', + value, + extraAttributes, + ); + continue; + case 'underlinePosition': + hydrateAttribute( + domElement, + propKey, + 'underline-position', + value, + extraAttributes, + ); + continue; + case 'underlineThickness': + hydrateAttribute( + domElement, + propKey, + 'underline-thickness', + value, + extraAttributes, + ); + continue; + case 'unicodeBidi': + hydrateAttribute( + domElement, + propKey, + 'unicode-bidi', + value, + extraAttributes, + ); + continue; + case 'unicodeRange': + hydrateAttribute( + domElement, + propKey, + 'unicode-range', + value, + extraAttributes, + ); + continue; + case 'unitsPerEm': + hydrateAttribute( + domElement, + propKey, + 'units-per-em', + value, + extraAttributes, + ); + continue; + case 'vAlphabetic': + hydrateAttribute( + domElement, + propKey, + 'v-alphabetic', + value, + extraAttributes, + ); + continue; + case 'vHanging': + hydrateAttribute( + domElement, + propKey, + 'v-hanging', + value, + extraAttributes, + ); + continue; + case 'vIdeographic': + hydrateAttribute( + domElement, + propKey, + 'v-ideographic', + value, + extraAttributes, + ); + continue; + case 'vMathematical': + hydrateAttribute( + domElement, + propKey, + 'v-mathematical', + value, + extraAttributes, + ); + continue; + case 'vectorEffect': + hydrateAttribute( + domElement, + propKey, + 'vector-effect', + value, + extraAttributes, + ); + continue; + case 'vertAdvY': + hydrateAttribute( + domElement, + propKey, + 'vert-adv-y', + value, + extraAttributes, + ); + continue; + case 'vertOriginX': + hydrateAttribute( + domElement, + propKey, + 'vert-origin-x', + value, + extraAttributes, + ); + continue; + case 'vertOriginY': + hydrateAttribute( + domElement, + propKey, + 'vert-origin-y', + value, + extraAttributes, + ); + continue; + case 'wordSpacing': + hydrateAttribute( + domElement, + propKey, + 'word-spacing', + value, + extraAttributes, + ); + continue; + case 'writingMode': + hydrateAttribute( + domElement, + propKey, + 'writing-mode', + value, + extraAttributes, + ); + continue; + case 'xmlnsXlink': + hydrateAttribute( + domElement, + propKey, + 'xmlns:xlink', + value, + extraAttributes, + ); + continue; + case 'xHeight': + hydrateAttribute( + domElement, + propKey, + 'x-height', + value, + extraAttributes, + ); + continue; + case 'xlinkActuate': + hydrateAttribute( + domElement, + propKey, + 'xlink:actuate', + value, + extraAttributes, + ); + continue; + case 'xlinkArcrole': + hydrateAttribute( + domElement, + propKey, + 'xlink:arcrole', + value, + extraAttributes, + ); + continue; + case 'xlinkRole': + hydrateAttribute( + domElement, + propKey, + 'xlink:role', + value, + extraAttributes, + ); + continue; + case 'xlinkShow': + hydrateAttribute( + domElement, + propKey, + 'xlink:show', + value, + extraAttributes, + ); + continue; + case 'xlinkTitle': + hydrateAttribute( + domElement, + propKey, + 'xlink:title', + value, + extraAttributes, + ); + continue; + case 'xlinkType': + hydrateAttribute( + domElement, + propKey, + 'xlink:type', + value, + extraAttributes, + ); + continue; + case 'xmlBase': + hydrateAttribute( + domElement, + propKey, + 'xml:base', + value, + extraAttributes, + ); + continue; + case 'xmlLang': + hydrateAttribute( + domElement, + propKey, + 'xml:lang', + value, + extraAttributes, + ); + continue; + case 'xmlSpace': + hydrateAttribute( + domElement, + propKey, + 'xml:space', + value, + extraAttributes, + ); + 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; + } + let isMismatchDueToBadCasing = false; + let ownNamespaceDev = parentNamespaceDev; + if (ownNamespaceDev === HTML_NAMESPACE) { + ownNamespaceDev = getIntrinsicNamespace(tag); + } + if (ownNamespaceDev === HTML_NAMESPACE) { + extraAttributes.delete(propKey.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(propKey); + } + const serverValue = getValueForAttribute(domElement, propKey, value); + if (!isMismatchDueToBadCasing) { + warnForPropDifference(propKey, serverValue, value); + } + } + } + } +} + +export function diffHydratedProperties( + domElement: Element, + tag: string, + props: Object, + isConcurrentMode: boolean, + shouldWarnDev: boolean, + parentNamespaceDev: string, +): null | Array { + 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': + ReactDOMInputInitWrapperState(domElement, 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. + track((domElement: any)); + // 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. + ReactDOMInputPostMountWrapper(domElement, props, true); + break; + case 'option': + ReactDOMOptionValidateProps(domElement, props); + break; + case 'select': + ReactDOMSelectInitWrapperState(domElement, props); + // We listen to this event in case to ensure emulated bubble // listeners still fire for the invalid event. listenToNonDelegatedEvent('invalid', domElement); break; @@ -1346,7 +3199,7 @@ export function diffHydratedProperties( } if (__DEV__ && shouldWarnDev) { - const extraAttributeNames: Set = new Set(); + const extraAttributes: Set = new Set(); const attributes = domElement.attributes; for (let i = 0; i < attributes.length; i++) { const name = attributes[i].name.toLowerCase(); @@ -1362,7 +3215,7 @@ export function diffHydratedProperties( default: // Intentionally use the original name. // See discussion in https://github.com/facebook/react/pull/10676. - extraAttributeNames.add(attributes[i].name); + extraAttributes.add(attributes[i].name); } } if (isCustomElement(tag, props)) { @@ -1371,7 +3224,7 @@ export function diffHydratedProperties( tag, props, parentNamespaceDev, - extraAttributeNames, + extraAttributes, ); } else { diffHydratedGenericElement( @@ -1379,14 +3232,11 @@ export function diffHydratedProperties( tag, props, parentNamespaceDev, - extraAttributeNames, + extraAttributes, ); } - if ( - extraAttributeNames.size > 0 && - props.suppressHydrationWarning !== true - ) { - warnForExtraAttributes(extraAttributeNames); + if (extraAttributes.size > 0 && props.suppressHydrationWarning !== true) { + warnForExtraAttributes(extraAttributes); } } diff --git a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js index 60d53197bd44a..601706df50157 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js +++ b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js @@ -39,13 +39,6 @@ import { } from 'react-server/src/ReactServerStreamConfig'; import isAttributeNameSafe from '../shared/isAttributeNameSafe'; -import { - getPropertyInfo, - BOOLEAN, - OVERLOADED_BOOLEAN, - NUMERIC, - POSITIVE_NUMERIC, -} from '../shared/DOMProperty'; import isUnitlessNumber from '../shared/isUnitlessNumber'; import {checkControlledValueProps} from '../shared/ReactControlledValuePropTypes'; @@ -621,6 +614,26 @@ function pushBooleanAttribute( } } +function pushStringAttribute( + target: Array, + name: string, + value: string | boolean | number | Function | Object, // not null or undefined +): void { + if ( + typeof value !== 'function' && + typeof value !== 'symbol' && + typeof value !== 'boolean' + ) { + target.push( + attributeSeparator, + stringToChunk(name), + attributeAssign, + stringToChunk(escapeTextForBrowser(value)), + attributeEnd, + ); + } +} + function pushAttribute( target: Array, name: string, @@ -638,151 +651,505 @@ function pushAttribute( case 'suppressHydrationWarning': // Ignored. These are built-in to React on the client. return; + case 'autoFocus': case 'multiple': - case 'muted': - pushBooleanAttribute(target, name, value); + case 'muted': { + pushBooleanAttribute(target, name.toLowerCase(), value); return; - } - if ( - // shouldIgnoreAttribute - // We have already filtered out null/undefined and reserved words. - name.length > 2 && - (name[0] === 'o' || name[0] === 'O') && - (name[1] === 'n' || name[1] === 'N') - ) { - return; - } - - const propertyInfo = getPropertyInfo(name); - if (propertyInfo !== null) { - // shouldRemoveAttribute - switch (typeof value) { - case 'function': - case 'symbol': // eslint-disable-line - return; - case 'boolean': { - if (!propertyInfo.acceptsBooleans) { - return; - } - } } - if (enableFilterEmptyStringAttributesDOM) { - if (propertyInfo.removeEmptyString && value === '') { - if (__DEV__) { - if (name === '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.', - name, - name, - ); - } 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.', - name, - name, - ); + case 'src': + case 'href': + case 'action': + if (enableFilterEmptyStringAttributesDOM) { + if (value === '') { + if (__DEV__) { + if (name === '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.', + name, + name, + ); + } 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.', + name, + name, + ); + } } + return; } + } + // Fall through to the last case which shouldn't remove empty strings. + // eslint-disable-next-line no-fallthrough + case 'formAction': { + if ( + value == null || + typeof value === 'function' || + typeof value === 'symbol' || + typeof value === 'boolean' + ) { return; } + if (__DEV__) { + checkAttributeStringCoercion(value, name); + } + const sanitizedValue = sanitizeURL('' + value); + target.push( + attributeSeparator, + stringToChunk(name), + attributeAssign, + stringToChunk(escapeTextForBrowser(sanitizedValue)), + attributeEnd, + ); + return; } - - const attributeName = propertyInfo.attributeName; - const attributeNameChunk = stringToChunk(attributeName); // TODO: If it's known we can cache the chunk. - - switch (propertyInfo.type) { - case BOOLEAN: - if (value) { - target.push( - attributeSeparator, - attributeNameChunk, - attributeEmptyString, - ); - } - return; - case OVERLOADED_BOOLEAN: - if (value === true) { - target.push( - attributeSeparator, - attributeNameChunk, - attributeEmptyString, - ); - } else if (value === false) { - // Ignored - } else { - target.push( - attributeSeparator, - attributeNameChunk, - attributeAssign, - stringToChunk(escapeTextForBrowser(value)), - attributeEnd, - ); - } + case 'xlinkHref': { + if ( + typeof value === 'function' || + typeof value === 'symbol' || + typeof value === 'boolean' + ) { return; - case NUMERIC: - if (!isNaN(value)) { - target.push( - attributeSeparator, - attributeNameChunk, - attributeAssign, - stringToChunk(escapeTextForBrowser(value)), - attributeEnd, - ); - } - break; - case POSITIVE_NUMERIC: - if (!isNaN(value) && (value: any) >= 1) { - target.push( - attributeSeparator, - attributeNameChunk, - attributeAssign, - stringToChunk(escapeTextForBrowser(value)), - attributeEnd, - ); - } - break; - default: - if (__DEV__) { - checkAttributeStringCoercion(value, attributeName); - } - if (propertyInfo.sanitizeURL) { - // We've already checked above. - // eslint-disable-next-line react-internal/safe-string-coercion - value = sanitizeURL('' + (value: any)); - } + } + if (__DEV__) { + checkAttributeStringCoercion(value, name); + } + const sanitizedValue = sanitizeURL('' + value); + target.push( + attributeSeparator, + stringToChunk('xlink:href'), + attributeAssign, + stringToChunk(escapeTextForBrowser(sanitizedValue)), + attributeEnd, + ); + return; + } + 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). + if (typeof value !== 'function' && typeof value !== 'symbol') { target.push( attributeSeparator, - attributeNameChunk, + stringToChunk(name), attributeAssign, stringToChunk(escapeTextForBrowser(value)), attributeEnd, ); + } + return; + } + 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': { + // Boolean + if (value && typeof value !== 'function' && typeof value !== 'symbol') { + target.push( + attributeSeparator, + stringToChunk(name), + attributeEmptyString, + ); + } + return; + } + case 'capture': + case 'download': { + // Overloaded Boolean + if (value === true) { + target.push( + attributeSeparator, + stringToChunk(name), + attributeEmptyString, + ); + } else if (value === false) { + // Ignored + } else if (typeof value !== 'function' && typeof value !== 'symbol') { + target.push( + attributeSeparator, + stringToChunk(name), + attributeAssign, + stringToChunk(escapeTextForBrowser(value)), + attributeEnd, + ); + } + return; + } + case 'cols': + case 'rows': + case 'size': + case 'span': { + // These are HTML attributes that must be positive numbers. + if ( + typeof value !== 'function' && + typeof value !== 'symbol' && + !isNaN(value) && + (value: any) >= 1 + ) { + target.push( + attributeSeparator, + stringToChunk(name), + attributeAssign, + stringToChunk(escapeTextForBrowser(value)), + attributeEnd, + ); + } + return; + } + case 'rowSpan': + case 'start': { + // These are HTML attributes that must be numbers. + if ( + typeof value !== 'function' && + typeof value !== 'symbol' && + !isNaN(value) + ) { + target.push( + attributeSeparator, + stringToChunk(name), + attributeAssign, + stringToChunk(escapeTextForBrowser(value)), + attributeEnd, + ); + } + return; } - } else if (isAttributeNameSafe(name)) { - // shouldRemoveAttribute - switch (typeof value) { - case 'function': - case 'symbol': // eslint-disable-line + // A few React string attributes have a different name. + // This is a mapping from React prop names to the attribute names. + case 'acceptCharset': + pushStringAttribute(target, 'accept-charset', value); + return; + case 'className': + pushStringAttribute(target, 'class', value); + return; + case 'htmlFor': + pushStringAttribute(target, 'for', value); + return; + case 'httpEquiv': + pushStringAttribute(target, 'http-equiv', value); + return; + // HTML and SVG attributes, but the SVG attribute is case sensitive. + case 'tabIndex': + pushStringAttribute(target, 'tabindex', value); + return; + case 'crossOrigin': + pushStringAttribute(target, 'crossorigin', value); + return; + // This is a list of all SVG attributes that need special casing. + // Regular attributes that just accept strings. + case 'accentHeight': + pushStringAttribute(target, 'accent-height', value); + return; + case 'alignmentBaseline': + pushStringAttribute(target, 'alignment-baseline', value); + return; + case 'arabicForm': + pushStringAttribute(target, 'arabic-form', value); + return; + case 'baselineShift': + pushStringAttribute(target, 'baseline-shift', value); + return; + case 'capHeight': + pushStringAttribute(target, 'cap-height', value); + return; + case 'clipPath': + pushStringAttribute(target, 'clip-path', value); + return; + case 'clipRule': + pushStringAttribute(target, 'clip-rule', value); + return; + case 'colorInterpolation': + pushStringAttribute(target, 'color-interpolation', value); + return; + case 'colorInterpolationFilters': + pushStringAttribute(target, 'color-interpolation-filters', value); + return; + case 'colorProfile': + pushStringAttribute(target, 'color-profile', value); + return; + case 'colorRendering': + pushStringAttribute(target, 'color-rendering', value); + return; + case 'dominantBaseline': + pushStringAttribute(target, 'dominant-baseline', value); + return; + case 'enableBackground': + pushStringAttribute(target, 'enable-background', value); + return; + case 'fillOpacity': + pushStringAttribute(target, 'fill-opacity', value); + return; + case 'fillRule': + pushStringAttribute(target, 'fill-rule', value); + return; + case 'floodColor': + pushStringAttribute(target, 'flood-color', value); + return; + case 'floodOpacity': + pushStringAttribute(target, 'flood-opacity', value); + return; + case 'fontFamily': + pushStringAttribute(target, 'font-family', value); + return; + case 'fontSize': + pushStringAttribute(target, 'font-size', value); + return; + case 'fontSizeAdjust': + pushStringAttribute(target, 'font-size-adjust', value); + return; + case 'fontStretch': + pushStringAttribute(target, 'font-stretch', value); + return; + case 'fontStyle': + pushStringAttribute(target, 'font-style', value); + return; + case 'fontVariant': + pushStringAttribute(target, 'font-variant', value); + return; + case 'fontWeight': + pushStringAttribute(target, 'font-weight', value); + return; + case 'glyphName': + pushStringAttribute(target, 'glyph-name', value); + return; + case 'glyphOrientationHorizontal': + pushStringAttribute(target, 'glyph-orientation-horizontal', value); + return; + case 'glyphOrientationVertical': + pushStringAttribute(target, 'glyph-orientation-vertical', value); + return; + case 'horizAdvX': + pushStringAttribute(target, 'horiz-adv-x', value); + return; + case 'horizOriginX': + pushStringAttribute(target, 'horiz-origin-x', value); + return; + case 'imageRendering': + pushStringAttribute(target, 'image-rendering', value); + return; + case 'letterSpacing': + pushStringAttribute(target, 'letter-spacing', value); + return; + case 'lightingColor': + pushStringAttribute(target, 'lighting-color', value); + return; + case 'markerEnd': + pushStringAttribute(target, 'marker-end', value); + return; + case 'markerMid': + pushStringAttribute(target, 'marker-mid', value); + return; + case 'markerStart': + pushStringAttribute(target, 'marker-start', value); + return; + case 'overlinePosition': + pushStringAttribute(target, 'overline-position', value); + return; + case 'overlineThickness': + pushStringAttribute(target, 'overline-thickness', value); + return; + case 'paintOrder': + pushStringAttribute(target, 'paint-order', value); + return; + case 'panose-1': + pushStringAttribute(target, 'panose-1', value); + return; + case 'pointerEvents': + pushStringAttribute(target, 'pointer-events', value); + return; + case 'renderingIntent': + pushStringAttribute(target, 'rendering-intent', value); + return; + case 'shapeRendering': + pushStringAttribute(target, 'shape-rendering', value); + return; + case 'stopColor': + pushStringAttribute(target, 'stop-color', value); + return; + case 'stopOpacity': + pushStringAttribute(target, 'stop-opacity', value); + return; + case 'strikethroughPosition': + pushStringAttribute(target, 'strikethrough-position', value); + return; + case 'strikethroughThickness': + pushStringAttribute(target, 'strikethrough-thickness', value); + return; + case 'strokeDasharray': + pushStringAttribute(target, 'stroke-dasharray', value); + return; + case 'strokeDashoffset': + pushStringAttribute(target, 'stroke-dashoffset', value); + return; + case 'strokeLinecap': + pushStringAttribute(target, 'stroke-linecap', value); + return; + case 'strokeLinejoin': + pushStringAttribute(target, 'stroke-linejoin', value); + return; + case 'strokeMiterlimit': + pushStringAttribute(target, 'stroke-miterlimit', value); + return; + case 'strokeOpacity': + pushStringAttribute(target, 'stroke-opacity', value); + return; + case 'strokeWidth': + pushStringAttribute(target, 'stroke-width', value); + return; + case 'textAnchor': + pushStringAttribute(target, 'text-anchor', value); + return; + case 'textDecoration': + pushStringAttribute(target, 'text-decoration', value); + return; + case 'textRendering': + pushStringAttribute(target, 'text-rendering', value); + return; + case 'transformOrigin': + pushStringAttribute(target, 'transform-origin', value); + return; + case 'underlinePosition': + pushStringAttribute(target, 'underline-position', value); + return; + case 'underlineThickness': + pushStringAttribute(target, 'underline-thickness', value); + return; + case 'unicodeBidi': + pushStringAttribute(target, 'unicode-bidi', value); + return; + case 'unicodeRange': + pushStringAttribute(target, 'unicode-range', value); + return; + case 'unitsPerEm': + pushStringAttribute(target, 'units-per-em', value); + return; + case 'vAlphabetic': + pushStringAttribute(target, 'v-alphabetic', value); + return; + case 'vHanging': + pushStringAttribute(target, 'v-hanging', value); + return; + case 'vIdeographic': + pushStringAttribute(target, 'v-ideographic', value); + return; + case 'vMathematical': + pushStringAttribute(target, 'v-mathematical', value); + return; + case 'vectorEffect': + pushStringAttribute(target, 'vector-effect', value); + return; + case 'vertAdvY': + pushStringAttribute(target, 'vert-adv-y', value); + return; + case 'vertOriginX': + pushStringAttribute(target, 'vert-origin-x', value); + return; + case 'vertOriginY': + pushStringAttribute(target, 'vert-origin-y', value); + return; + case 'wordSpacing': + pushStringAttribute(target, 'word-spacing', value); + return; + case 'writingMode': + pushStringAttribute(target, 'writing-mode', value); + return; + case 'xmlnsXlink': + pushStringAttribute(target, 'xmlns:xlink', value); + return; + case 'xHeight': + pushStringAttribute(target, 'x-height', value); + return; + case 'xlinkActuate': + pushStringAttribute(target, 'xlink:actuate', value); + break; + case 'xlinkArcrole': + pushStringAttribute(target, 'xlink:arcrole', value); + break; + case 'xlinkRole': + pushStringAttribute(target, 'xlink:role', value); + break; + case 'xlinkShow': + pushStringAttribute(target, 'xlink:show', value); + break; + case 'xlinkTitle': + pushStringAttribute(target, 'xlink:title', value); + break; + case 'xlinkType': + pushStringAttribute(target, 'xlink:type', value); + break; + case 'xmlBase': + pushStringAttribute(target, 'xml:base', value); + break; + case 'xmlLang': + pushStringAttribute(target, 'xml:lang', value); + break; + case 'xmlSpace': + pushStringAttribute(target, 'xml:space', value); + break; + default: + if ( + // shouldIgnoreAttribute + // We have already filtered out null/undefined and reserved words. + name.length > 2 && + (name[0] === 'o' || name[0] === 'O') && + (name[1] === 'n' || name[1] === 'N') + ) { return; - case 'boolean': { - const prefix = name.toLowerCase().slice(0, 5); - if (prefix !== 'data-' && prefix !== 'aria-') { - return; + } + + if (isAttributeNameSafe(name)) { + // shouldRemoveAttribute + switch (typeof value) { + case 'function': + case 'symbol': // eslint-disable-line + return; + case 'boolean': { + const prefix = name.toLowerCase().slice(0, 5); + if (prefix !== 'data-' && prefix !== 'aria-') { + return; + } + } } + target.push( + attributeSeparator, + stringToChunk(name), + attributeAssign, + stringToChunk(escapeTextForBrowser(value)), + attributeEnd, + ); } - } - target.push( - attributeSeparator, - stringToChunk(name), - attributeAssign, - stringToChunk(escapeTextForBrowser(value)), - attributeEnd, - ); } } diff --git a/packages/react-dom-bindings/src/shared/DOMProperty.js b/packages/react-dom-bindings/src/shared/DOMProperty.js deleted file mode 100644 index 8bd96110f3a50..0000000000000 --- a/packages/react-dom-bindings/src/shared/DOMProperty.js +++ /dev/null @@ -1,411 +0,0 @@ -/** - * 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 - */ - -type PropertyType = 0 | 1 | 2 | 3 | 4 | 5 | 6; - -// A simple string attribute. -// Attributes that aren't in the filter are presumed to have this type. -export const STRING = 1; - -// A string attribute that accepts booleans in React. In HTML, these are called -// "enumerated" attributes with "true" and "false" as possible values. -// When true, it should be set to a "true" string. -// When false, it should be set to a "false" string. -export const BOOLEANISH_STRING = 2; - -// A real boolean attribute. -// When true, it should be present (set either to an empty string or its name). -// When false, it should be omitted. -export const BOOLEAN = 3; - -// 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. -export const OVERLOADED_BOOLEAN = 4; - -// An attribute that must be numeric or parse as a numeric. -// When falsy, it should be removed. -export const NUMERIC = 5; - -// An attribute that must be positive numeric or parse as a positive numeric. -// When falsy, it should be removed. -export const POSITIVE_NUMERIC = 6; - -export type PropertyInfo = { - +acceptsBooleans: boolean, - +attributeName: string, - +attributeNamespace: string | null, - +type: PropertyType, - +sanitizeURL: boolean, - +removeEmptyString: boolean, -}; - -export function getPropertyInfo(name: string): PropertyInfo | null { - return properties.hasOwnProperty(name) ? properties[name] : null; -} - -// $FlowFixMe[missing-this-annot] -function PropertyInfoRecord( - type: PropertyType, - attributeName: string, - attributeNamespace: string | null, - sanitizeURL: boolean, - removeEmptyString: boolean, -) { - this.acceptsBooleans = - type === BOOLEANISH_STRING || - type === BOOLEAN || - type === OVERLOADED_BOOLEAN; - this.attributeName = attributeName; - this.attributeNamespace = attributeNamespace; - this.type = type; - this.sanitizeURL = sanitizeURL; - this.removeEmptyString = removeEmptyString; -} - -// When adding attributes to this list, be sure to also add them to -// the `possibleStandardNames` module to ensure casing and incorrect -// name warnings. -const properties: {[string]: $FlowFixMe} = {}; - -// A few React string attributes have a different name. -// This is a mapping from React prop names to the attribute names. -[ - ['acceptCharset', 'accept-charset'], - ['className', 'class'], - ['htmlFor', 'for'], - ['httpEquiv', 'http-equiv'], -].forEach(([name, attributeName]) => { - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions - properties[name] = new PropertyInfoRecord( - STRING, - attributeName, // attributeName - null, // attributeNamespace - false, // sanitizeURL - false, // removeEmptyString - ); -}); - -// These are "enumerated" HTML 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). -['contentEditable', 'draggable', 'spellCheck', 'value'].forEach(name => { - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions - properties[name] = new PropertyInfoRecord( - BOOLEANISH_STRING, - name.toLowerCase(), // attributeName - null, // attributeNamespace - false, // sanitizeURL - false, // removeEmptyString - ); -}); - -// These are "enumerated" SVG 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). -// Since these are SVG attributes, their attribute names are case-sensitive. -[ - 'autoReverse', - 'externalResourcesRequired', - 'focusable', - 'preserveAlpha', -].forEach(name => { - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions - properties[name] = new PropertyInfoRecord( - BOOLEANISH_STRING, - name, // attributeName - null, // attributeNamespace - false, // sanitizeURL - false, // removeEmptyString - ); -}); - -// These are HTML boolean attributes. -[ - 'allowFullScreen', - 'async', - // Note: there is a special case that prevents it from being written to the DOM - // on the client side because the browsers are inconsistent. Instead we call focus(). - 'autoFocus', - 'autoPlay', - 'controls', - 'default', - 'defer', - 'disabled', - 'disablePictureInPicture', - 'disableRemotePlayback', - 'formNoValidate', - 'hidden', - 'loop', - 'noModule', - 'noValidate', - 'open', - 'playsInline', - 'readOnly', - 'required', - 'reversed', - 'scoped', - 'seamless', - // Microdata - 'itemScope', -].forEach(name => { - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions - properties[name] = new PropertyInfoRecord( - BOOLEAN, - name.toLowerCase(), // attributeName - null, // attributeNamespace - false, // sanitizeURL - false, // removeEmptyString - ); -}); - -// These are HTML attributes that are "overloaded booleans": they behave like -// booleans, but can also accept a string value. -[ - 'capture', - 'download', - - // NOTE: if you add a camelCased prop to this list, - // you'll need to set attributeName to name.toLowerCase() - // instead in the assignment below. -].forEach(name => { - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions - properties[name] = new PropertyInfoRecord( - OVERLOADED_BOOLEAN, - name, // attributeName - null, // attributeNamespace - false, // sanitizeURL - false, // removeEmptyString - ); -}); - -// These are HTML attributes that must be positive numbers. -[ - 'cols', - 'rows', - 'size', - 'span', - - // NOTE: if you add a camelCased prop to this list, - // you'll need to set attributeName to name.toLowerCase() - // instead in the assignment below. -].forEach(name => { - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions - properties[name] = new PropertyInfoRecord( - POSITIVE_NUMERIC, - name, // attributeName - null, // attributeNamespace - false, // sanitizeURL - false, // removeEmptyString - ); -}); - -// These are HTML attributes that must be numbers. -['rowSpan', 'start'].forEach(name => { - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions - properties[name] = new PropertyInfoRecord( - NUMERIC, - name.toLowerCase(), // attributeName - null, // attributeNamespace - false, // sanitizeURL - false, // removeEmptyString - ); -}); - -const CAMELIZE = /[\-\:]([a-z])/g; -const capitalize = (token: string) => token[1].toUpperCase(); - -// This is a list of all SVG attributes that need special casing, namespacing, -// or boolean value assignment. Regular attributes that just accept strings -// and have the same names are omitted, just like in the HTML attribute filter. -// Some of these attributes can be hard to find. This list was created by -// scraping the MDN documentation. -[ - 'accent-height', - 'alignment-baseline', - 'arabic-form', - 'baseline-shift', - 'cap-height', - 'clip-path', - 'clip-rule', - 'color-interpolation', - 'color-interpolation-filters', - 'color-profile', - 'color-rendering', - 'dominant-baseline', - 'enable-background', - 'fill-opacity', - 'fill-rule', - 'flood-color', - 'flood-opacity', - 'font-family', - 'font-size', - 'font-size-adjust', - 'font-stretch', - 'font-style', - 'font-variant', - 'font-weight', - 'glyph-name', - 'glyph-orientation-horizontal', - 'glyph-orientation-vertical', - 'horiz-adv-x', - 'horiz-origin-x', - 'image-rendering', - 'letter-spacing', - 'lighting-color', - 'marker-end', - 'marker-mid', - 'marker-start', - 'overline-position', - 'overline-thickness', - 'paint-order', - 'panose-1', - 'pointer-events', - 'rendering-intent', - 'shape-rendering', - 'stop-color', - 'stop-opacity', - 'strikethrough-position', - 'strikethrough-thickness', - 'stroke-dasharray', - 'stroke-dashoffset', - 'stroke-linecap', - 'stroke-linejoin', - 'stroke-miterlimit', - 'stroke-opacity', - 'stroke-width', - 'text-anchor', - 'text-decoration', - 'text-rendering', - 'transform-origin', - 'underline-position', - 'underline-thickness', - 'unicode-bidi', - 'unicode-range', - 'units-per-em', - 'v-alphabetic', - 'v-hanging', - 'v-ideographic', - 'v-mathematical', - 'vector-effect', - 'vert-adv-y', - 'vert-origin-x', - 'vert-origin-y', - 'word-spacing', - 'writing-mode', - 'xmlns:xlink', - 'x-height', - - // NOTE: if you add a camelCased prop to this list, - // you'll need to set attributeName to name.toLowerCase() - // instead in the assignment below. -].forEach(attributeName => { - const name = attributeName.replace(CAMELIZE, capitalize); - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions - properties[name] = new PropertyInfoRecord( - STRING, - attributeName, - null, // attributeNamespace - false, // sanitizeURL - false, // removeEmptyString - ); -}); - -// String SVG attributes with the xlink namespace. -[ - 'xlink:actuate', - 'xlink:arcrole', - 'xlink:role', - 'xlink:show', - 'xlink:title', - 'xlink:type', - - // NOTE: if you add a camelCased prop to this list, - // you'll need to set attributeName to name.toLowerCase() - // instead in the assignment below. -].forEach(attributeName => { - const name = attributeName.replace(CAMELIZE, capitalize); - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions - properties[name] = new PropertyInfoRecord( - STRING, - attributeName, - 'http://www.w3.org/1999/xlink', - false, // sanitizeURL - false, // removeEmptyString - ); -}); - -// String SVG attributes with the xml namespace. -[ - 'xml:base', - 'xml:lang', - 'xml:space', - - // NOTE: if you add a camelCased prop to this list, - // you'll need to set attributeName to name.toLowerCase() - // instead in the assignment below. -].forEach(attributeName => { - const name = attributeName.replace(CAMELIZE, capitalize); - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions - properties[name] = new PropertyInfoRecord( - STRING, - attributeName, - 'http://www.w3.org/XML/1998/namespace', - false, // sanitizeURL - false, // removeEmptyString - ); -}); - -// These attribute exists both in HTML and SVG. -// The attribute name is case-sensitive in SVG so we can't just use -// the React name like we do for attributes that exist only in HTML. -['tabIndex', 'crossOrigin'].forEach(attributeName => { - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions - properties[attributeName] = new PropertyInfoRecord( - STRING, - attributeName.toLowerCase(), // attributeName - null, // attributeNamespace - false, // sanitizeURL - false, // removeEmptyString - ); -}); - -// These attributes accept URLs. These must not allow javascript: URLS. -// These will also need to accept Trusted Types object in the future. -const xlinkHref = 'xlinkHref'; -// $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions -properties[xlinkHref] = new PropertyInfoRecord( - STRING, - 'xlink:href', - 'http://www.w3.org/1999/xlink', - true, // sanitizeURL - false, // removeEmptyString -); - -const formAction = 'formAction'; -// $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions -properties[formAction] = new PropertyInfoRecord( - STRING, - 'formaction', // attributeName - null, // attributeNamespace - true, // sanitizeURL - false, // removeEmptyString -); - -['src', 'href', 'action'].forEach(attributeName => { - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions - properties[attributeName] = new PropertyInfoRecord( - STRING, - attributeName.toLowerCase(), // attributeName - null, // attributeNamespace - true, // sanitizeURL - true, // removeEmptyString - ); -}); diff --git a/packages/react-dom-bindings/src/shared/ReactDOMUnknownPropertyHook.js b/packages/react-dom-bindings/src/shared/ReactDOMUnknownPropertyHook.js index e941899ab4d76..d7d2eb3b77fdb 100644 --- a/packages/react-dom-bindings/src/shared/ReactDOMUnknownPropertyHook.js +++ b/packages/react-dom-bindings/src/shared/ReactDOMUnknownPropertyHook.js @@ -5,7 +5,6 @@ * LICENSE file in the root directory of this source tree. */ -import {BOOLEAN, getPropertyInfo} from './DOMProperty'; import {ATTRIBUTE_NAME_CHAR} from './isAttributeNameSafe'; import isCustomElement from './isCustomElement'; import possibleStandardNames from './possibleStandardNames'; @@ -131,8 +130,6 @@ function validateProperty(tagName, name, value, eventRegistry) { return true; } - const propertyInfo = getPropertyInfo(name); - // Known attributes should match the casing specified in the property config. if (possibleStandardNames.hasOwnProperty(lowerCasedName)) { const standardName = possibleStandardNames[lowerCasedName]; @@ -184,20 +181,49 @@ function validateProperty(tagName, name, value, eventRegistry) { switch (typeof value) { case 'boolean': { switch (name) { + case 'autoFocus': case 'checked': - case 'selected': case 'multiple': - case 'muted': { + case 'muted': + case 'selected': + case 'contentEditable': + case 'spellCheck': + case 'draggable': + case 'value': + case 'autoReverse': + case 'externalResourcesRequired': + case 'focusable': + case 'preserveAlpha': + 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': + case 'capture': + case 'download': { // Boolean properties can accept boolean values return true; } default: { - if (propertyInfo === null) { - const prefix = name.toLowerCase().slice(0, 5); - if (prefix === 'data-' || prefix === 'aria-') { - return true; - } - } else if (propertyInfo.acceptsBooleans) { + const prefix = name.toLowerCase().slice(0, 5); + if (prefix === 'data-' || prefix === 'aria-') { return true; } if (value) { @@ -244,13 +270,33 @@ function validateProperty(tagName, name, value, eventRegistry) { case 'checked': case 'selected': case 'multiple': - case 'muted': { + case 'muted': + 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': { break; } default: { - if (propertyInfo === null || propertyInfo.type !== BOOLEAN) { - return true; - } + return true; } } console.error( diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationUntrustedURL-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationUntrustedURL-test.js index c7da08897ae37..5bdfba529641b 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationUntrustedURL-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationUntrustedURL-test.js @@ -339,7 +339,7 @@ describe('ReactDOMServerIntegration - Untrusted URLs - disableJavaScriptURLs', ( // The hydration validation calls it one extra time. // TODO: It would be good if we only called toString once for // consistency but the code structure makes that hard right now. - expectedToStringCalls = 5; + expectedToStringCalls = 4; } else if (__DEV__) { // Checking for string coercion problems results in double the // toString calls in DEV