From eeabb7312f509eb2094452a4389646000ea8ea14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Tue, 4 Apr 2023 11:05:56 -0400 Subject: [PATCH] Refactor DOM Bindings Completely Off of DOMProperty Meta Programming (#26546) There are four places we have special cases based off the DOMProperty config: 1) DEV-only: ReactDOMUnknownPropertyHook warns for passing booleans to non-boolean attributes. We just need a simple list of all properties that are affected by that. We could probably move this in under setProp instead and have it covered by that list. 2) DEV-only: Hydration. This just needs to read the value from an attribute and compare it to what we'd expect to see if it was rendered on the client. This could use some simplification/unification of the code but I decided to just keep it simple and duplicated since code size isn't an issue. 3) DOMServerFormatConfig pushAttribute: This just maps the special case to how to emit it as a HTML attribute. 4) ReactDOMComponent setProp: This just maps the special case to how to emit it as setAttribute or removeAttribute. Basically we just have to remember to keep pushAttribute and setProp aligned. There's only one long switch in prod per environment. This just turns it all to a giant simple switch statement with string cases. This is in theory the most optimizable since syntactically all the information for a hash table is there. However, unfortunately we know that most VMs don't optimize this very well and instead just turn them into a bunch of ifs. JSC is best. We can minimize the cost by just moving common attribute to the beginning of the list. If we shipped this, maybe VMs will get it together to start optimizing this case but there's a chicken and egg problem here and the game theory reality is that we probably don't want to regress. Therefore, I intend to do a follow up after landing this which reintroduces an object indirection for simple property aliases. That should be enough to make the remaining cases palatable. I'll also extract the most common attributes to the beginning or separate ifs. Ran attribute-behavior fixture and the table is the same. --- .../src/client/CSSPropertyOperations.js | 15 + .../src/client/DOMPropertyOperations.js | 328 +-- .../src/client/ReactDOMComponent.js | 2156 +++++++++++++++-- .../src/server/ReactDOMServerFormatConfig.js | 637 +++-- .../src/shared/DOMProperty.js | 411 ---- .../src/shared/ReactDOMUnknownPropertyHook.js | 76 +- ...ctDOMServerIntegrationUntrustedURL-test.js | 2 +- 7 files changed, 2611 insertions(+), 1014 deletions(-) delete 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 3f101ae828..9c07945aec 100644 --- a/packages/react-dom-bindings/src/client/CSSPropertyOperations.js +++ b/packages/react-dom-bindings/src/client/CSSPropertyOperations.js @@ -72,6 +72,21 @@ export function createDangerousStringForStyles(styles) { * @param {object} styles */ export function setValueForStyles(node, styles) { + if (styles != null && typeof styles !== 'object') { + throw new Error( + 'The `style` prop expects a mapping from style properties to values, ' + + "not a string. For example, style={{marginRight: spacing + 'em'}} when " + + 'using JSX.', + ); + } + if (__DEV__) { + if (styles) { + // Freeze the next style object so that we can assume it won't be + // mutated. We have already warned for this in the past. + Object.freeze(styles); + } + } + const style = node.style; for (const styleName in styles) { if (!styles.hasOwnProperty(styleName)) { diff --git a/packages/react-dom-bindings/src/client/DOMPropertyOperations.js b/packages/react-dom-bindings/src/client/DOMPropertyOperations.js index d89f40037e..4066055e01 100644 --- a/packages/react-dom-bindings/src/client/DOMPropertyOperations.js +++ b/packages/react-dom-bindings/src/client/DOMPropertyOperations.js @@ -7,181 +7,14 @@ * @flow */ -import { - BOOLEAN, - OVERLOADED_BOOLEAN, - NUMERIC, - POSITIVE_NUMERIC, -} from '../shared/DOMProperty'; - import isAttributeNameSafe from '../shared/isAttributeNameSafe'; -import sanitizeURL from '../shared/sanitizeURL'; import { enableTrustedTypesIntegration, enableCustomElementPropertySupport, - enableFilterEmptyStringAttributesDOM, } from 'shared/ReactFeatureFlags'; import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion'; import {getFiberCurrentPropsFromNode} from './ReactDOMComponentTree'; -import type {PropertyInfo} from '../shared/DOMProperty'; - -/** - * Get the value for a property on a node. Only used in DEV for SSR validation. - * The "expected" argument is used as a hint of what the expected value is. - * Some properties have multiple equivalent values. - */ -export function getValueForProperty( - node: Element, - name: string, - expected: mixed, - propertyInfo: PropertyInfo, -): mixed { - if (__DEV__) { - const attributeName = propertyInfo.attributeName; - - if (!node.hasAttribute(attributeName)) { - // shouldRemoveAttribute - switch (typeof expected) { - case 'function': - case 'symbol': // eslint-disable-line - return expected; - case 'boolean': { - if (!propertyInfo.acceptsBooleans) { - return expected; - } - } - } - switch (propertyInfo.type) { - case BOOLEAN: { - if (!expected) { - return expected; - } - break; - } - case OVERLOADED_BOOLEAN: { - if (expected === false) { - return expected; - } - break; - } - case NUMERIC: { - if (isNaN(expected)) { - return expected; - } - break; - } - case POSITIVE_NUMERIC: { - if (isNaN(expected) || (expected: any) < 1) { - return expected; - } - break; - } - } - if (enableFilterEmptyStringAttributesDOM) { - if (propertyInfo.removeEmptyString && expected === '') { - if (__DEV__) { - if (name === 'src') { - console.error( - 'An empty string ("") was passed to the %s attribute. ' + - 'This may cause the browser to download the whole page again over the network. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to %s instead of an empty string.', - name, - name, - ); - } else { - console.error( - 'An empty string ("") was passed to the %s attribute. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to %s instead of an empty string.', - name, - name, - ); - } - } - return expected; - } - } - return expected === undefined ? undefined : null; - } - - // Even if this property uses a namespace we use getAttribute - // because we assume its namespaced name is the same as our config. - // To use getAttributeNS we need the local name which we don't have - // in our config atm. - const value = node.getAttribute(attributeName); - - if (expected == null) { - // We had an attribute but shouldn't have had one, so read it - // for the error message. - return value; - } - - // shouldRemoveAttribute - switch (typeof expected) { - case 'function': - case 'symbol': // eslint-disable-line - return value; - } - switch (propertyInfo.type) { - case BOOLEAN: { - if (expected) { - // If this was a boolean, it doesn't matter what the value is - // the fact that we have it is the same as the expected. - // As long as it's positive. - return expected; - } - return value; - } - case OVERLOADED_BOOLEAN: { - if (value === '') { - return true; - } - if (expected === false) { - // We had an attribute but shouldn't have had one, so read it - // for the error message. - return value; - } - break; - } - case NUMERIC: { - if (isNaN(expected)) { - // We had an attribute but shouldn't have had one, so read it - // for the error message. - return value; - } - break; - } - case POSITIVE_NUMERIC: { - if (isNaN(expected) || (expected: any) < 1) { - // We had an attribute but shouldn't have had one, so read it - // for the error message. - return value; - } - break; - } - } - if (__DEV__) { - checkAttributeStringCoercion(expected, name); - } - if (propertyInfo.sanitizeURL) { - // We have already verified this above. - // eslint-disable-next-line react-internal/safe-string-coercion - if (value === '' + (sanitizeURL(expected): any)) { - return expected; - } - return value; - } - // We have already verified this above. - // eslint-disable-next-line react-internal/safe-string-coercion - if (value === '' + (expected: any)) { - return expected; - } - return value; - } -} - /** * Get the value for a attribute on a node. Only used in DEV for SSR validation. * The third argument is used as a hint of what the expected value is. Some @@ -271,138 +104,6 @@ export function getValueForAttributeOnCustomComponent( } } -/** - * Sets the value for a property on a node. - * - * @param {DOMElement} node - * @param {string} name - * @param {*} value - */ -export function setValueForProperty( - node: Element, - propertyInfo: PropertyInfo, - value: mixed, -) { - const attributeName = propertyInfo.attributeName; - - if (value === null) { - node.removeAttribute(attributeName); - return; - } - - // shouldRemoveAttribute - switch (typeof value) { - case 'undefined': - case 'function': - case 'symbol': // eslint-disable-line - node.removeAttribute(attributeName); - return; - case 'boolean': { - if (!propertyInfo.acceptsBooleans) { - node.removeAttribute(attributeName); - return; - } - } - } - if (enableFilterEmptyStringAttributesDOM) { - if (propertyInfo.removeEmptyString && value === '') { - if (__DEV__) { - if (attributeName === 'src') { - console.error( - 'An empty string ("") was passed to the %s attribute. ' + - 'This may cause the browser to download the whole page again over the network. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to %s instead of an empty string.', - attributeName, - attributeName, - ); - } else { - console.error( - 'An empty string ("") was passed to the %s attribute. ' + - 'To fix this, either do not render the element at all ' + - 'or pass null to %s instead of an empty string.', - attributeName, - attributeName, - ); - } - } - node.removeAttribute(attributeName); - return; - } - } - - switch (propertyInfo.type) { - case BOOLEAN: - if (value) { - node.setAttribute(attributeName, ''); - } else { - node.removeAttribute(attributeName); - return; - } - break; - case OVERLOADED_BOOLEAN: - if (value === true) { - node.setAttribute(attributeName, ''); - } else if (value === false) { - node.removeAttribute(attributeName); - } else { - if (__DEV__) { - checkAttributeStringCoercion(value, attributeName); - } - node.setAttribute(attributeName, (value: any)); - } - return; - case NUMERIC: - if (!isNaN(value)) { - if (__DEV__) { - checkAttributeStringCoercion(value, attributeName); - } - node.setAttribute(attributeName, (value: any)); - } else { - node.removeAttribute(attributeName); - } - break; - case POSITIVE_NUMERIC: - if (!isNaN(value) && (value: any) >= 1) { - if (__DEV__) { - checkAttributeStringCoercion(value, attributeName); - } - node.setAttribute(attributeName, (value: any)); - } else { - node.removeAttribute(attributeName); - } - break; - default: { - if (__DEV__) { - checkAttributeStringCoercion(value, attributeName); - } - let attributeValue; - // `setAttribute` with objects becomes only `[object]` in IE8/9, - // ('' + value) makes it output the correct toString()-value. - if (enableTrustedTypesIntegration) { - if (propertyInfo.sanitizeURL) { - attributeValue = (sanitizeURL(value): any); - } else { - attributeValue = (value: any); - } - } else { - // We have already verified this above. - // eslint-disable-next-line react-internal/safe-string-coercion - attributeValue = '' + (value: any); - if (propertyInfo.sanitizeURL) { - attributeValue = sanitizeURL(attributeValue); - } - } - const attributeNamespace = propertyInfo.attributeNamespace; - if (attributeNamespace) { - node.setAttributeNS(attributeNamespace, attributeName, attributeValue); - } else { - node.setAttribute(attributeName, attributeValue); - } - } - } -} - export function setValueForAttribute( node: Element, name: string, @@ -439,6 +140,35 @@ export function setValueForAttribute( } } +export function setValueForNamespacedAttribute( + node: Element, + namespace: string, + name: string, + value: mixed, +) { + if (value === null) { + node.removeAttribute(name); + return; + } + switch (typeof value) { + case 'undefined': + case 'function': + case 'symbol': + case 'boolean': { + node.removeAttribute(name); + return; + } + } + if (__DEV__) { + checkAttributeStringCoercion(value, name); + } + node.setAttributeNS( + namespace, + name, + enableTrustedTypesIntegration ? (value: any) : '' + (value: any), + ); +} + export function setValueForPropertyOnCustomComponent( node: Element, name: string, diff --git a/packages/react-dom-bindings/src/client/ReactDOMComponent.js b/packages/react-dom-bindings/src/client/ReactDOMComponent.js index e587d84707..60776996e5 100644 --- a/packages/react-dom-bindings/src/client/ReactDOMComponent.js +++ b/packages/react-dom-bindings/src/client/ReactDOMComponent.js @@ -16,14 +16,14 @@ import { import {canUseDOM} from 'shared/ExecutionEnvironment'; import {checkHtmlStringCoercion} from 'shared/CheckStringCoercion'; +import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion'; import { getValueForAttribute, getValueForAttributeOnCustomComponent, - getValueForProperty, - setValueForProperty, setValueForPropertyOnCustomComponent, setValueForAttribute, + setValueForNamespacedAttribute, } from './DOMPropertyOperations'; import { initWrapperState as ReactDOMInputInitWrapperState, @@ -57,18 +57,20 @@ import { validateShorthandPropertyCollisionInDev, } from './CSSPropertyOperations'; import {HTML_NAMESPACE, getIntrinsicNamespace} from './DOMNamespaces'; -import {getPropertyInfo} from '../shared/DOMProperty'; import isCustomElement from '../shared/isCustomElement'; import possibleStandardNames from '../shared/possibleStandardNames'; import {validateProperties as validateARIAProperties} from '../shared/ReactDOMInvalidARIAHook'; import {validateProperties as validateInputProperties} from '../shared/ReactDOMNullInputValuePropHook'; import {validateProperties as validateUnknownProperties} from '../shared/ReactDOMUnknownPropertyHook'; +import sanitizeURL from '../shared/sanitizeURL'; import { enableCustomElementPropertySupport, enableClientRenderFallbackOnTextMismatch, enableHostSingletons, disableIEWorkarounds, + enableTrustedTypesIntegration, + enableFilterEmptyStringAttributesDOM, } from 'shared/ReactFeatureFlags'; import { mediaEventTypes, @@ -122,6 +124,9 @@ function warnForPropDifference( if (didWarnInvalidHydration) { return; } + if (serverValue === clientValue) { + return; + } const normalizedClientValue = normalizeMarkupForTextOrAttribute(clientValue); const normalizedServerValue = @@ -259,31 +264,18 @@ export function trapClickOnNonInteractiveElement(node: HTMLElement) { node.onclick = noop; } +const xlinkNamespace = 'http://www.w3.org/1999/xlink'; +const xmlNamespace = 'http://www.w3.org/XML/1998/namespace'; + function setProp( domElement: Element, tag: string, key: string, value: mixed, - isCustomElementTag: boolean, props: any, ): void { switch (key) { case 'style': { - if (value != null && typeof value !== 'object') { - throw new Error( - 'The `style` prop expects a mapping from style properties to values, ' + - "not a string. For example, style={{marginRight: spacing + 'em'}} when " + - 'using JSX.', - ); - } - if (__DEV__) { - if (value) { - // Freeze the next style object so that we can assume it won't be - // mutated. We have already warned for this in the past. - Object.freeze(value); - } - } - // Relies on `updateStylesByID` not mutating `styleUpdates`. setValueForStyles(domElement, value); break; } @@ -378,6 +370,616 @@ function setProp( // on server rendering (but we *do* want to emit it in SSR). break; } + // These attributes accept URLs. These must not allow javascript: URLS. + case 'src': + case 'href': + case 'action': + if (enableFilterEmptyStringAttributesDOM) { + if (value === '') { + if (__DEV__) { + if (key === 'src') { + console.error( + 'An empty string ("") was passed to the %s attribute. ' + + 'This may cause the browser to download the whole page again over the network. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to %s instead of an empty string.', + key, + key, + ); + } else { + console.error( + 'An empty string ("") was passed to the %s attribute. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to %s instead of an empty string.', + key, + key, + ); + } + } + domElement.removeAttribute(key); + break; + } + } + // Fall through to the last case which shouldn't remove empty strings. + // eslint-disable-next-line no-fallthrough + case 'formAction': { + if ( + value == null || + typeof value === 'function' || + typeof value === 'symbol' || + typeof value === 'boolean' + ) { + domElement.removeAttribute(key); + break; + } + // `setAttribute` with objects becomes only `[object]` in IE8/9, + // ('' + value) makes it output the correct toString()-value. + if (__DEV__) { + checkAttributeStringCoercion(value, key); + } + const sanitizedValue = (sanitizeURL( + enableTrustedTypesIntegration ? value : '' + (value: any), + ): any); + domElement.setAttribute(key, sanitizedValue); + break; + } + case 'xlinkHref': { + if ( + value == null || + typeof value === 'function' || + typeof value === 'boolean' || + typeof value === 'symbol' + ) { + domElement.removeAttribute('xlink:href'); + break; + } + // `setAttribute` with objects becomes only `[object]` in IE8/9, + // ('' + value) makes it output the correct toString()-value. + if (__DEV__) { + checkAttributeStringCoercion(value, key); + } + const sanitizedValue = (sanitizeURL( + enableTrustedTypesIntegration ? value : '' + (value: any), + ): any); + domElement.setAttributeNS(xlinkNamespace, 'xlink:href', sanitizedValue); + break; + } + case 'contentEditable': + case 'spellCheck': + case 'draggable': + case 'value': + case 'autoReverse': + case 'externalResourcesRequired': + case 'focusable': + case 'preserveAlpha': { + // Booleanish String + // These are "enumerated" attributes that accept "true" and "false". + // In React, we let users pass `true` and `false` even though technically + // these aren't boolean attributes (they are coerced to strings). + // The SVG attributes are case-sensitive. Since the HTML attributes are + // insensitive they also work even though we canonically use lower case. + if ( + value != null && + typeof value !== 'function' && + typeof value !== 'symbol' + ) { + if (__DEV__) { + checkAttributeStringCoercion(value, key); + } + domElement.setAttribute(key, (value: any)); + } else { + domElement.removeAttribute(key); + } + break; + } + // Boolean + case 'allowFullScreen': + case 'async': + case 'autoPlay': + case 'controls': + case 'default': + case 'defer': + case 'disabled': + case 'disablePictureInPicture': + case 'disableRemotePlayback': + case 'formNoValidate': + case 'hidden': + case 'loop': + case 'noModule': + case 'noValidate': + case 'open': + case 'playsInline': + case 'readOnly': + case 'required': + case 'reversed': + case 'scoped': + case 'seamless': + case 'itemScope': { + if (value && typeof value !== 'function' && typeof value !== 'symbol') { + domElement.setAttribute(key, ''); + } else { + domElement.removeAttribute(key); + } + break; + } + // Overloaded Boolean + case 'capture': + case 'download': { + // An attribute that can be used as a flag as well as with a value. + // When true, it should be present (set either to an empty string or its name). + // When false, it should be omitted. + // For any other value, should be present with that value. + if (value === true) { + domElement.setAttribute(key, ''); + } else if ( + value !== false && + value != null && + typeof value !== 'function' && + typeof value !== 'symbol' + ) { + if (__DEV__) { + checkAttributeStringCoercion(value, key); + } + domElement.setAttribute(key, (value: any)); + } else { + domElement.removeAttribute(key); + } + break; + } + case 'cols': + case 'rows': + case 'size': + case 'span': { + // These are HTML attributes that must be positive numbers. + if ( + value != null && + typeof value !== 'function' && + typeof value !== 'symbol' && + !isNaN(value) && + (value: any) >= 1 + ) { + if (__DEV__) { + checkAttributeStringCoercion(value, key); + } + domElement.setAttribute(key, (value: any)); + } else { + domElement.removeAttribute(key); + } + break; + } + case 'rowSpan': + case 'start': { + // These are HTML attributes that must be numbers. + if ( + value != null && + typeof value !== 'function' && + typeof value !== 'symbol' && + !isNaN(value) + ) { + if (__DEV__) { + checkAttributeStringCoercion(value, key); + } + domElement.setAttribute(key, (value: any)); + } else { + domElement.removeAttribute(key); + } + break; + } + // A few React string attributes have a different name. + // This is a mapping from React prop names to the attribute names. + case 'acceptCharset': + setValueForAttribute(domElement, 'accept-charset', value); + break; + case 'className': + setValueForAttribute(domElement, 'class', value); + break; + case 'htmlFor': + setValueForAttribute(domElement, 'for', value); + break; + case 'httpEquiv': + setValueForAttribute(domElement, 'http-equiv', value); + break; + // HTML and SVG attributes, but the SVG attribute is case sensitive. + case 'tabIndex': + setValueForAttribute(domElement, 'tabindex', value); + break; + case 'crossOrigin': + setValueForAttribute(domElement, 'crossorigin', value); + break; + // This is a list of all SVG attributes that need special casing. + // Regular attributes that just accept strings. + case 'accentHeight': + setValueForAttribute(domElement, 'accent-height', value); + break; + case 'alignmentBaseline': + setValueForAttribute(domElement, 'alignment-baseline', value); + break; + case 'arabicForm': + setValueForAttribute(domElement, 'arabic-form', value); + break; + case 'baselineShift': + setValueForAttribute(domElement, 'baseline-shift', value); + break; + case 'capHeight': + setValueForAttribute(domElement, 'cap-height', value); + break; + case 'clipPath': + setValueForAttribute(domElement, 'clip-path', value); + break; + case 'clipRule': + setValueForAttribute(domElement, 'clip-rule', value); + break; + case 'colorInterpolation': + setValueForAttribute(domElement, 'color-interpolation', value); + break; + case 'colorInterpolationFilters': + setValueForAttribute(domElement, 'color-interpolation-filters', value); + break; + case 'colorProfile': + setValueForAttribute(domElement, 'color-profile', value); + break; + case 'colorRendering': + setValueForAttribute(domElement, 'color-rendering', value); + break; + case 'dominantBaseline': + setValueForAttribute(domElement, 'dominant-baseline', value); + break; + case 'enableBackground': + setValueForAttribute(domElement, 'enable-background', value); + break; + case 'fillOpacity': + setValueForAttribute(domElement, 'fill-opacity', value); + break; + case 'fillRule': + setValueForAttribute(domElement, 'fill-rule', value); + break; + case 'floodColor': + setValueForAttribute(domElement, 'flood-color', value); + break; + case 'floodOpacity': + setValueForAttribute(domElement, 'flood-opacity', value); + break; + case 'fontFamily': + setValueForAttribute(domElement, 'font-family', value); + break; + case 'fontSize': + setValueForAttribute(domElement, 'font-size', value); + break; + case 'fontSizeAdjust': + setValueForAttribute(domElement, 'font-size-adjust', value); + break; + case 'fontStretch': + setValueForAttribute(domElement, 'font-stretch', value); + break; + case 'fontStyle': + setValueForAttribute(domElement, 'font-style', value); + break; + case 'fontVariant': + setValueForAttribute(domElement, 'font-variant', value); + break; + case 'fontWeight': + setValueForAttribute(domElement, 'font-weight', value); + break; + case 'glyphName': + setValueForAttribute(domElement, 'glyph-name', value); + break; + case 'glyphOrientationHorizontal': + setValueForAttribute(domElement, 'glyph-orientation-horizontal', value); + break; + case 'glyphOrientationVertical': + setValueForAttribute(domElement, 'glyph-orientation-vertical', value); + break; + case 'horizAdvX': + setValueForAttribute(domElement, 'horiz-adv-x', value); + break; + case 'horizOriginX': + setValueForAttribute(domElement, 'horiz-origin-x', value); + break; + case 'imageRendering': + setValueForAttribute(domElement, 'image-rendering', value); + break; + case 'letterSpacing': + setValueForAttribute(domElement, 'letter-spacing', value); + break; + case 'lightingColor': + setValueForAttribute(domElement, 'lighting-color', value); + break; + case 'markerEnd': + setValueForAttribute(domElement, 'marker-end', value); + break; + case 'markerMid': + setValueForAttribute(domElement, 'marker-mid', value); + break; + case 'markerStart': + setValueForAttribute(domElement, 'marker-start', value); + break; + case 'overlinePosition': + setValueForAttribute(domElement, 'overline-position', value); + break; + case 'overlineThickness': + setValueForAttribute(domElement, 'overline-thickness', value); + break; + case 'paintOrder': + setValueForAttribute(domElement, 'paint-order', value); + break; + case 'panose-1': + setValueForAttribute(domElement, 'panose-1', value); + break; + case 'pointerEvents': + setValueForAttribute(domElement, 'pointer-events', value); + break; + case 'renderingIntent': + setValueForAttribute(domElement, 'rendering-intent', value); + break; + case 'shapeRendering': + setValueForAttribute(domElement, 'shape-rendering', value); + break; + case 'stopColor': + setValueForAttribute(domElement, 'stop-color', value); + break; + case 'stopOpacity': + setValueForAttribute(domElement, 'stop-opacity', value); + break; + case 'strikethroughPosition': + setValueForAttribute(domElement, 'strikethrough-position', value); + break; + case 'strikethroughThickness': + setValueForAttribute(domElement, 'strikethrough-thickness', value); + break; + case 'strokeDasharray': + setValueForAttribute(domElement, 'stroke-dasharray', value); + break; + case 'strokeDashoffset': + setValueForAttribute(domElement, 'stroke-dashoffset', value); + break; + case 'strokeLinecap': + setValueForAttribute(domElement, 'stroke-linecap', value); + break; + case 'strokeLinejoin': + setValueForAttribute(domElement, 'stroke-linejoin', value); + break; + case 'strokeMiterlimit': + setValueForAttribute(domElement, 'stroke-miterlimit', value); + break; + case 'strokeOpacity': + setValueForAttribute(domElement, 'stroke-opacity', value); + break; + case 'strokeWidth': + setValueForAttribute(domElement, 'stroke-width', value); + break; + case 'textAnchor': + setValueForAttribute(domElement, 'text-anchor', value); + break; + case 'textDecoration': + setValueForAttribute(domElement, 'text-decoration', value); + break; + case 'textRendering': + setValueForAttribute(domElement, 'text-rendering', value); + break; + case 'transformOrigin': + setValueForAttribute(domElement, 'transform-origin', value); + break; + case 'underlinePosition': + setValueForAttribute(domElement, 'underline-position', value); + break; + case 'underlineThickness': + setValueForAttribute(domElement, 'underline-thickness', value); + break; + case 'unicodeBidi': + setValueForAttribute(domElement, 'unicode-bidi', value); + break; + case 'unicodeRange': + setValueForAttribute(domElement, 'unicode-range', value); + break; + case 'unitsPerEm': + setValueForAttribute(domElement, 'units-per-em', value); + break; + case 'vAlphabetic': + setValueForAttribute(domElement, 'v-alphabetic', value); + break; + case 'vHanging': + setValueForAttribute(domElement, 'v-hanging', value); + break; + case 'vIdeographic': + setValueForAttribute(domElement, 'v-ideographic', value); + break; + case 'vMathematical': + setValueForAttribute(domElement, 'v-mathematical', value); + break; + case 'vectorEffect': + setValueForAttribute(domElement, 'vector-effect', value); + break; + case 'vertAdvY': + setValueForAttribute(domElement, 'vert-adv-y', value); + break; + case 'vertOriginX': + setValueForAttribute(domElement, 'vert-origin-x', value); + break; + case 'vertOriginY': + setValueForAttribute(domElement, 'vert-origin-y', value); + break; + case 'wordSpacing': + setValueForAttribute(domElement, 'word-spacing', value); + break; + case 'writingMode': + setValueForAttribute(domElement, 'writing-mode', value); + break; + case 'xmlnsXlink': + setValueForAttribute(domElement, 'xmlns:xlink', value); + break; + case 'xHeight': + setValueForAttribute(domElement, 'x-height', value); + break; + case 'xlinkActuate': + setValueForNamespacedAttribute( + domElement, + xlinkNamespace, + 'xlink:actuate', + value, + ); + break; + case 'xlinkArcrole': + setValueForNamespacedAttribute( + domElement, + xlinkNamespace, + 'xlink:arcrole', + value, + ); + break; + case 'xlinkRole': + setValueForNamespacedAttribute( + domElement, + xlinkNamespace, + 'xlink:role', + value, + ); + break; + case 'xlinkShow': + setValueForNamespacedAttribute( + domElement, + xlinkNamespace, + 'xlink:show', + value, + ); + break; + case 'xlinkTitle': + setValueForNamespacedAttribute( + domElement, + xlinkNamespace, + 'xlink:title', + value, + ); + break; + case 'xlinkType': + setValueForNamespacedAttribute( + domElement, + xlinkNamespace, + 'xlink:type', + value, + ); + break; + case 'xmlBase': + setValueForNamespacedAttribute( + domElement, + xmlNamespace, + 'xml:base', + value, + ); + break; + case 'xmlLang': + setValueForNamespacedAttribute( + domElement, + xmlNamespace, + 'xml:lang', + value, + ); + break; + case 'xmlSpace': + setValueForNamespacedAttribute( + domElement, + xmlNamespace, + 'xml:space', + value, + ); + break; + // Properties that should not be allowed on custom elements. + case 'innerText': + case 'textContent': + if (enableCustomElementPropertySupport) { + break; + } + // eslint-disable-next-line no-fallthrough + default: { + if ( + key.length > 2 && + (key[0] === 'o' || key[0] === 'O') && + (key[1] === 'n' || key[1] === 'N') + ) { + if ( + __DEV__ && + registrationNameDependencies.hasOwnProperty(key) && + value != null && + typeof value !== 'function' + ) { + warnForInvalidEventListener(key, value); + } + } else { + setValueForAttribute(domElement, key, value); + } + } + } +} + +function setPropOnCustomElement( + domElement: Element, + tag: string, + key: string, + value: mixed, + props: any, +): void { + switch (key) { + case 'style': { + setValueForStyles(domElement, value); + break; + } + case 'dangerouslySetInnerHTML': { + if (value != null) { + if (typeof value !== 'object' || !('__html' in value)) { + throw new Error( + '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' + + 'Please visit https://reactjs.org/link/dangerously-set-inner-html ' + + 'for more information.', + ); + } + const nextHtml: any = value.__html; + if (nextHtml != null) { + if (props.children != null) { + throw new Error( + 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.', + ); + } + if (disableIEWorkarounds) { + domElement.innerHTML = nextHtml; + } else { + setInnerHTML(domElement, nextHtml); + } + } + } + break; + } + case 'children': { + if (typeof value === 'string') { + setTextContent(domElement, value); + } else if (typeof value === 'number') { + setTextContent(domElement, '' + value); + } + break; + } + case 'onScroll': { + if (value != null) { + if (__DEV__ && typeof value !== 'function') { + warnForInvalidEventListener(key, value); + } + listenToNonDelegatedEvent('scroll', domElement); + } + break; + } + case 'onClick': { + // TODO: This cast may not be sound for SVG, MathML or custom elements. + if (value != null) { + if (__DEV__ && typeof value !== 'function') { + warnForInvalidEventListener(key, value); + } + trapClickOnNonInteractiveElement(((domElement: any): HTMLElement)); + } + break; + } + case 'suppressContentEditableWarning': + case 'suppressHydrationWarning': + case 'innerHTML': { + // Noop + break; + } case 'innerText': // Properties case 'textContent': if (enableCustomElementPropertySupport) { @@ -390,33 +992,14 @@ function setProp( warnForInvalidEventListener(key, value); } } else { - if (isCustomElementTag) { - if (enableCustomElementPropertySupport) { - setValueForPropertyOnCustomComponent(domElement, key, value); - } else { - if (typeof value === 'boolean') { - // Special case before the new flag is on - value = '' + (value: any); - } - setValueForAttribute(domElement, key, value); - } + if (enableCustomElementPropertySupport) { + setValueForPropertyOnCustomComponent(domElement, key, value); } else { - if ( - // shouldIgnoreAttribute - // We have already filtered out reserved words. - key.length > 2 && - (key[0] === 'o' || key[0] === 'O') && - (key[1] === 'n' || key[1] === 'N') - ) { - return; - } - - const propertyInfo = getPropertyInfo(key); - if (propertyInfo !== null) { - setValueForProperty(domElement, propertyInfo, value); - } else { - setValueForAttribute(domElement, key, value); + if (typeof value === 'boolean') { + // Special case before the new flag is on + value = '' + (value: any); } + setValueForAttribute(domElement, key, value); } } } @@ -475,7 +1058,7 @@ export function setInitialProperties( } // defaultChecked and defaultValue are ignored by setProp default: { - setProp(domElement, tag, propKey, propValue, false, props); + setProp(domElement, tag, propKey, propValue, props); } } } @@ -505,7 +1088,7 @@ export function setInitialProperties( } // defaultValue are ignored by setProp default: { - setProp(domElement, tag, propKey, propValue, false, props); + setProp(domElement, tag, propKey, propValue, props); } } } @@ -545,7 +1128,7 @@ export function setInitialProperties( } // defaultValue is ignored by setProp default: { - setProp(domElement, tag, propKey, propValue, false, props); + setProp(domElement, tag, propKey, propValue, props); } } } @@ -575,7 +1158,7 @@ export function setInitialProperties( break; } default: { - setProp(domElement, tag, propKey, propValue, false, props); + setProp(domElement, tag, propKey, propValue, props); } } } @@ -657,7 +1240,7 @@ export function setInitialProperties( } // defaultChecked and defaultValue are ignored by setProp default: { - setProp(domElement, tag, propKey, propValue, false, props); + setProp(domElement, tag, propKey, propValue, props); } } } @@ -665,16 +1248,28 @@ export function setInitialProperties( } } - const isCustomElementTag = isCustomElement(tag, props); - for (const propKey in props) { - if (!props.hasOwnProperty(propKey)) { - continue; + 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 propValue = props[propKey]; - if (propValue == null) { - 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); } - setProp(domElement, tag, propKey, propValue, isCustomElementTag, props); } } @@ -837,7 +1432,7 @@ export function updateProperties( } // defaultChecked and defaultValue are ignored by setProp default: { - setProp(domElement, tag, propKey, propValue, false, nextProps); + setProp(domElement, tag, propKey, propValue, nextProps); } } } @@ -858,7 +1453,7 @@ export function updateProperties( } // defaultValue are ignored by setProp default: { - setProp(domElement, tag, propKey, propValue, false, nextProps); + setProp(domElement, tag, propKey, propValue, nextProps); } } } @@ -891,7 +1486,7 @@ export function updateProperties( } // defaultValue is ignored by setProp default: { - setProp(domElement, tag, propKey, propValue, false, nextProps); + setProp(domElement, tag, propKey, propValue, nextProps); } } } @@ -912,7 +1507,7 @@ export function updateProperties( break; } default: { - setProp(domElement, tag, propKey, propValue, false, nextProps); + setProp(domElement, tag, propKey, propValue, nextProps); } } } @@ -951,7 +1546,7 @@ export function updateProperties( } // defaultChecked and defaultValue are ignored by setProp default: { - setProp(domElement, tag, propKey, propValue, false, nextProps); + setProp(domElement, tag, propKey, propValue, nextProps); } } } @@ -959,12 +1554,19 @@ export function updateProperties( } } - const isCustomElementTag = isCustomElement(tag, nextProps); // Apply the diff. - for (let i = 0; i < updatePayload.length; i += 2) { - const propKey = updatePayload[i]; - const propValue = updatePayload[i + 1]; - setProp(domElement, tag, propKey, propValue, isCustomElementTag, nextProps); + if (isCustomElement(tag, nextProps)) { + for (let i = 0; i < updatePayload.length; i += 2) { + const propKey = updatePayload[i]; + const propValue = updatePayload[i + 1]; + setPropOnCustomElement(domElement, tag, propKey, propValue, nextProps); + } + } else { + for (let i = 0; i < updatePayload.length; i += 2) { + const propKey = updatePayload[i]; + const propValue = updatePayload[i + 1]; + setProp(domElement, tag, propKey, propValue, nextProps); + } } } @@ -990,10 +1592,314 @@ function diffHydratedStyles(domElement: Element, value: mixed) { if (canDiffStyleForHydrationWarning) { const expectedStyle = createDangerousStringForStyles(value); const serverValue = domElement.getAttribute('style'); - if (expectedStyle !== serverValue) { - warnForPropDifference('style', serverValue, expectedStyle); + warnForPropDifference('style', serverValue, expectedStyle); + } +} + +function hydrateAttribute( + domElement: Element, + propKey: string, + attributeName: string, + value: any, + extraAttributes: Set, +): void { + extraAttributes.delete(attributeName); + const serverValue = domElement.getAttribute(attributeName); + if (serverValue === null) { + switch (typeof value) { + case 'undefined': + case 'function': + case 'symbol': + case 'boolean': + return; + } + } else { + if (value == null) { + // We had an attribute but shouldn't have had one, so read it + // for the error message. + } else { + switch (typeof value) { + case 'function': + case 'symbol': + case 'boolean': + break; + default: { + if (__DEV__) { + checkAttributeStringCoercion(value, propKey); + } + if (serverValue === '' + value) { + return; + } + } + } } } + warnForPropDifference(propKey, serverValue, value); +} + +function hydrateBooleanAttribute( + domElement: Element, + propKey: string, + attributeName: string, + value: any, + extraAttributes: Set, +): void { + extraAttributes.delete(attributeName); + const serverValue = domElement.getAttribute(attributeName); + if (serverValue === null) { + switch (typeof value) { + case 'function': + case 'symbol': + return; + } + if (!value) { + return; + } + } else { + switch (typeof value) { + case 'function': + case 'symbol': + break; + default: { + if (value) { + // If this was a boolean, it doesn't matter what the value is + // the fact that we have it is the same as the expected. + // As long as it's positive. + return; + } + } + } + } + warnForPropDifference(propKey, serverValue, value); +} + +function hydrateOverloadedBooleanAttribute( + domElement: Element, + propKey: string, + attributeName: string, + value: any, + extraAttributes: Set, +): void { + extraAttributes.delete(attributeName); + const serverValue = domElement.getAttribute(attributeName); + if (serverValue === null) { + switch (typeof value) { + case 'undefined': + case 'function': + case 'symbol': + return; + default: + if (value === false) { + return; + } + } + } else { + if (value == null) { + // We had an attribute but shouldn't have had one, so read it + // for the error message. + } else { + switch (typeof value) { + case 'function': + case 'symbol': + break; + case 'boolean': + if (value === true && serverValue === '') { + return; + } + break; + default: { + if (__DEV__) { + checkAttributeStringCoercion(value, propKey); + } + if (serverValue === '' + value) { + return; + } + } + } + } + } + warnForPropDifference(propKey, serverValue, value); +} + +function hydrateBooleanishAttribute( + domElement: Element, + propKey: string, + attributeName: string, + value: any, + extraAttributes: Set, +): void { + extraAttributes.delete(attributeName); + const serverValue = domElement.getAttribute(attributeName); + if (serverValue === null) { + switch (typeof value) { + case 'undefined': + case 'function': + case 'symbol': + return; + } + } else { + if (value == null) { + // We had an attribute but shouldn't have had one, so read it + // for the error message. + } else { + switch (typeof value) { + case 'function': + case 'symbol': + break; + default: { + if (__DEV__) { + checkAttributeStringCoercion(value, attributeName); + } + if (serverValue === '' + (value: any)) { + return; + } + } + } + } + } + warnForPropDifference(propKey, serverValue, value); +} + +function hydrateNumericAttribute( + domElement: Element, + propKey: string, + attributeName: string, + value: any, + extraAttributes: Set, +): void { + extraAttributes.delete(attributeName); + const serverValue = domElement.getAttribute(attributeName); + if (serverValue === null) { + switch (typeof value) { + case 'undefined': + case 'function': + case 'symbol': + case 'boolean': + return; + default: + if (isNaN(value)) { + return; + } + } + } else { + if (value == null) { + // We had an attribute but shouldn't have had one, so read it + // for the error message. + } else { + switch (typeof value) { + case 'function': + case 'symbol': + case 'boolean': + break; + default: { + if (isNaN(value)) { + // We had an attribute but shouldn't have had one, so read it + // for the error message. + break; + } + if (__DEV__) { + checkAttributeStringCoercion(value, propKey); + } + if (serverValue === '' + value) { + return; + } + } + } + } + } + warnForPropDifference(propKey, serverValue, value); +} + +function hydratePositiveNumericAttribute( + domElement: Element, + propKey: string, + attributeName: string, + value: any, + extraAttributes: Set, +): void { + extraAttributes.delete(attributeName); + const serverValue = domElement.getAttribute(attributeName); + if (serverValue === null) { + switch (typeof value) { + case 'undefined': + case 'function': + case 'symbol': + case 'boolean': + return; + default: + if (isNaN(value) || value < 1) { + return; + } + } + } else { + if (value == null) { + // We had an attribute but shouldn't have had one, so read it + // for the error message. + } else { + switch (typeof value) { + case 'function': + case 'symbol': + case 'boolean': + break; + default: { + if (isNaN(value) || value < 1) { + // We had an attribute but shouldn't have had one, so read it + // for the error message. + break; + } + if (__DEV__) { + checkAttributeStringCoercion(value, propKey); + } + if (serverValue === '' + value) { + return; + } + } + } + } + } + warnForPropDifference(propKey, serverValue, value); +} + +function hydrateSanitizedAttribute( + domElement: Element, + propKey: string, + attributeName: string, + value: any, + extraAttributes: Set, +): void { + extraAttributes.delete(attributeName); + const serverValue = domElement.getAttribute(attributeName); + if (serverValue === null) { + switch (typeof value) { + case 'undefined': + case 'function': + case 'symbol': + case 'boolean': + return; + } + } else { + if (value == null) { + // We had an attribute but shouldn't have had one, so read it + // for the error message. + } else { + switch (typeof value) { + case 'function': + case 'symbol': + case 'boolean': + break; + default: { + if (__DEV__) { + checkAttributeStringCoercion(value, propKey); + } + const sanitizedValue = sanitizeURL('' + value); + if (serverValue === sanitizedValue) { + return; + } + } + } + } + } + warnForPropDifference(propKey, serverValue, value); } function diffHydratedCustomComponent( @@ -1001,19 +1907,19 @@ function diffHydratedCustomComponent( tag: string, props: Object, parentNamespaceDev: string, - extraAttributeNames: Set, + extraAttributes: Set, ) { for (const propKey in props) { if (!props.hasOwnProperty(propKey)) { continue; } - const nextProp = props[propKey]; - if (nextProp == null) { + const value = props[propKey]; + if (value == null) { continue; } if (registrationNameDependencies.hasOwnProperty(propKey)) { - if (typeof nextProp !== 'function') { - warnForInvalidEventListener(propKey, nextProp); + if (typeof value !== 'function') { + warnForInvalidEventListener(propKey, value); } continue; } @@ -1033,17 +1939,15 @@ function diffHydratedCustomComponent( continue; case 'dangerouslySetInnerHTML': const serverHTML = domElement.innerHTML; - const nextHtml = nextProp ? nextProp.__html : undefined; + const nextHtml = value ? value.__html : undefined; if (nextHtml != null) { const expectedHTML = normalizeHTML(domElement, nextHtml); - if (expectedHTML !== serverHTML) { - warnForPropDifference(propKey, serverHTML, expectedHTML); - } + warnForPropDifference(propKey, serverHTML, expectedHTML); } continue; case 'style': - extraAttributeNames.delete(propKey); - diffHydratedStyles(domElement, nextProp); + extraAttributes.delete(propKey); + diffHydratedStyles(domElement, value); continue; case 'offsetParent': case 'offsetTop': @@ -1054,7 +1958,7 @@ function diffHydratedCustomComponent( case 'outerText': case 'outerHTML': if (enableCustomElementPropertySupport) { - extraAttributeNames.delete(propKey.toLowerCase()); + extraAttributes.delete(propKey.toLowerCase()); if (__DEV__) { console.error( 'Assignment to read-only property will result in a no-op: `%s`', @@ -1067,15 +1971,13 @@ function diffHydratedCustomComponent( case 'className': if (enableCustomElementPropertySupport) { // className is a special cased property on the server to render as an attribute. - extraAttributeNames.delete('class'); + extraAttributes.delete('class'); const serverValue = getValueForAttributeOnCustomComponent( domElement, 'class', - nextProp, + value, ); - if (nextProp !== serverValue) { - warnForPropDifference('className', serverValue, nextProp); - } + warnForPropDifference('className', serverValue, value); continue; } // eslint-disable-next-line no-fallthrough @@ -1085,18 +1987,16 @@ function diffHydratedCustomComponent( ownNamespaceDev = getIntrinsicNamespace(tag); } if (ownNamespaceDev === HTML_NAMESPACE) { - extraAttributeNames.delete(propKey.toLowerCase()); + extraAttributes.delete(propKey.toLowerCase()); } else { - extraAttributeNames.delete(propKey); + extraAttributes.delete(propKey); } const serverValue = getValueForAttributeOnCustomComponent( domElement, propKey, - nextProp, + value, ); - if (nextProp !== serverValue) { - warnForPropDifference(propKey, serverValue, nextProp); - } + warnForPropDifference(propKey, serverValue, value); } } } @@ -1107,19 +2007,19 @@ function diffHydratedGenericElement( tag: string, props: Object, parentNamespaceDev: string, - extraAttributeNames: Set, + extraAttributes: Set, ) { for (const propKey in props) { if (!props.hasOwnProperty(propKey)) { continue; } - const nextProp = props[propKey]; - if (nextProp == null) { + const value = props[propKey]; + if (value == null) { continue; } if (registrationNameDependencies.hasOwnProperty(propKey)) { - if (typeof nextProp !== 'function') { - warnForInvalidEventListener(propKey, nextProp); + if (typeof value !== 'function') { + warnForInvalidEventListener(propKey, value); } continue; } @@ -1142,35 +2042,1000 @@ function diffHydratedGenericElement( continue; case 'dangerouslySetInnerHTML': const serverHTML = domElement.innerHTML; - const nextHtml = nextProp ? nextProp.__html : undefined; + const nextHtml = value ? value.__html : undefined; if (nextHtml != null) { const expectedHTML = normalizeHTML(domElement, nextHtml); - if (expectedHTML !== serverHTML) { - warnForPropDifference(propKey, serverHTML, expectedHTML); - } + warnForPropDifference(propKey, serverHTML, expectedHTML); } continue; case 'style': - extraAttributeNames.delete(propKey); - diffHydratedStyles(domElement, nextProp); + extraAttributes.delete(propKey); + diffHydratedStyles(domElement, value); continue; case 'multiple': { - extraAttributeNames.delete(propKey); + extraAttributes.delete(propKey); const serverValue = (domElement: any).multiple; - if (nextProp !== serverValue) { - warnForPropDifference('multiple', serverValue, nextProp); - } + warnForPropDifference(propKey, serverValue, value); continue; } case 'muted': { - extraAttributeNames.delete(propKey); + extraAttributes.delete(propKey); const serverValue = (domElement: any).muted; - if (nextProp !== serverValue) { - warnForPropDifference('muted', serverValue, nextProp); - } + warnForPropDifference(propKey, serverValue, value); continue; } - default: + case 'autoFocus': { + extraAttributes.delete('autofocus'); + const serverValue = (domElement: any).autofocus; + warnForPropDifference(propKey, serverValue, value); + continue; + } + case 'src': + case 'href': + case 'action': + if (enableFilterEmptyStringAttributesDOM) { + if (value === '') { + if (__DEV__) { + if (propKey === 'src') { + console.error( + 'An empty string ("") was passed to the %s attribute. ' + + 'This may cause the browser to download the whole page again over the network. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to %s instead of an empty string.', + propKey, + propKey, + ); + } else { + console.error( + 'An empty string ("") was passed to the %s attribute. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to %s instead of an empty string.', + propKey, + propKey, + ); + } + } + hydrateSanitizedAttribute( + domElement, + propKey, + propKey, + null, + extraAttributes, + ); + continue; + } + } + 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. @@ -1180,43 +3045,31 @@ function diffHydratedGenericElement( ) { continue; } - const propertyInfo = getPropertyInfo(propKey); let isMismatchDueToBadCasing = false; - let serverValue; - if (propertyInfo !== null) { - extraAttributeNames.delete(propertyInfo.attributeName); - serverValue = getValueForProperty( - domElement, - propKey, - nextProp, - propertyInfo, - ); + let ownNamespaceDev = parentNamespaceDev; + if (ownNamespaceDev === HTML_NAMESPACE) { + ownNamespaceDev = getIntrinsicNamespace(tag); + } + if (ownNamespaceDev === HTML_NAMESPACE) { + extraAttributes.delete(propKey.toLowerCase()); } else { - let ownNamespaceDev = parentNamespaceDev; - if (ownNamespaceDev === HTML_NAMESPACE) { - ownNamespaceDev = getIntrinsicNamespace(tag); + 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); } - 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); - } - extraAttributeNames.delete(propKey); - } - serverValue = getValueForAttribute(domElement, propKey, nextProp); + extraAttributes.delete(propKey); } - - if (nextProp !== serverValue && !isMismatchDueToBadCasing) { - warnForPropDifference(propKey, serverValue, nextProp); + const serverValue = getValueForAttribute(domElement, propKey, value); + if (!isMismatchDueToBadCasing) { + warnForPropDifference(propKey, serverValue, value); } + } } } } @@ -1346,7 +3199,7 @@ export function diffHydratedProperties( } if (__DEV__ && shouldWarnDev) { - const extraAttributeNames: Set = new Set(); + const extraAttributes: Set = new Set(); const attributes = domElement.attributes; for (let i = 0; i < attributes.length; i++) { const name = attributes[i].name.toLowerCase(); @@ -1362,7 +3215,7 @@ export function diffHydratedProperties( default: // Intentionally use the original name. // See discussion in https://github.com/facebook/react/pull/10676. - extraAttributeNames.add(attributes[i].name); + extraAttributes.add(attributes[i].name); } } if (isCustomElement(tag, props)) { @@ -1371,7 +3224,7 @@ export function diffHydratedProperties( tag, props, parentNamespaceDev, - extraAttributeNames, + extraAttributes, ); } else { diffHydratedGenericElement( @@ -1379,14 +3232,11 @@ export function diffHydratedProperties( tag, props, parentNamespaceDev, - extraAttributeNames, + extraAttributes, ); } - if ( - extraAttributeNames.size > 0 && - props.suppressHydrationWarning !== true - ) { - warnForExtraAttributes(extraAttributeNames); + if (extraAttributes.size > 0 && props.suppressHydrationWarning !== true) { + warnForExtraAttributes(extraAttributes); } } diff --git a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js index 60d53197bd..601706df50 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js +++ b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js @@ -39,13 +39,6 @@ import { } from 'react-server/src/ReactServerStreamConfig'; import isAttributeNameSafe from '../shared/isAttributeNameSafe'; -import { - getPropertyInfo, - BOOLEAN, - OVERLOADED_BOOLEAN, - NUMERIC, - POSITIVE_NUMERIC, -} from '../shared/DOMProperty'; import isUnitlessNumber from '../shared/isUnitlessNumber'; import {checkControlledValueProps} from '../shared/ReactControlledValuePropTypes'; @@ -621,6 +614,26 @@ function pushBooleanAttribute( } } +function pushStringAttribute( + target: Array, + name: string, + value: string | boolean | number | Function | Object, // not null or undefined +): void { + if ( + typeof value !== 'function' && + typeof value !== 'symbol' && + typeof value !== 'boolean' + ) { + target.push( + attributeSeparator, + stringToChunk(name), + attributeAssign, + stringToChunk(escapeTextForBrowser(value)), + attributeEnd, + ); + } +} + function pushAttribute( target: Array, name: string, @@ -638,151 +651,505 @@ function pushAttribute( case 'suppressHydrationWarning': // Ignored. These are built-in to React on the client. return; + case 'autoFocus': case 'multiple': - case 'muted': - pushBooleanAttribute(target, name, value); + case 'muted': { + pushBooleanAttribute(target, name.toLowerCase(), value); return; - } - if ( - // shouldIgnoreAttribute - // We have already filtered out null/undefined and reserved words. - name.length > 2 && - (name[0] === 'o' || name[0] === 'O') && - (name[1] === 'n' || name[1] === 'N') - ) { - return; - } - - const propertyInfo = getPropertyInfo(name); - if (propertyInfo !== null) { - // shouldRemoveAttribute - switch (typeof value) { - case 'function': - case 'symbol': // eslint-disable-line - return; - case 'boolean': { - if (!propertyInfo.acceptsBooleans) { + } + case 'src': + case 'href': + case 'action': + if (enableFilterEmptyStringAttributesDOM) { + if (value === '') { + if (__DEV__) { + if (name === 'src') { + console.error( + 'An empty string ("") was passed to the %s attribute. ' + + 'This may cause the browser to download the whole page again over the network. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to %s instead of an empty string.', + name, + name, + ); + } else { + console.error( + 'An empty string ("") was passed to the %s attribute. ' + + 'To fix this, either do not render the element at all ' + + 'or pass null to %s instead of an empty string.', + name, + name, + ); + } + } return; } } - } - 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, - ); - } - } + // Fall through to the last case which shouldn't remove empty strings. + // eslint-disable-next-line no-fallthrough + case 'formAction': { + if ( + value == null || + typeof value === 'function' || + typeof value === 'symbol' || + typeof value === 'boolean' + ) { return; } + if (__DEV__) { + checkAttributeStringCoercion(value, name); + } + const sanitizedValue = sanitizeURL('' + value); + target.push( + attributeSeparator, + stringToChunk(name), + attributeAssign, + stringToChunk(escapeTextForBrowser(sanitizedValue)), + attributeEnd, + ); + return; } - - const attributeName = propertyInfo.attributeName; - const attributeNameChunk = stringToChunk(attributeName); // TODO: If it's known we can cache the chunk. - - switch (propertyInfo.type) { - case BOOLEAN: - if (value) { - target.push( - attributeSeparator, - attributeNameChunk, - attributeEmptyString, - ); - } + case 'xlinkHref': { + if ( + typeof value === 'function' || + typeof value === 'symbol' || + typeof value === 'boolean' + ) { 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)); - } + } + if (__DEV__) { + checkAttributeStringCoercion(value, name); + } + const sanitizedValue = sanitizeURL('' + value); + target.push( + attributeSeparator, + stringToChunk('xlink:href'), + attributeAssign, + stringToChunk(escapeTextForBrowser(sanitizedValue)), + attributeEnd, + ); + return; + } + case 'contentEditable': + case 'spellCheck': + case 'draggable': + case 'value': + case 'autoReverse': + case 'externalResourcesRequired': + case 'focusable': + case 'preserveAlpha': { + // Booleanish String + // These are "enumerated" attributes that accept "true" and "false". + // In React, we let users pass `true` and `false` even though technically + // these aren't boolean attributes (they are coerced to strings). + if (typeof value !== 'function' && typeof value !== 'symbol') { target.push( attributeSeparator, - attributeNameChunk, + stringToChunk(name), attributeAssign, stringToChunk(escapeTextForBrowser(value)), attributeEnd, ); - } - } else 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; - } } + return; } - target.push( - attributeSeparator, - stringToChunk(name), - attributeAssign, - stringToChunk(escapeTextForBrowser(value)), - attributeEnd, - ); + case 'allowFullScreen': + case 'async': + case 'autoPlay': + case 'controls': + case 'default': + case 'defer': + case 'disabled': + case 'disablePictureInPicture': + case 'disableRemotePlayback': + case 'formNoValidate': + case 'hidden': + case 'loop': + case 'noModule': + case 'noValidate': + case 'open': + case 'playsInline': + case 'readOnly': + case 'required': + case 'reversed': + case 'scoped': + case 'seamless': + case 'itemScope': { + // Boolean + if (value && typeof value !== 'function' && typeof value !== 'symbol') { + target.push( + attributeSeparator, + stringToChunk(name), + attributeEmptyString, + ); + } + return; + } + case 'capture': + case 'download': { + // Overloaded Boolean + if (value === true) { + target.push( + attributeSeparator, + stringToChunk(name), + attributeEmptyString, + ); + } else if (value === false) { + // Ignored + } else if (typeof value !== 'function' && typeof value !== 'symbol') { + target.push( + attributeSeparator, + stringToChunk(name), + attributeAssign, + stringToChunk(escapeTextForBrowser(value)), + attributeEnd, + ); + } + return; + } + case 'cols': + case 'rows': + case 'size': + case 'span': { + // These are HTML attributes that must be positive numbers. + if ( + typeof value !== 'function' && + typeof value !== 'symbol' && + !isNaN(value) && + (value: any) >= 1 + ) { + target.push( + attributeSeparator, + stringToChunk(name), + attributeAssign, + stringToChunk(escapeTextForBrowser(value)), + attributeEnd, + ); + } + return; + } + case 'rowSpan': + case 'start': { + // These are HTML attributes that must be numbers. + if ( + typeof value !== 'function' && + typeof value !== 'symbol' && + !isNaN(value) + ) { + target.push( + attributeSeparator, + stringToChunk(name), + attributeAssign, + stringToChunk(escapeTextForBrowser(value)), + attributeEnd, + ); + } + return; + } + // A few React string attributes have a different name. + // This is a mapping from React prop names to the attribute names. + case 'acceptCharset': + pushStringAttribute(target, 'accept-charset', value); + return; + case 'className': + pushStringAttribute(target, 'class', value); + return; + case 'htmlFor': + pushStringAttribute(target, 'for', value); + return; + case 'httpEquiv': + pushStringAttribute(target, 'http-equiv', value); + return; + // HTML and SVG attributes, but the SVG attribute is case sensitive. + case 'tabIndex': + pushStringAttribute(target, 'tabindex', value); + return; + case 'crossOrigin': + pushStringAttribute(target, 'crossorigin', value); + return; + // This is a list of all SVG attributes that need special casing. + // Regular attributes that just accept strings. + case 'accentHeight': + pushStringAttribute(target, 'accent-height', value); + return; + case 'alignmentBaseline': + pushStringAttribute(target, 'alignment-baseline', value); + return; + case 'arabicForm': + pushStringAttribute(target, 'arabic-form', value); + return; + case 'baselineShift': + pushStringAttribute(target, 'baseline-shift', value); + return; + case 'capHeight': + pushStringAttribute(target, 'cap-height', value); + return; + case 'clipPath': + pushStringAttribute(target, 'clip-path', value); + return; + case 'clipRule': + pushStringAttribute(target, 'clip-rule', value); + return; + case 'colorInterpolation': + pushStringAttribute(target, 'color-interpolation', value); + return; + case 'colorInterpolationFilters': + pushStringAttribute(target, 'color-interpolation-filters', value); + return; + case 'colorProfile': + pushStringAttribute(target, 'color-profile', value); + return; + case 'colorRendering': + pushStringAttribute(target, 'color-rendering', value); + return; + case 'dominantBaseline': + pushStringAttribute(target, 'dominant-baseline', value); + return; + case 'enableBackground': + pushStringAttribute(target, 'enable-background', value); + return; + case 'fillOpacity': + pushStringAttribute(target, 'fill-opacity', value); + return; + case 'fillRule': + pushStringAttribute(target, 'fill-rule', value); + return; + case 'floodColor': + pushStringAttribute(target, 'flood-color', value); + return; + case 'floodOpacity': + pushStringAttribute(target, 'flood-opacity', value); + return; + case 'fontFamily': + pushStringAttribute(target, 'font-family', value); + return; + case 'fontSize': + pushStringAttribute(target, 'font-size', value); + return; + case 'fontSizeAdjust': + pushStringAttribute(target, 'font-size-adjust', value); + return; + case 'fontStretch': + pushStringAttribute(target, 'font-stretch', value); + return; + case 'fontStyle': + pushStringAttribute(target, 'font-style', value); + return; + case 'fontVariant': + pushStringAttribute(target, 'font-variant', value); + return; + case 'fontWeight': + pushStringAttribute(target, 'font-weight', value); + return; + case 'glyphName': + pushStringAttribute(target, 'glyph-name', value); + return; + case 'glyphOrientationHorizontal': + pushStringAttribute(target, 'glyph-orientation-horizontal', value); + return; + case 'glyphOrientationVertical': + pushStringAttribute(target, 'glyph-orientation-vertical', value); + return; + case 'horizAdvX': + pushStringAttribute(target, 'horiz-adv-x', value); + return; + case 'horizOriginX': + pushStringAttribute(target, 'horiz-origin-x', value); + return; + case 'imageRendering': + pushStringAttribute(target, 'image-rendering', value); + return; + case 'letterSpacing': + pushStringAttribute(target, 'letter-spacing', value); + return; + case 'lightingColor': + pushStringAttribute(target, 'lighting-color', value); + return; + case 'markerEnd': + pushStringAttribute(target, 'marker-end', value); + return; + case 'markerMid': + pushStringAttribute(target, 'marker-mid', value); + return; + case 'markerStart': + pushStringAttribute(target, 'marker-start', value); + return; + case 'overlinePosition': + pushStringAttribute(target, 'overline-position', value); + return; + case 'overlineThickness': + pushStringAttribute(target, 'overline-thickness', value); + return; + case 'paintOrder': + pushStringAttribute(target, 'paint-order', value); + return; + case 'panose-1': + pushStringAttribute(target, 'panose-1', value); + return; + case 'pointerEvents': + pushStringAttribute(target, 'pointer-events', value); + return; + case 'renderingIntent': + pushStringAttribute(target, 'rendering-intent', value); + return; + case 'shapeRendering': + pushStringAttribute(target, 'shape-rendering', value); + return; + case 'stopColor': + pushStringAttribute(target, 'stop-color', value); + return; + case 'stopOpacity': + pushStringAttribute(target, 'stop-opacity', value); + return; + case 'strikethroughPosition': + pushStringAttribute(target, 'strikethrough-position', value); + return; + case 'strikethroughThickness': + pushStringAttribute(target, 'strikethrough-thickness', value); + return; + case 'strokeDasharray': + pushStringAttribute(target, 'stroke-dasharray', value); + return; + case 'strokeDashoffset': + pushStringAttribute(target, 'stroke-dashoffset', value); + return; + case 'strokeLinecap': + pushStringAttribute(target, 'stroke-linecap', value); + return; + case 'strokeLinejoin': + pushStringAttribute(target, 'stroke-linejoin', value); + return; + case 'strokeMiterlimit': + pushStringAttribute(target, 'stroke-miterlimit', value); + return; + case 'strokeOpacity': + pushStringAttribute(target, 'stroke-opacity', value); + return; + case 'strokeWidth': + pushStringAttribute(target, 'stroke-width', value); + return; + case 'textAnchor': + pushStringAttribute(target, 'text-anchor', value); + return; + case 'textDecoration': + pushStringAttribute(target, 'text-decoration', value); + return; + case 'textRendering': + pushStringAttribute(target, 'text-rendering', value); + return; + case 'transformOrigin': + pushStringAttribute(target, 'transform-origin', value); + return; + case 'underlinePosition': + pushStringAttribute(target, 'underline-position', value); + return; + case 'underlineThickness': + pushStringAttribute(target, 'underline-thickness', value); + return; + case 'unicodeBidi': + pushStringAttribute(target, 'unicode-bidi', value); + return; + case 'unicodeRange': + pushStringAttribute(target, 'unicode-range', value); + return; + case 'unitsPerEm': + pushStringAttribute(target, 'units-per-em', value); + return; + case 'vAlphabetic': + pushStringAttribute(target, 'v-alphabetic', value); + return; + case 'vHanging': + pushStringAttribute(target, 'v-hanging', value); + return; + case 'vIdeographic': + pushStringAttribute(target, 'v-ideographic', value); + return; + case 'vMathematical': + pushStringAttribute(target, 'v-mathematical', value); + return; + case 'vectorEffect': + pushStringAttribute(target, 'vector-effect', value); + return; + case 'vertAdvY': + pushStringAttribute(target, 'vert-adv-y', value); + return; + case 'vertOriginX': + pushStringAttribute(target, 'vert-origin-x', value); + return; + case 'vertOriginY': + pushStringAttribute(target, 'vert-origin-y', value); + return; + case 'wordSpacing': + pushStringAttribute(target, 'word-spacing', value); + return; + case 'writingMode': + pushStringAttribute(target, 'writing-mode', value); + return; + case 'xmlnsXlink': + pushStringAttribute(target, 'xmlns:xlink', value); + return; + case 'xHeight': + pushStringAttribute(target, 'x-height', value); + return; + case 'xlinkActuate': + pushStringAttribute(target, 'xlink:actuate', value); + break; + case 'xlinkArcrole': + pushStringAttribute(target, 'xlink:arcrole', value); + break; + case 'xlinkRole': + pushStringAttribute(target, 'xlink:role', value); + break; + case 'xlinkShow': + pushStringAttribute(target, 'xlink:show', value); + break; + case 'xlinkTitle': + pushStringAttribute(target, 'xlink:title', value); + break; + case 'xlinkType': + pushStringAttribute(target, 'xlink:type', value); + break; + case 'xmlBase': + pushStringAttribute(target, 'xml:base', value); + break; + case 'xmlLang': + pushStringAttribute(target, 'xml:lang', value); + break; + case 'xmlSpace': + pushStringAttribute(target, 'xml:space', value); + break; + default: + if ( + // shouldIgnoreAttribute + // We have already filtered out null/undefined and reserved words. + name.length > 2 && + (name[0] === 'o' || name[0] === 'O') && + (name[1] === 'n' || name[1] === 'N') + ) { + return; + } + + if (isAttributeNameSafe(name)) { + // shouldRemoveAttribute + switch (typeof value) { + case 'function': + case 'symbol': // eslint-disable-line + return; + case 'boolean': { + const prefix = name.toLowerCase().slice(0, 5); + if (prefix !== 'data-' && prefix !== 'aria-') { + return; + } + } + } + target.push( + attributeSeparator, + stringToChunk(name), + attributeAssign, + stringToChunk(escapeTextForBrowser(value)), + attributeEnd, + ); + } } } diff --git a/packages/react-dom-bindings/src/shared/DOMProperty.js b/packages/react-dom-bindings/src/shared/DOMProperty.js deleted file mode 100644 index 8bd96110f3..0000000000 --- a/packages/react-dom-bindings/src/shared/DOMProperty.js +++ /dev/null @@ -1,411 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -type PropertyType = 0 | 1 | 2 | 3 | 4 | 5 | 6; - -// A simple string attribute. -// Attributes that aren't in the filter are presumed to have this type. -export const STRING = 1; - -// A string attribute that accepts booleans in React. In HTML, these are called -// "enumerated" attributes with "true" and "false" as possible values. -// When true, it should be set to a "true" string. -// When false, it should be set to a "false" string. -export const BOOLEANISH_STRING = 2; - -// A real boolean attribute. -// When true, it should be present (set either to an empty string or its name). -// When false, it should be omitted. -export const BOOLEAN = 3; - -// An attribute that can be used as a flag as well as with a value. -// When true, it should be present (set either to an empty string or its name). -// When false, it should be omitted. -// For any other value, should be present with that value. -export const OVERLOADED_BOOLEAN = 4; - -// An attribute that must be numeric or parse as a numeric. -// When falsy, it should be removed. -export const NUMERIC = 5; - -// An attribute that must be positive numeric or parse as a positive numeric. -// When falsy, it should be removed. -export const POSITIVE_NUMERIC = 6; - -export type PropertyInfo = { - +acceptsBooleans: boolean, - +attributeName: string, - +attributeNamespace: string | null, - +type: PropertyType, - +sanitizeURL: boolean, - +removeEmptyString: boolean, -}; - -export function getPropertyInfo(name: string): PropertyInfo | null { - return properties.hasOwnProperty(name) ? properties[name] : null; -} - -// $FlowFixMe[missing-this-annot] -function PropertyInfoRecord( - type: PropertyType, - attributeName: string, - attributeNamespace: string | null, - sanitizeURL: boolean, - removeEmptyString: boolean, -) { - this.acceptsBooleans = - type === BOOLEANISH_STRING || - type === BOOLEAN || - type === OVERLOADED_BOOLEAN; - this.attributeName = attributeName; - this.attributeNamespace = attributeNamespace; - this.type = type; - this.sanitizeURL = sanitizeURL; - this.removeEmptyString = removeEmptyString; -} - -// When adding attributes to this list, be sure to also add them to -// the `possibleStandardNames` module to ensure casing and incorrect -// name warnings. -const properties: {[string]: $FlowFixMe} = {}; - -// A few React string attributes have a different name. -// This is a mapping from React prop names to the attribute names. -[ - ['acceptCharset', 'accept-charset'], - ['className', 'class'], - ['htmlFor', 'for'], - ['httpEquiv', 'http-equiv'], -].forEach(([name, attributeName]) => { - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions - properties[name] = new PropertyInfoRecord( - STRING, - attributeName, // attributeName - null, // attributeNamespace - false, // sanitizeURL - false, // removeEmptyString - ); -}); - -// These are "enumerated" HTML attributes that accept "true" and "false". -// In React, we let users pass `true` and `false` even though technically -// these aren't boolean attributes (they are coerced to strings). -['contentEditable', 'draggable', 'spellCheck', 'value'].forEach(name => { - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions - properties[name] = new PropertyInfoRecord( - BOOLEANISH_STRING, - name.toLowerCase(), // attributeName - null, // attributeNamespace - false, // sanitizeURL - false, // removeEmptyString - ); -}); - -// These are "enumerated" SVG attributes that accept "true" and "false". -// In React, we let users pass `true` and `false` even though technically -// these aren't boolean attributes (they are coerced to strings). -// Since these are SVG attributes, their attribute names are case-sensitive. -[ - 'autoReverse', - 'externalResourcesRequired', - 'focusable', - 'preserveAlpha', -].forEach(name => { - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions - properties[name] = new PropertyInfoRecord( - BOOLEANISH_STRING, - name, // attributeName - null, // attributeNamespace - false, // sanitizeURL - false, // removeEmptyString - ); -}); - -// These are HTML boolean attributes. -[ - 'allowFullScreen', - 'async', - // Note: there is a special case that prevents it from being written to the DOM - // on the client side because the browsers are inconsistent. Instead we call focus(). - 'autoFocus', - 'autoPlay', - 'controls', - 'default', - 'defer', - 'disabled', - 'disablePictureInPicture', - 'disableRemotePlayback', - 'formNoValidate', - 'hidden', - 'loop', - 'noModule', - 'noValidate', - 'open', - 'playsInline', - 'readOnly', - 'required', - 'reversed', - 'scoped', - 'seamless', - // Microdata - 'itemScope', -].forEach(name => { - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions - properties[name] = new PropertyInfoRecord( - BOOLEAN, - name.toLowerCase(), // attributeName - null, // attributeNamespace - false, // sanitizeURL - false, // removeEmptyString - ); -}); - -// These are HTML attributes that are "overloaded booleans": they behave like -// booleans, but can also accept a string value. -[ - 'capture', - 'download', - - // NOTE: if you add a camelCased prop to this list, - // you'll need to set attributeName to name.toLowerCase() - // instead in the assignment below. -].forEach(name => { - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions - properties[name] = new PropertyInfoRecord( - OVERLOADED_BOOLEAN, - name, // attributeName - null, // attributeNamespace - false, // sanitizeURL - false, // removeEmptyString - ); -}); - -// These are HTML attributes that must be positive numbers. -[ - 'cols', - 'rows', - 'size', - 'span', - - // NOTE: if you add a camelCased prop to this list, - // you'll need to set attributeName to name.toLowerCase() - // instead in the assignment below. -].forEach(name => { - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions - properties[name] = new PropertyInfoRecord( - POSITIVE_NUMERIC, - name, // attributeName - null, // attributeNamespace - false, // sanitizeURL - false, // removeEmptyString - ); -}); - -// These are HTML attributes that must be numbers. -['rowSpan', 'start'].forEach(name => { - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions - properties[name] = new PropertyInfoRecord( - NUMERIC, - name.toLowerCase(), // attributeName - null, // attributeNamespace - false, // sanitizeURL - false, // removeEmptyString - ); -}); - -const CAMELIZE = /[\-\:]([a-z])/g; -const capitalize = (token: string) => token[1].toUpperCase(); - -// This is a list of all SVG attributes that need special casing, namespacing, -// or boolean value assignment. Regular attributes that just accept strings -// and have the same names are omitted, just like in the HTML attribute filter. -// Some of these attributes can be hard to find. This list was created by -// scraping the MDN documentation. -[ - 'accent-height', - 'alignment-baseline', - 'arabic-form', - 'baseline-shift', - 'cap-height', - 'clip-path', - 'clip-rule', - 'color-interpolation', - 'color-interpolation-filters', - 'color-profile', - 'color-rendering', - 'dominant-baseline', - 'enable-background', - 'fill-opacity', - 'fill-rule', - 'flood-color', - 'flood-opacity', - 'font-family', - 'font-size', - 'font-size-adjust', - 'font-stretch', - 'font-style', - 'font-variant', - 'font-weight', - 'glyph-name', - 'glyph-orientation-horizontal', - 'glyph-orientation-vertical', - 'horiz-adv-x', - 'horiz-origin-x', - 'image-rendering', - 'letter-spacing', - 'lighting-color', - 'marker-end', - 'marker-mid', - 'marker-start', - 'overline-position', - 'overline-thickness', - 'paint-order', - 'panose-1', - 'pointer-events', - 'rendering-intent', - 'shape-rendering', - 'stop-color', - 'stop-opacity', - 'strikethrough-position', - 'strikethrough-thickness', - 'stroke-dasharray', - 'stroke-dashoffset', - 'stroke-linecap', - 'stroke-linejoin', - 'stroke-miterlimit', - 'stroke-opacity', - 'stroke-width', - 'text-anchor', - 'text-decoration', - 'text-rendering', - 'transform-origin', - 'underline-position', - 'underline-thickness', - 'unicode-bidi', - 'unicode-range', - 'units-per-em', - 'v-alphabetic', - 'v-hanging', - 'v-ideographic', - 'v-mathematical', - 'vector-effect', - 'vert-adv-y', - 'vert-origin-x', - 'vert-origin-y', - 'word-spacing', - 'writing-mode', - 'xmlns:xlink', - 'x-height', - - // NOTE: if you add a camelCased prop to this list, - // you'll need to set attributeName to name.toLowerCase() - // instead in the assignment below. -].forEach(attributeName => { - const name = attributeName.replace(CAMELIZE, capitalize); - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions - properties[name] = new PropertyInfoRecord( - STRING, - attributeName, - null, // attributeNamespace - false, // sanitizeURL - false, // removeEmptyString - ); -}); - -// String SVG attributes with the xlink namespace. -[ - 'xlink:actuate', - 'xlink:arcrole', - 'xlink:role', - 'xlink:show', - 'xlink:title', - 'xlink:type', - - // NOTE: if you add a camelCased prop to this list, - // you'll need to set attributeName to name.toLowerCase() - // instead in the assignment below. -].forEach(attributeName => { - const name = attributeName.replace(CAMELIZE, capitalize); - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions - properties[name] = new PropertyInfoRecord( - STRING, - attributeName, - 'http://www.w3.org/1999/xlink', - false, // sanitizeURL - false, // removeEmptyString - ); -}); - -// String SVG attributes with the xml namespace. -[ - 'xml:base', - 'xml:lang', - 'xml:space', - - // NOTE: if you add a camelCased prop to this list, - // you'll need to set attributeName to name.toLowerCase() - // instead in the assignment below. -].forEach(attributeName => { - const name = attributeName.replace(CAMELIZE, capitalize); - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions - properties[name] = new PropertyInfoRecord( - STRING, - attributeName, - 'http://www.w3.org/XML/1998/namespace', - false, // sanitizeURL - false, // removeEmptyString - ); -}); - -// These attribute exists both in HTML and SVG. -// The attribute name is case-sensitive in SVG so we can't just use -// the React name like we do for attributes that exist only in HTML. -['tabIndex', 'crossOrigin'].forEach(attributeName => { - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions - properties[attributeName] = new PropertyInfoRecord( - STRING, - attributeName.toLowerCase(), // attributeName - null, // attributeNamespace - false, // sanitizeURL - false, // removeEmptyString - ); -}); - -// These attributes accept URLs. These must not allow javascript: URLS. -// These will also need to accept Trusted Types object in the future. -const xlinkHref = 'xlinkHref'; -// $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions -properties[xlinkHref] = new PropertyInfoRecord( - STRING, - 'xlink:href', - 'http://www.w3.org/1999/xlink', - true, // sanitizeURL - false, // removeEmptyString -); - -const formAction = 'formAction'; -// $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions -properties[formAction] = new PropertyInfoRecord( - STRING, - 'formaction', // attributeName - null, // attributeNamespace - true, // sanitizeURL - false, // removeEmptyString -); - -['src', 'href', 'action'].forEach(attributeName => { - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions - properties[attributeName] = new PropertyInfoRecord( - STRING, - attributeName.toLowerCase(), // attributeName - null, // attributeNamespace - true, // sanitizeURL - true, // removeEmptyString - ); -}); diff --git a/packages/react-dom-bindings/src/shared/ReactDOMUnknownPropertyHook.js b/packages/react-dom-bindings/src/shared/ReactDOMUnknownPropertyHook.js index e941899ab4..d7d2eb3b77 100644 --- a/packages/react-dom-bindings/src/shared/ReactDOMUnknownPropertyHook.js +++ b/packages/react-dom-bindings/src/shared/ReactDOMUnknownPropertyHook.js @@ -5,7 +5,6 @@ * LICENSE file in the root directory of this source tree. */ -import {BOOLEAN, getPropertyInfo} from './DOMProperty'; import {ATTRIBUTE_NAME_CHAR} from './isAttributeNameSafe'; import isCustomElement from './isCustomElement'; import possibleStandardNames from './possibleStandardNames'; @@ -131,8 +130,6 @@ function validateProperty(tagName, name, value, eventRegistry) { return true; } - const propertyInfo = getPropertyInfo(name); - // Known attributes should match the casing specified in the property config. if (possibleStandardNames.hasOwnProperty(lowerCasedName)) { const standardName = possibleStandardNames[lowerCasedName]; @@ -184,20 +181,49 @@ function validateProperty(tagName, name, value, eventRegistry) { switch (typeof value) { case 'boolean': { switch (name) { + case 'autoFocus': case 'checked': - case 'selected': case 'multiple': - case 'muted': { + case 'muted': + case 'selected': + case 'contentEditable': + case 'spellCheck': + case 'draggable': + case 'value': + case 'autoReverse': + case 'externalResourcesRequired': + case 'focusable': + case 'preserveAlpha': + case 'allowFullScreen': + case 'async': + case 'autoPlay': + case 'controls': + case 'default': + case 'defer': + case 'disabled': + case 'disablePictureInPicture': + case 'disableRemotePlayback': + case 'formNoValidate': + case 'hidden': + case 'loop': + case 'noModule': + case 'noValidate': + case 'open': + case 'playsInline': + case 'readOnly': + case 'required': + case 'reversed': + case 'scoped': + case 'seamless': + case 'itemScope': + case 'capture': + case 'download': { // Boolean properties can accept boolean values return true; } default: { - if (propertyInfo === null) { - const prefix = name.toLowerCase().slice(0, 5); - if (prefix === 'data-' || prefix === 'aria-') { - return true; - } - } else if (propertyInfo.acceptsBooleans) { + const prefix = name.toLowerCase().slice(0, 5); + if (prefix === 'data-' || prefix === 'aria-') { return true; } if (value) { @@ -244,13 +270,33 @@ function validateProperty(tagName, name, value, eventRegistry) { case 'checked': case 'selected': case 'multiple': - case 'muted': { + case 'muted': + case 'allowFullScreen': + case 'async': + case 'autoPlay': + case 'controls': + case 'default': + case 'defer': + case 'disabled': + case 'disablePictureInPicture': + case 'disableRemotePlayback': + case 'formNoValidate': + case 'hidden': + case 'loop': + case 'noModule': + case 'noValidate': + case 'open': + case 'playsInline': + case 'readOnly': + case 'required': + case 'reversed': + case 'scoped': + case 'seamless': + case 'itemScope': { break; } default: { - if (propertyInfo === null || propertyInfo.type !== BOOLEAN) { - return true; - } + return true; } } console.error( diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationUntrustedURL-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationUntrustedURL-test.js index c7da08897a..5bdfba5296 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationUntrustedURL-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationUntrustedURL-test.js @@ -339,7 +339,7 @@ describe('ReactDOMServerIntegration - Untrusted URLs - disableJavaScriptURLs', ( // The hydration validation calls it one extra time. // TODO: It would be good if we only called toString once for // consistency but the code structure makes that hard right now. - expectedToStringCalls = 5; + expectedToStringCalls = 4; } else if (__DEV__) { // Checking for string coercion problems results in double the // toString calls in DEV