From a5a65426ca740bd0cb76993a01bf26d7e17161a2 Mon Sep 17 00:00:00 2001 From: Jan Kassens Date: Mon, 17 Apr 2023 16:24:36 -0400 Subject: [PATCH] Revert "Refactor DOM Bindings Completely Off of DOMProperty Meta Programming (#26546)" This reverts commit eeabb7312f509eb2094452a4389646000ea8ea14. --- .../src/client/CSSPropertyOperations.js | 15 - .../src/client/DOMPropertyOperations.js | 328 ++- .../src/client/ReactDOMComponent.js | 2248 ++--------------- .../src/server/ReactFizzConfigDOM.js | 633 +---- .../src/shared/DOMProperty.js | 411 +++ .../src/shared/ReactDOMUnknownPropertyHook.js | 76 +- ...ctDOMServerIntegrationUntrustedURL-test.js | 2 +- 7 files changed, 1058 insertions(+), 2655 deletions(-) create mode 100644 packages/react-dom-bindings/src/shared/DOMProperty.js diff --git a/packages/react-dom-bindings/src/client/CSSPropertyOperations.js b/packages/react-dom-bindings/src/client/CSSPropertyOperations.js index 9c07945aecff9..3f101ae8282c6 100644 --- a/packages/react-dom-bindings/src/client/CSSPropertyOperations.js +++ b/packages/react-dom-bindings/src/client/CSSPropertyOperations.js @@ -72,21 +72,6 @@ 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 4066055e0197f..d89f40037ef2f 100644 --- a/packages/react-dom-bindings/src/client/DOMPropertyOperations.js +++ b/packages/react-dom-bindings/src/client/DOMPropertyOperations.js @@ -7,14 +7,181 @@ * @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 @@ -104,6 +271,138 @@ 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, @@ -140,35 +439,6 @@ 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 60776996e537a..e587d84707189 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,20 +57,18 @@ 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, @@ -124,9 +122,6 @@ function warnForPropDifference( if (didWarnInvalidHydration) { return; } - if (serverValue === clientValue) { - return; - } const normalizedClientValue = normalizeMarkupForTextOrAttribute(clientValue); const normalizedServerValue = @@ -264,18 +259,31 @@ 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; } @@ -370,616 +378,6 @@ 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) { @@ -992,14 +390,33 @@ function setPropOnCustomElement( warnForInvalidEventListener(key, value); } } else { - if (enableCustomElementPropertySupport) { - setValueForPropertyOnCustomComponent(domElement, key, value); + 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); + } } else { - if (typeof value === 'boolean') { - // Special case before the new flag is on - value = '' + (value: any); + 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); } - setValueForAttribute(domElement, key, value); } } } @@ -1058,7 +475,7 @@ export function setInitialProperties( } // defaultChecked and defaultValue are ignored by setProp default: { - setProp(domElement, tag, propKey, propValue, props); + setProp(domElement, tag, propKey, propValue, false, props); } } } @@ -1088,7 +505,7 @@ export function setInitialProperties( } // defaultValue are ignored by setProp default: { - setProp(domElement, tag, propKey, propValue, props); + setProp(domElement, tag, propKey, propValue, false, props); } } } @@ -1128,7 +545,7 @@ export function setInitialProperties( } // defaultValue is ignored by setProp default: { - setProp(domElement, tag, propKey, propValue, props); + setProp(domElement, tag, propKey, propValue, false, props); } } } @@ -1158,7 +575,7 @@ export function setInitialProperties( break; } default: { - setProp(domElement, tag, propKey, propValue, props); + setProp(domElement, tag, propKey, propValue, false, props); } } } @@ -1240,7 +657,7 @@ export function setInitialProperties( } // defaultChecked and defaultValue are ignored by setProp default: { - setProp(domElement, tag, propKey, propValue, props); + setProp(domElement, tag, propKey, propValue, false, props); } } } @@ -1248,28 +665,16 @@ export function setInitialProperties( } } - 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); + const isCustomElementTag = isCustomElement(tag, props); + for (const propKey in props) { + if (!props.hasOwnProperty(propKey)) { + continue; } - } 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); + const propValue = props[propKey]; + if (propValue == null) { + continue; } + setProp(domElement, tag, propKey, propValue, isCustomElementTag, props); } } @@ -1432,7 +837,7 @@ export function updateProperties( } // defaultChecked and defaultValue are ignored by setProp default: { - setProp(domElement, tag, propKey, propValue, nextProps); + setProp(domElement, tag, propKey, propValue, false, nextProps); } } } @@ -1453,7 +858,7 @@ export function updateProperties( } // defaultValue are ignored by setProp default: { - setProp(domElement, tag, propKey, propValue, nextProps); + setProp(domElement, tag, propKey, propValue, false, nextProps); } } } @@ -1486,7 +891,7 @@ export function updateProperties( } // defaultValue is ignored by setProp default: { - setProp(domElement, tag, propKey, propValue, nextProps); + setProp(domElement, tag, propKey, propValue, false, nextProps); } } } @@ -1507,7 +912,7 @@ export function updateProperties( break; } default: { - setProp(domElement, tag, propKey, propValue, nextProps); + setProp(domElement, tag, propKey, propValue, false, nextProps); } } } @@ -1546,7 +951,7 @@ export function updateProperties( } // defaultChecked and defaultValue are ignored by setProp default: { - setProp(domElement, tag, propKey, propValue, nextProps); + setProp(domElement, tag, propKey, propValue, false, nextProps); } } } @@ -1554,19 +959,12 @@ export function updateProperties( } } + const isCustomElementTag = isCustomElement(tag, nextProps); // Apply the diff. - 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); - } + 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); } } @@ -1592,314 +990,10 @@ function diffHydratedStyles(domElement: Element, value: mixed) { if (canDiffStyleForHydrationWarning) { const expectedStyle = createDangerousStringForStyles(value); const serverValue = domElement.getAttribute('style'); - 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; - } - } - } + if (expectedStyle !== serverValue) { + warnForPropDifference('style', serverValue, expectedStyle); } } - warnForPropDifference(propKey, serverValue, value); } function diffHydratedCustomComponent( @@ -1907,19 +1001,19 @@ function diffHydratedCustomComponent( tag: string, props: Object, parentNamespaceDev: string, - extraAttributes: Set, + extraAttributeNames: Set, ) { for (const propKey in props) { if (!props.hasOwnProperty(propKey)) { continue; } - const value = props[propKey]; - if (value == null) { + const nextProp = props[propKey]; + if (nextProp == null) { continue; } if (registrationNameDependencies.hasOwnProperty(propKey)) { - if (typeof value !== 'function') { - warnForInvalidEventListener(propKey, value); + if (typeof nextProp !== 'function') { + warnForInvalidEventListener(propKey, nextProp); } continue; } @@ -1939,15 +1033,17 @@ function diffHydratedCustomComponent( continue; case 'dangerouslySetInnerHTML': const serverHTML = domElement.innerHTML; - const nextHtml = value ? value.__html : undefined; + const nextHtml = nextProp ? nextProp.__html : undefined; if (nextHtml != null) { const expectedHTML = normalizeHTML(domElement, nextHtml); - warnForPropDifference(propKey, serverHTML, expectedHTML); + if (expectedHTML !== serverHTML) { + warnForPropDifference(propKey, serverHTML, expectedHTML); + } } continue; case 'style': - extraAttributes.delete(propKey); - diffHydratedStyles(domElement, value); + extraAttributeNames.delete(propKey); + diffHydratedStyles(domElement, nextProp); continue; case 'offsetParent': case 'offsetTop': @@ -1958,7 +1054,7 @@ function diffHydratedCustomComponent( case 'outerText': case 'outerHTML': if (enableCustomElementPropertySupport) { - extraAttributes.delete(propKey.toLowerCase()); + extraAttributeNames.delete(propKey.toLowerCase()); if (__DEV__) { console.error( 'Assignment to read-only property will result in a no-op: `%s`', @@ -1971,13 +1067,15 @@ function diffHydratedCustomComponent( case 'className': if (enableCustomElementPropertySupport) { // className is a special cased property on the server to render as an attribute. - extraAttributes.delete('class'); + extraAttributeNames.delete('class'); const serverValue = getValueForAttributeOnCustomComponent( domElement, 'class', - value, + nextProp, ); - warnForPropDifference('className', serverValue, value); + if (nextProp !== serverValue) { + warnForPropDifference('className', serverValue, nextProp); + } continue; } // eslint-disable-next-line no-fallthrough @@ -1987,16 +1085,18 @@ function diffHydratedCustomComponent( ownNamespaceDev = getIntrinsicNamespace(tag); } if (ownNamespaceDev === HTML_NAMESPACE) { - extraAttributes.delete(propKey.toLowerCase()); + extraAttributeNames.delete(propKey.toLowerCase()); } else { - extraAttributes.delete(propKey); + extraAttributeNames.delete(propKey); } const serverValue = getValueForAttributeOnCustomComponent( domElement, propKey, - value, + nextProp, ); - warnForPropDifference(propKey, serverValue, value); + if (nextProp !== serverValue) { + warnForPropDifference(propKey, serverValue, nextProp); + } } } } @@ -2007,19 +1107,19 @@ function diffHydratedGenericElement( tag: string, props: Object, parentNamespaceDev: string, - extraAttributes: Set, + extraAttributeNames: Set, ) { for (const propKey in props) { if (!props.hasOwnProperty(propKey)) { continue; } - const value = props[propKey]; - if (value == null) { + const nextProp = props[propKey]; + if (nextProp == null) { continue; } if (registrationNameDependencies.hasOwnProperty(propKey)) { - if (typeof value !== 'function') { - warnForInvalidEventListener(propKey, value); + if (typeof nextProp !== 'function') { + warnForInvalidEventListener(propKey, nextProp); } continue; } @@ -2042,1069 +1142,116 @@ function diffHydratedGenericElement( continue; case 'dangerouslySetInnerHTML': const serverHTML = domElement.innerHTML; - const nextHtml = value ? value.__html : undefined; + const nextHtml = nextProp ? nextProp.__html : undefined; if (nextHtml != null) { const expectedHTML = normalizeHTML(domElement, nextHtml); - warnForPropDifference(propKey, serverHTML, expectedHTML); + if (expectedHTML !== serverHTML) { + warnForPropDifference(propKey, serverHTML, expectedHTML); + } } continue; case 'style': - extraAttributes.delete(propKey); - diffHydratedStyles(domElement, value); + extraAttributeNames.delete(propKey); + diffHydratedStyles(domElement, nextProp); continue; case 'multiple': { - extraAttributes.delete(propKey); + extraAttributeNames.delete(propKey); const serverValue = (domElement: any).multiple; - warnForPropDifference(propKey, serverValue, value); + if (nextProp !== serverValue) { + warnForPropDifference('multiple', serverValue, nextProp); + } continue; } case 'muted': { - extraAttributes.delete(propKey); + extraAttributeNames.delete(propKey); const serverValue = (domElement: any).muted; - warnForPropDifference(propKey, serverValue, value); - continue; - } - case 'autoFocus': { - extraAttributes.delete('autofocus'); - const serverValue = (domElement: any).autofocus; - warnForPropDifference(propKey, serverValue, value); + if (nextProp !== serverValue) { + warnForPropDifference('muted', serverValue, nextProp); + } 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, - ); - } + 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); } - hydrateSanitizedAttribute( - domElement, - propKey, - propKey, - null, - extraAttributes, - ); - continue; + extraAttributeNames.delete(propKey); } + serverValue = getValueForAttribute(domElement, propKey, nextProp); } - 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; - } - 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); + + 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); } break; case 'source': @@ -3199,7 +1346,7 @@ export function diffHydratedProperties( } if (__DEV__ && shouldWarnDev) { - const extraAttributes: Set = new Set(); + const extraAttributeNames: Set = new Set(); const attributes = domElement.attributes; for (let i = 0; i < attributes.length; i++) { const name = attributes[i].name.toLowerCase(); @@ -3215,7 +1362,7 @@ export function diffHydratedProperties( default: // Intentionally use the original name. // See discussion in https://github.com/facebook/react/pull/10676. - extraAttributes.add(attributes[i].name); + extraAttributeNames.add(attributes[i].name); } } if (isCustomElement(tag, props)) { @@ -3224,7 +1371,7 @@ export function diffHydratedProperties( tag, props, parentNamespaceDev, - extraAttributes, + extraAttributeNames, ); } else { diffHydratedGenericElement( @@ -3232,11 +1379,14 @@ export function diffHydratedProperties( tag, props, parentNamespaceDev, - extraAttributes, + extraAttributeNames, ); } - if (extraAttributes.size > 0 && props.suppressHydrationWarning !== true) { - warnForExtraAttributes(extraAttributes); + if ( + extraAttributeNames.size > 0 && + props.suppressHydrationWarning !== true + ) { + warnForExtraAttributes(extraAttributeNames); } } diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js index 601706df50157..60d53197bd44a 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -39,6 +39,13 @@ 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'; @@ -614,26 +621,6 @@ 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, @@ -651,505 +638,151 @@ function pushAttribute( case 'suppressHydrationWarning': // Ignored. These are built-in to React on the client. return; - case 'autoFocus': case 'multiple': - case 'muted': { - pushBooleanAttribute(target, name.toLowerCase(), value); + case 'muted': + pushBooleanAttribute(target, name, value); return; - } - 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, - ); - } - } + } + 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; } } - // 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; } - case 'xlinkHref': { - if ( - typeof value === 'function' || - typeof value === 'symbol' || - typeof value === 'boolean' - ) { + 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, + ); + } + } return; } - 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, - 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) - ) { + + 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, + ); + } + 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)); + } target.push( attributeSeparator, - stringToChunk(name), + attributeNameChunk, attributeAssign, stringToChunk(escapeTextForBrowser(value)), attributeEnd, ); - } - return; } - // 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') - ) { + } else if (isAttributeNameSafe(name)) { + // shouldRemoveAttribute + switch (typeof value) { + case 'function': + case 'symbol': // eslint-disable-line 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; - } - } + 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 new file mode 100644 index 0000000000000..8bd96110f3a50 --- /dev/null +++ b/packages/react-dom-bindings/src/shared/DOMProperty.js @@ -0,0 +1,411 @@ +/** + * 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 d7d2eb3b77fdb..e941899ab4d76 100644 --- a/packages/react-dom-bindings/src/shared/ReactDOMUnknownPropertyHook.js +++ b/packages/react-dom-bindings/src/shared/ReactDOMUnknownPropertyHook.js @@ -5,6 +5,7 @@ * 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'; @@ -130,6 +131,8 @@ 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]; @@ -181,49 +184,20 @@ function validateProperty(tagName, name, value, eventRegistry) { switch (typeof value) { case 'boolean': { switch (name) { - case 'autoFocus': case 'checked': - case 'multiple': - 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': { + case 'multiple': + case 'muted': { // Boolean properties can accept boolean values return true; } default: { - const prefix = name.toLowerCase().slice(0, 5); - if (prefix === 'data-' || prefix === 'aria-') { + if (propertyInfo === null) { + const prefix = name.toLowerCase().slice(0, 5); + if (prefix === 'data-' || prefix === 'aria-') { + return true; + } + } else if (propertyInfo.acceptsBooleans) { return true; } if (value) { @@ -270,33 +244,13 @@ function validateProperty(tagName, name, value, eventRegistry) { case 'checked': case 'selected': case 'multiple': - 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': { + case 'muted': { break; } default: { - return true; + if (propertyInfo === null || propertyInfo.type !== BOOLEAN) { + 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 5bdfba529641b..c7da08897ae37 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 = 4; + expectedToStringCalls = 5; } else if (__DEV__) { // Checking for string coercion problems results in double the // toString calls in DEV