Refactor ReactDOMComponent to use flatter property operations (#26433)

This is in line with the refactor I already did on Fizz earlier and
brings Fiber up to a similar structure.

We end up with a lot of extra checks due the extra abstractions we use
to check the various properties. This uses a flatter and more inline
model which makes it easier to see what each property does. The tradeoff
is that a change might need changes in more places.

The general structure is that there's a switch for tag first, then a
switch for each attribute special case, then a switch for the value. So
it's easy to follow where each scenario will end up and there shouldn't
be any unnecessary code executed along the way.

My goal is to eventually get rid of the meta-programming in DOMProperty
and CSSProperty but I'm leaving that in for now - in line with Fizz.

My next step is moving around things a bit in the diff/commit phases.
This is the first step to more refactors for perf and size, but also
because I'm adding more special cases so I need to have a flatter
structure that I can reason about for those special cases.
This commit is contained in:
Sebastian Markbåge
2023-03-20 20:56:14 -04:00
committed by GitHub
parent 0131d0cff4
commit 520f7f3ed4
12 changed files with 1193 additions and 906 deletions
@@ -7,9 +7,10 @@
import {shorthandToLonghand} from './CSSShorthandProperty';
import dangerousStyleValue from './dangerousStyleValue';
import hyphenateStyleName from '../shared/hyphenateStyleName';
import warnValidStyle from '../shared/warnValidStyle';
import {isUnitlessNumber} from '../shared/CSSProperty';
import {checkCSSPropertyStringCoercion} from 'shared/CheckStringCoercion';
/**
* Operations for dealing with CSS properties.
@@ -29,19 +30,36 @@ export function createDangerousStringForStyles(styles) {
if (!styles.hasOwnProperty(styleName)) {
continue;
}
const styleValue = styles[styleName];
if (styleValue != null) {
const value = styles[styleName];
if (value != null && typeof value !== 'boolean' && value !== '') {
const isCustomProperty = styleName.indexOf('--') === 0;
serialized +=
delimiter +
(isCustomProperty ? styleName : hyphenateStyleName(styleName)) +
':';
serialized += dangerousStyleValue(
styleName,
styleValue,
isCustomProperty,
);
if (isCustomProperty) {
if (__DEV__) {
checkCSSPropertyStringCoercion(value, styleName);
}
serialized += delimiter + styleName + ':' + ('' + value).trim();
} else {
if (
typeof value === 'number' &&
value !== 0 &&
!(
isUnitlessNumber.hasOwnProperty(styleName) &&
isUnitlessNumber[styleName]
)
) {
serialized +=
delimiter + hyphenateStyleName(styleName) + ':' + value + 'px';
} else {
if (__DEV__) {
checkCSSPropertyStringCoercion(value, styleName);
}
serialized +=
delimiter +
hyphenateStyleName(styleName) +
':' +
('' + value).trim();
}
}
delimiter = ';';
}
}
@@ -58,28 +76,46 @@ export function createDangerousStringForStyles(styles) {
*/
export function setValueForStyles(node, styles) {
const style = node.style;
for (let styleName in styles) {
for (const styleName in styles) {
if (!styles.hasOwnProperty(styleName)) {
continue;
}
const value = styles[styleName];
const isCustomProperty = styleName.indexOf('--') === 0;
if (__DEV__) {
if (!isCustomProperty) {
warnValidStyle(styleName, styles[styleName]);
warnValidStyle(styleName, value);
}
}
const styleValue = dangerousStyleValue(
styleName,
styles[styleName],
isCustomProperty,
);
if (styleName === 'float') {
styleName = 'cssFloat';
}
if (isCustomProperty) {
style.setProperty(styleName, styleValue);
if (value == null || typeof value === 'boolean' || value === '') {
if (isCustomProperty) {
style.setProperty(styleName, '');
} else if (styleName === 'float') {
style.cssFloat = '';
} else {
style[styleName] = '';
}
} else if (isCustomProperty) {
style.setProperty(styleName, value);
} else if (
typeof value === 'number' &&
value !== 0 &&
!(
isUnitlessNumber.hasOwnProperty(styleName) &&
isUnitlessNumber[styleName]
)
) {
style[styleName] = value + 'px'; // Presumes implicit 'px' suffix for unitless numbers
} else {
style[styleName] = styleValue;
if (styleName === 'float') {
style.cssFloat = value;
} else {
if (__DEV__) {
checkCSSPropertyStringCoercion(value, styleName);
}
style[styleName] = ('' + value).trim();
}
}
}
}
+383 -134
View File
@@ -9,17 +9,18 @@
import {
getPropertyInfo,
shouldIgnoreAttribute,
shouldRemoveAttribute,
isAttributeNameSafe,
BOOLEAN,
OVERLOADED_BOOLEAN,
NUMERIC,
POSITIVE_NUMERIC,
} from '../shared/DOMProperty';
import sanitizeURL from '../shared/sanitizeURL';
import {
disableJavaScriptURLs,
enableTrustedTypesIntegration,
enableCustomElementPropertySupport,
enableFilterEmptyStringAttributesDOM,
} from 'shared/ReactFeatureFlags';
import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
import {getFiberCurrentPropsFromNode} from './ReactDOMComponentTree';
@@ -41,68 +42,143 @@ export function getValueForProperty(
if (propertyInfo.mustUseProperty) {
const {propertyName} = propertyInfo;
return (node: any)[propertyName];
} else {
// This check protects multiple uses of `expected`, which is why the
// react-internal/safe-string-coercion rule is disabled in several spots
// below.
}
if (!disableJavaScriptURLs && propertyInfo.sanitizeURL) {
// If we haven't fully disabled javascript: URLs, and if
// the hydration is successful of a javascript: URL, we
// still want to warn on the client.
if (__DEV__) {
checkAttributeStringCoercion(expected, name);
}
sanitizeURL('' + (expected: any));
}
if (!disableJavaScriptURLs && propertyInfo.sanitizeURL) {
// If we haven't fully disabled javascript: URLs, and if
// the hydration is successful of a javascript: URL, we
// still want to warn on the client.
// eslint-disable-next-line react-internal/safe-string-coercion
sanitizeURL('' + (expected: any));
}
const attributeName = propertyInfo.attributeName;
const attributeName = propertyInfo.attributeName;
let stringValue = null;
if (propertyInfo.type === OVERLOADED_BOOLEAN) {
if (node.hasAttribute(attributeName)) {
const value = node.getAttribute(attributeName);
if (value === '') {
return true;
}
if (shouldRemoveAttribute(name, expected, propertyInfo, false)) {
return value;
}
// eslint-disable-next-line react-internal/safe-string-coercion
if (value === '' + (expected: any)) {
if (!node.hasAttribute(attributeName)) {
// shouldRemoveAttribute
switch (typeof expected) {
case 'function':
case 'symbol': // eslint-disable-line
return expected;
case 'boolean': {
if (!propertyInfo.acceptsBooleans) {
return expected;
}
return value;
}
} else if (node.hasAttribute(attributeName)) {
if (shouldRemoveAttribute(name, expected, propertyInfo, false)) {
// We had an attribute but shouldn't have had one, so read it
// for the error message.
return node.getAttribute(attributeName);
}
switch (propertyInfo.type) {
case BOOLEAN: {
if (!expected) {
return expected;
}
break;
}
if (propertyInfo.type === BOOLEAN) {
// 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.
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;
}
// 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.
stringValue = node.getAttribute(attributeName);
}
return expected === undefined ? undefined : null;
}
if (shouldRemoveAttribute(name, expected, propertyInfo, false)) {
return stringValue === null ? expected : stringValue;
// eslint-disable-next-line react-internal/safe-string-coercion
} else if (stringValue === '' + (expected: any)) {
return expected;
} else {
return stringValue;
// 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 (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 (value === '' + (expected: any)) {
return expected;
}
return value;
}
}
@@ -115,19 +191,76 @@ export function getValueForAttribute(
node: Element,
name: string,
expected: mixed,
isCustomComponentTag: boolean,
): mixed {
if (__DEV__) {
if (!isAttributeNameSafe(name)) {
return;
}
if (!node.hasAttribute(name)) {
// shouldRemoveAttribute
switch (typeof expected) {
case 'function':
case 'symbol': // eslint-disable-line
return expected;
case 'boolean': {
const prefix = name.toLowerCase().slice(0, 5);
if (prefix !== 'data-' && prefix !== 'aria-') {
return expected;
}
}
}
return expected === undefined ? undefined : null;
}
const value = node.getAttribute(name);
if (__DEV__) {
checkAttributeStringCoercion(expected, name);
}
if (value === '' + (expected: any)) {
return expected;
}
return value;
}
}
export function getValueForAttributeOnCustomComponent(
node: Element,
name: string,
expected: mixed,
): mixed {
if (__DEV__) {
if (!isAttributeNameSafe(name)) {
return;
}
if (!node.hasAttribute(name)) {
// shouldRemoveAttribute
switch (typeof expected) {
case 'symbol':
case 'object':
// Symbols and objects are ignored when they're emitted so
// it would be expected that they end up not having an attribute.
return expected;
case 'function':
if (enableCustomElementPropertySupport) {
return expected;
}
break;
case 'boolean':
if (enableCustomElementPropertySupport) {
if (expected === false) {
return expected;
}
}
}
return expected === undefined ? undefined : null;
}
if (enableCustomElementPropertySupport && name === 'className') {
// className is a special cased property on the server to render as an attribute.
name = 'class';
}
const value = node.getAttribute(name);
if (enableCustomElementPropertySupport) {
if (isCustomComponentTag && value === '') {
if (value === '' && expected === true) {
return true;
}
}
@@ -149,26 +282,188 @@ export function getValueForAttribute(
* @param {string} name
* @param {*} value
*/
export function setValueForProperty(
node: Element,
name: string,
value: mixed,
isCustomComponentTag: boolean,
) {
const propertyInfo = getPropertyInfo(name);
if (shouldIgnoreAttribute(name, propertyInfo, isCustomComponentTag)) {
export function setValueForProperty(node: Element, name: string, value: mixed) {
if (
// shouldIgnoreAttribute
// We have already filtered out 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) {
if (propertyInfo.mustUseProperty) {
// We assume mustUseProperty are of BOOLEAN type because that's the only way we use it
// right now.
(node: any)[propertyInfo.propertyName] =
value && typeof value !== 'function' && typeof value !== 'symbol';
return;
}
// The rest are treated as attributes with special cases.
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 (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,
);
}
}
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: {
let attributeValue;
// `setAttribute` with objects becomes only `[object]` in IE8/9,
// ('' + value) makes it output the correct toString()-value.
if (enableTrustedTypesIntegration) {
attributeValue = (value: any);
} else {
if (__DEV__) {
checkAttributeStringCoercion(value, attributeName);
}
attributeValue = '' + (value: any);
}
if (propertyInfo.sanitizeURL) {
sanitizeURL(attributeValue.toString());
}
const attributeNamespace = propertyInfo.attributeNamespace;
if (attributeNamespace) {
node.setAttributeNS(
attributeNamespace,
attributeName,
attributeValue,
);
} else {
node.setAttribute(attributeName, attributeValue);
}
}
}
} else if (isAttributeNameSafe(name)) {
// If the prop isn't in the special list, treat it as a simple attribute.
// shouldRemoveAttribute
if (value === null) {
node.removeAttribute(name);
return;
}
switch (typeof value) {
case 'undefined':
case 'function':
case 'symbol': // eslint-disable-line
node.removeAttribute(name);
return;
case 'boolean': {
const prefix = name.toLowerCase().slice(0, 5);
if (prefix !== 'data-' && prefix !== 'aria-') {
node.removeAttribute(name);
return;
}
}
}
if (__DEV__) {
checkAttributeStringCoercion(value, name);
}
node.setAttribute(
name,
enableTrustedTypesIntegration ? (value: any) : '' + (value: any),
);
}
}
export function setValueForPropertyOnCustomComponent(
node: Element,
name: string,
value: mixed,
) {
if (
enableCustomElementPropertySupport &&
isCustomComponentTag &&
name[0] === 'o' &&
name[1] === 'n'
) {
let eventName = name.replace(/Capture$/, '');
const useCapture = name !== eventName;
eventName = eventName.slice(2);
const useCapture = name.endsWith('Capture');
const eventName = name.substr(2, useCapture ? name.length - 9 : undefined);
const prevProps = getFiberCurrentPropsFromNode(node);
const prevValue = prevProps != null ? prevProps[name] : null;
@@ -185,92 +480,46 @@ export function setValueForProperty(
node.removeAttribute(name);
}
}
// $FlowFixMe value can't be casted to EventListener.
node.addEventListener(eventName, (value: EventListener), useCapture);
return;
}
}
if (
enableCustomElementPropertySupport &&
isCustomComponentTag &&
name in (node: any)
) {
if (enableCustomElementPropertySupport && name in (node: any)) {
(node: any)[name] = value;
return;
}
if (shouldRemoveAttribute(name, value, propertyInfo, isCustomComponentTag)) {
value = null;
}
if (enableCustomElementPropertySupport) {
if (isCustomComponentTag && value === true) {
value = '';
}
}
// If the prop isn't in the special list, treat it as a simple attribute.
if (isCustomComponentTag || propertyInfo === null) {
if (isAttributeNameSafe(name)) {
const attributeName = name;
if (value === null) {
node.removeAttribute(attributeName);
} else {
if (__DEV__) {
checkAttributeStringCoercion(value, name);
}
node.setAttribute(
attributeName,
enableTrustedTypesIntegration ? (value: any) : '' + (value: any),
);
}
}
return;
}
const {mustUseProperty} = propertyInfo;
if (mustUseProperty) {
const {propertyName} = propertyInfo;
if (isAttributeNameSafe(name)) {
// shouldRemoveAttribute
if (value === null) {
const {type} = propertyInfo;
(node: any)[propertyName] = type === BOOLEAN ? false : '';
} else {
// Contrary to `setAttribute`, object properties are properly
// `toString`ed by IE8/9.
(node: any)[propertyName] = value;
node.removeAttribute(name);
return;
}
return;
}
// The rest are treated as attributes with special cases.
const {attributeName, attributeNamespace} = propertyInfo;
if (value === null) {
node.removeAttribute(attributeName);
} else {
const {type} = propertyInfo;
let attributeValue;
if (type === BOOLEAN || (type === OVERLOADED_BOOLEAN && value === true)) {
// If attribute type is boolean, we know for sure it won't be an execution sink
// and we won't require Trusted Type here.
attributeValue = '';
} else {
// `setAttribute` with objects becomes only `[object]` in IE8/9,
// ('' + value) makes it output the correct toString()-value.
if (enableTrustedTypesIntegration) {
attributeValue = (value: any);
} else {
if (__DEV__) {
checkAttributeStringCoercion(value, attributeName);
switch (typeof value) {
case 'undefined':
case 'function':
case 'symbol': // eslint-disable-line
node.removeAttribute(name);
return;
case 'boolean': {
if (enableCustomElementPropertySupport) {
if (value === true) {
node.setAttribute(name, '');
return;
}
node.removeAttribute(name);
return;
}
attributeValue = '' + (value: any);
}
if (propertyInfo.sanitizeURL) {
sanitizeURL(attributeValue.toString());
}
}
if (attributeNamespace) {
node.setAttributeNS(attributeNamespace, attributeName, attributeValue);
} else {
node.setAttribute(attributeName, attributeValue);
if (__DEV__) {
checkAttributeStringCoercion(value, name);
}
node.setAttribute(
name,
enableTrustedTypesIntegration ? (value: any) : '' + (value: any),
);
}
}
File diff suppressed because it is too large Load Diff
@@ -70,7 +70,6 @@ import {
DOCUMENT_TYPE_NODE,
DOCUMENT_FRAGMENT_NODE,
} from './HTMLNodeType';
import dangerousStyleValue from './dangerousStyleValue';
import {retryIfBlockedOn} from '../events/ReactDOMEventReplaying';
@@ -750,7 +749,12 @@ export function unhideInstance(instance: Instance, props: Props): void {
styleProp.hasOwnProperty('display')
? styleProp.display
: null;
instance.style.display = dangerousStyleValue('display', display);
instance.style.display =
display == null || typeof display === 'boolean'
? ''
: // The value would've errored already if it wasn't safe.
// eslint-disable-next-line react-internal/safe-string-coercion
('' + display).trim();
}
export function unhideTextInstance(
+1 -2
View File
@@ -10,7 +10,6 @@
// TODO: direct imports like some-package/src/* are bad. Fix me.
import {getCurrentFiberOwnerNameInDevOrNull} from 'react-reconciler/src/ReactCurrentFiber';
import {setValueForProperty} from './DOMPropertyOperations';
import {getFiberCurrentPropsFromNode} from './ReactDOMComponentTree';
import {getToStringValue, toString} from './ToStringValue';
import {checkControlledValueProps} from '../shared/ReactControlledValuePropTypes';
@@ -130,7 +129,7 @@ export function updateChecked(element: Element, props: Object) {
const node = ((element: any): InputWithWrapperState);
const checked = props.checked;
if (checked != null) {
setValueForProperty(node, 'checked', checked, false);
node.checked = checked;
}
}
@@ -1,69 +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.
*
* @noflow
*/
import voidElementTags from './voidElementTags';
const HTML = '__html';
function assertValidProps(tag: string, props: ?Object) {
if (!props) {
return;
}
// Note the use of `==` which checks for null or undefined.
if (voidElementTags[tag]) {
if (props.children != null || props.dangerouslySetInnerHTML != null) {
throw new Error(
`${tag} is a void element tag and must neither have \`children\` nor ` +
'use `dangerouslySetInnerHTML`.',
);
}
}
if (props.dangerouslySetInnerHTML != null) {
if (props.children != null) {
throw new Error(
'Can only set one of `children` or `props.dangerouslySetInnerHTML`.',
);
}
if (
typeof props.dangerouslySetInnerHTML !== 'object' ||
!(HTML in props.dangerouslySetInnerHTML)
) {
throw new Error(
'`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +
'Please visit https://reactjs.org/link/dangerously-set-inner-html ' +
'for more information.',
);
}
}
if (__DEV__) {
if (
!props.suppressContentEditableWarning &&
props.contentEditable &&
props.children != null
) {
console.error(
'A component is `contentEditable` and contains `children` managed by ' +
'React. It is now your responsibility to guarantee that none of ' +
'those nodes are unexpectedly modified or duplicated. This is ' +
'probably not intentional.',
);
}
}
if (props.style != null && typeof props.style !== '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.',
);
}
}
export default assertValidProps;
@@ -1,51 +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.
*/
import {isUnitlessNumber} from '../shared/CSSProperty';
import {checkCSSPropertyStringCoercion} from 'shared/CheckStringCoercion';
/**
* Convert a value into the proper css writable value. The style name `name`
* should be logical (no hyphens), as specified
* in `CSSProperty.isUnitlessNumber`.
*
* @param {string} name CSS property name such as `topMargin`.
* @param {*} value CSS property value such as `10px`.
* @return {string} Normalized style value with dimensions applied.
*/
function dangerousStyleValue(name, value, isCustomProperty) {
// Note that we've removed escapeTextForBrowser() calls here since the
// whole string will be escaped when the attribute is injected into
// the markup. If you provide unsafe user data here they can inject
// arbitrary CSS which may be problematic (I couldn't repro this):
// https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet
// http://www.thespanner.co.uk/2007/11/26/ultimate-xss-css-injection/
// This is not an XSS hole but instead a potential CSS injection issue
// which has lead to a greater discussion about how we're going to
// trust URLs moving forward. See #2115901
const isEmpty = value == null || typeof value === 'boolean' || value === '';
if (isEmpty) {
return '';
}
if (
!isCustomProperty &&
typeof value === 'number' &&
value !== 0 &&
!(isUnitlessNumber.hasOwnProperty(name) && isUnitlessNumber[name])
) {
return value + 'px'; // Presumes implicit 'px' suffix for unitless numbers
}
if (__DEV__) {
checkCSSPropertyStringCoercion(value, name);
}
return ('' + value).trim();
}
export default dangerousStyleValue;
@@ -1,30 +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.
*/
// For HTML, certain tags should omit their close tag. We keep a list for
// those special-case tags.
const omittedCloseTags = {
area: true,
base: true,
br: true,
col: true,
embed: true,
hr: true,
img: true,
input: true,
keygen: true,
link: true,
meta: true,
param: true,
source: true,
track: true,
wbr: true,
// NOTE: menuitem's close tag should be omitted, but that causes problems.
};
export default omittedCloseTags;
@@ -1,18 +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.
*/
import omittedCloseTags from './omittedCloseTags';
// For HTML, certain tags cannot have children. This has the same purpose as
// `omittedCloseTags` except that `menuitem` should still have its closing tag.
const voidElementTags = {
menuitem: true,
...omittedCloseTags,
};
export default voidElementTags;
-157
View File
@@ -7,18 +7,10 @@
* @flow
*/
import {
enableFilterEmptyStringAttributesDOM,
enableCustomElementPropertySupport,
} from 'shared/ReactFeatureFlags';
import hasOwnProperty from 'shared/hasOwnProperty';
type PropertyType = 0 | 1 | 2 | 3 | 4 | 5 | 6;
// A reserved attribute.
// It is handled by React separately and shouldn't be written to the DOM.
export const RESERVED = 0;
// A simple string attribute.
// Attributes that aren't in the filter are presumed to have this type.
export const STRING = 1;
@@ -91,124 +83,6 @@ export function isAttributeNameSafe(attributeName: string): boolean {
return false;
}
export function shouldIgnoreAttribute(
name: string,
propertyInfo: PropertyInfo | null,
isCustomComponentTag: boolean,
): boolean {
if (propertyInfo !== null) {
return propertyInfo.type === RESERVED;
}
if (isCustomComponentTag) {
return false;
}
if (
name.length > 2 &&
(name[0] === 'o' || name[0] === 'O') &&
(name[1] === 'n' || name[1] === 'N')
) {
return true;
}
return false;
}
export function shouldRemoveAttributeWithWarning(
name: string,
value: mixed,
propertyInfo: PropertyInfo | null,
isCustomComponentTag: boolean,
): boolean {
if (propertyInfo !== null && propertyInfo.type === RESERVED) {
return false;
}
switch (typeof value) {
case 'function':
case 'symbol': // eslint-disable-line
return true;
case 'boolean': {
if (isCustomComponentTag) {
return false;
}
if (propertyInfo !== null) {
return !propertyInfo.acceptsBooleans;
} else {
const prefix = name.toLowerCase().slice(0, 5);
return prefix !== 'data-' && prefix !== 'aria-';
}
}
default:
return false;
}
}
export function shouldRemoveAttribute(
name: string,
value: mixed,
propertyInfo: PropertyInfo | null,
isCustomComponentTag: boolean,
): boolean {
if (value === null || typeof value === 'undefined') {
return true;
}
if (
shouldRemoveAttributeWithWarning(
name,
value,
propertyInfo,
isCustomComponentTag,
)
) {
return true;
}
if (isCustomComponentTag) {
if (enableCustomElementPropertySupport) {
if (value === false) {
return true;
}
}
return false;
}
if (propertyInfo !== null) {
if (enableFilterEmptyStringAttributesDOM) {
if (propertyInfo.removeEmptyString && value === '') {
if (__DEV__) {
if (name === 'src') {
console.error(
'An empty string ("") was passed to the %s attribute. ' +
'This may cause the browser to download the whole page again over the network. ' +
'To fix this, either do not render the element at all ' +
'or pass null to %s instead of an empty string.',
name,
name,
);
} else {
console.error(
'An empty string ("") was passed to the %s attribute. ' +
'To fix this, either do not render the element at all ' +
'or pass null to %s instead of an empty string.',
name,
name,
);
}
}
return true;
}
}
switch (propertyInfo.type) {
case BOOLEAN:
return !value;
case OVERLOADED_BOOLEAN:
return value === false;
case NUMERIC:
return isNaN(value);
case POSITIVE_NUMERIC:
return isNaN(value) || (value: any) < 1;
}
}
return false;
}
export function getPropertyInfo(name: string): PropertyInfo | null {
return properties.hasOwnProperty(name) ? properties[name] : null;
}
@@ -241,37 +115,6 @@ function PropertyInfoRecord(
// name warnings.
const properties: {[string]: $FlowFixMe} = {};
// These props are reserved by React. They shouldn't be written to the DOM.
const reservedProps = [
'children',
'dangerouslySetInnerHTML',
// TODO: This prevents the assignment of defaultValue to regular
// elements (not just inputs). Now that ReactDOMInput assigns to the
// defaultValue property -- do we need this?
'defaultValue',
'defaultChecked',
'innerHTML',
'suppressContentEditableWarning',
'suppressHydrationWarning',
'style',
];
if (enableCustomElementPropertySupport) {
reservedProps.push('innerText', 'textContent');
}
reservedProps.forEach(name => {
// $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
properties[name] = new PropertyInfoRecord(
name,
RESERVED,
false, // mustUseProperty
name, // attributeName
null, // attributeNamespace
false, // sanitizeURL
false, // removeEmptyString
);
});
// A few React string attributes have a different name.
// This is a mapping from React prop names to the attribute names.
[
@@ -5,16 +5,11 @@
* LICENSE file in the root directory of this source tree.
*/
import {
ATTRIBUTE_NAME_CHAR,
BOOLEAN,
RESERVED,
shouldRemoveAttributeWithWarning,
getPropertyInfo,
} from './DOMProperty';
import {ATTRIBUTE_NAME_CHAR, BOOLEAN, getPropertyInfo} from './DOMProperty';
import isCustomComponent from './isCustomComponent';
import possibleStandardNames from './possibleStandardNames';
import hasOwnProperty from 'shared/hasOwnProperty';
import {enableCustomElementPropertySupport} from 'shared/ReactFeatureFlags';
const warnedProperties = {};
const EVENT_NAME_REGEX = /^on./;
@@ -136,7 +131,6 @@ function validateProperty(tagName, name, value, eventRegistry) {
}
const propertyInfo = getPropertyInfo(name);
const isReserved = propertyInfo !== null && propertyInfo.type === RESERVED;
// Known attributes should match the casing specified in the property config.
if (possibleStandardNames.hasOwnProperty(lowerCasedName)) {
@@ -150,7 +144,7 @@ function validateProperty(tagName, name, value, eventRegistry) {
warnedProperties[name] = true;
return true;
}
} else if (!isReserved && name !== lowerCasedName) {
} else if (name !== lowerCasedName) {
// Unknown attributes should have lowercase casing since that's how they
// will be cased anyway with server rendering.
console.error(
@@ -166,51 +160,71 @@ function validateProperty(tagName, name, value, eventRegistry) {
return true;
}
if (
typeof value === 'boolean' &&
shouldRemoveAttributeWithWarning(name, value, propertyInfo, false)
) {
if (value) {
console.error(
'Received `%s` for a non-boolean attribute `%s`.\n\n' +
'If you want to write it to the DOM, pass a string instead: ' +
'%s="%s" or %s={value.toString()}.',
value,
name,
name,
value,
name,
);
} else {
console.error(
'Received `%s` for a non-boolean attribute `%s`.\n\n' +
'If you want to write it to the DOM, pass a string instead: ' +
'%s="%s" or %s={value.toString()}.\n\n' +
'If you used to conditionally omit it with %s={condition && value}, ' +
'pass %s={condition ? value : undefined} instead.',
value,
name,
name,
value,
name,
name,
name,
);
// Now that we've validated casing, do not validate
// data types for reserved props
switch (name) {
case 'dangerouslySetInnerHTML':
case 'children':
case 'style':
case 'suppressContentEditableWarning':
case 'suppressHydrationWarning':
case 'defaultValue': // Reserved
case 'defaultChecked':
case 'innerHTML': {
return true;
}
case 'innerText': // Properties
case 'textContent':
if (enableCustomElementPropertySupport) {
return true;
}
}
if (typeof value === 'boolean') {
const prefix = name.toLowerCase().slice(0, 5);
const acceptsBooleans =
propertyInfo !== null
? propertyInfo.acceptsBooleans
: prefix === 'data-' || prefix === 'aria-';
if (!acceptsBooleans) {
if (value) {
console.error(
'Received `%s` for a non-boolean attribute `%s`.\n\n' +
'If you want to write it to the DOM, pass a string instead: ' +
'%s="%s" or %s={value.toString()}.',
value,
name,
name,
value,
name,
);
} else {
console.error(
'Received `%s` for a non-boolean attribute `%s`.\n\n' +
'If you want to write it to the DOM, pass a string instead: ' +
'%s="%s" or %s={value.toString()}.\n\n' +
'If you used to conditionally omit it with %s={condition && value}, ' +
'pass %s={condition ? value : undefined} instead.',
value,
name,
name,
value,
name,
name,
name,
);
}
}
warnedProperties[name] = true;
return true;
}
// Now that we've validated casing, do not validate
// data types for reserved props
if (isReserved) {
return true;
}
// Warn when a known attribute is a bad type
if (shouldRemoveAttributeWithWarning(name, value, propertyInfo, false)) {
warnedProperties[name] = true;
return false;
switch (typeof value) {
case 'function':
case 'symbol': // eslint-disable-line
warnedProperties[name] = true;
return false;
}
// Warn when passing the strings 'false' or 'true' into a boolean prop
+2 -1
View File
@@ -1369,7 +1369,8 @@ describe('ReactDOMComponent', () => {
expect(function () {
mountComponent({children: '', dangerouslySetInnerHTML: ''});
}).toThrowError(
'Can only set one of `children` or `props.dangerouslySetInnerHTML`.',
'`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +
'Please visit https://reactjs.org/link/dangerously-set-inner-html for more information.',
);
});