/**
* 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 {ATTRIBUTE_NAME_CHAR} from './isAttributeNameSafe';
import isCustomElement from './isCustomElement';
import possibleStandardNames from './possibleStandardNames';
import hasOwnProperty from 'shared/hasOwnProperty';
import {
enableCustomElementPropertySupport,
enableFormActions,
} from 'shared/ReactFeatureFlags';
const warnedProperties = {};
const EVENT_NAME_REGEX = /^on./;
const INVALID_EVENT_NAME_REGEX = /^on[^A-Z]/;
const rARIA = __DEV__
? new RegExp('^(aria)-[' + ATTRIBUTE_NAME_CHAR + ']*$')
: null;
const rARIACamel = __DEV__
? new RegExp('^(aria)[A-Z][' + ATTRIBUTE_NAME_CHAR + ']*$')
: null;
function validateProperty(tagName, name, value, eventRegistry) {
if (__DEV__) {
if (hasOwnProperty.call(warnedProperties, name) && warnedProperties[name]) {
return true;
}
const lowerCasedName = name.toLowerCase();
if (lowerCasedName === 'onfocusin' || lowerCasedName === 'onfocusout') {
console.error(
'React uses onFocus and onBlur instead of onFocusIn and onFocusOut. ' +
'All React events are normalized to bubble, so onFocusIn and onFocusOut ' +
'are not needed/supported by React.',
);
warnedProperties[name] = true;
return true;
}
if (enableFormActions) {
// Actions are special because unlike events they can have other value types.
if (typeof value === 'function') {
if (tagName === 'form' && name === 'action') {
return true;
}
if (tagName === 'input' && name === 'formAction') {
return true;
}
if (tagName === 'button' && name === 'formAction') {
return true;
}
}
}
// We can't rely on the event system being injected on the server.
if (eventRegistry != null) {
const {registrationNameDependencies, possibleRegistrationNames} =
eventRegistry;
if (registrationNameDependencies.hasOwnProperty(name)) {
return true;
}
const registrationName = possibleRegistrationNames.hasOwnProperty(
lowerCasedName,
)
? possibleRegistrationNames[lowerCasedName]
: null;
if (registrationName != null) {
console.error(
'Invalid event handler property `%s`. Did you mean `%s`?',
name,
registrationName,
);
warnedProperties[name] = true;
return true;
}
if (EVENT_NAME_REGEX.test(name)) {
console.error(
'Unknown event handler property `%s`. It will be ignored.',
name,
);
warnedProperties[name] = true;
return true;
}
} else if (EVENT_NAME_REGEX.test(name)) {
// If no event plugins have been injected, we are in a server environment.
// So we can't tell if the event name is correct for sure, but we can filter
// out known bad ones like `onclick`. We can't suggest a specific replacement though.
if (INVALID_EVENT_NAME_REGEX.test(name)) {
console.error(
'Invalid event handler property `%s`. ' +
'React events use the camelCase naming convention, for example `onClick`.',
name,
);
}
warnedProperties[name] = true;
return true;
}
// Let the ARIA attribute hook validate ARIA attributes
if (rARIA.test(name) || rARIACamel.test(name)) {
return true;
}
if (lowerCasedName === 'innerhtml') {
console.error(
'Directly setting property `innerHTML` is not permitted. ' +
'For more information, lookup documentation on `dangerouslySetInnerHTML`.',
);
warnedProperties[name] = true;
return true;
}
if (lowerCasedName === 'aria') {
console.error(
'The `aria` attribute is reserved for future use in React. ' +
'Pass individual `aria-` attributes instead.',
);
warnedProperties[name] = true;
return true;
}
if (
lowerCasedName === 'is' &&
value !== null &&
value !== undefined &&
typeof value !== 'string'
) {
console.error(
'Received a `%s` for a string attribute `is`. If this is expected, cast ' +
'the value to a string.',
typeof value,
);
warnedProperties[name] = true;
return true;
}
if (typeof value === 'number' && isNaN(value)) {
console.error(
'Received NaN for the `%s` attribute. If this is expected, cast ' +
'the value to a string.',
name,
);
warnedProperties[name] = true;
return true;
}
// Known attributes should match the casing specified in the property config.
if (possibleStandardNames.hasOwnProperty(lowerCasedName)) {
const standardName = possibleStandardNames[lowerCasedName];
if (standardName !== name) {
console.error(
'Invalid DOM property `%s`. Did you mean `%s`?',
name,
standardName,
);
warnedProperties[name] = true;
return true;
}
} else if (name !== lowerCasedName) {
// Unknown attributes should have lowercase casing since that's how they
// will be cased anyway with server rendering.
console.error(
'React does not recognize the `%s` prop on a DOM element. If you ' +
'intentionally want it to appear in the DOM as a custom ' +
'attribute, spell it as lowercase `%s` instead. ' +
'If you accidentally passed it from a parent component, remove ' +
'it from the DOM element.',
name,
lowerCasedName,
);
warnedProperties[name] = true;
return true;
}
// 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;
}
}
switch (typeof value) {
case 'boolean': {
switch (name) {
case 'autoFocus':
case 'checked':
case 'multiple':
case 'muted':
case 'selected':
case 'contentEditable':
case 'spellCheck':
case 'draggable':
case 'value':
case 'autoReverse':
case 'externalResourcesRequired':
case 'focusable':
case 'preserveAlpha':
case 'allowFullScreen':
case 'async':
case 'autoPlay':
case 'controls':
case 'default':
case 'defer':
case 'disabled':
case 'disablePictureInPicture':
case 'disableRemotePlayback':
case 'formNoValidate':
case 'hidden':
case 'loop':
case 'noModule':
case 'noValidate':
case 'open':
case 'playsInline':
case 'readOnly':
case 'required':
case 'reversed':
case 'scoped':
case 'seamless':
case 'itemScope':
case 'capture':
case 'download': {
// Boolean properties can accept boolean values
return true;
}
default: {
const prefix = name.toLowerCase().slice(0, 5);
if (prefix === 'data-' || prefix === 'aria-') {
return true;
}
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;
}
}
}
case 'function':
case 'symbol': // eslint-disable-line
// Warn when a known attribute is a bad type
warnedProperties[name] = true;
return false;
case 'string': {
// Warn when passing the strings 'false' or 'true' into a boolean prop
if (value === 'false' || value === 'true') {
switch (name) {
case 'checked':
case 'selected':
case 'multiple':
case 'muted':
case 'allowFullScreen':
case 'async':
case 'autoPlay':
case 'controls':
case 'default':
case 'defer':
case 'disabled':
case 'disablePictureInPicture':
case 'disableRemotePlayback':
case 'formNoValidate':
case 'hidden':
case 'loop':
case 'noModule':
case 'noValidate':
case 'open':
case 'playsInline':
case 'readOnly':
case 'required':
case 'reversed':
case 'scoped':
case 'seamless':
case 'itemScope': {
break;
}
default: {
return true;
}
}
console.error(
'Received the string `%s` for the boolean attribute `%s`. ' +
'%s ' +
'Did you mean %s={%s}?',
value,
name,
value === 'false'
? 'The browser will interpret it as a truthy value.'
: 'Although this works, it will not work as expected if you pass the string "false".',
name,
value,
);
warnedProperties[name] = true;
return true;
}
}
}
return true;
}
}
function warnUnknownProperties(type, props, eventRegistry) {
if (__DEV__) {
const unknownProps = [];
for (const key in props) {
const isValid = validateProperty(type, key, props[key], eventRegistry);
if (!isValid) {
unknownProps.push(key);
}
}
const unknownPropString = unknownProps
.map(prop => '`' + prop + '`')
.join(', ');
if (unknownProps.length === 1) {
console.error(
'Invalid value for prop %s on <%s> tag. Either remove it from the element, ' +
'or pass a string or number value to keep it in the DOM. ' +
'For details, see https://reactjs.org/link/attribute-behavior ',
unknownPropString,
type,
);
} else if (unknownProps.length > 1) {
console.error(
'Invalid values for props %s on <%s> tag. Either remove them from the element, ' +
'or pass a string or number value to keep them in the DOM. ' +
'For details, see https://reactjs.org/link/attribute-behavior ',
unknownPropString,
type,
);
}
}
}
export function validateProperties(type, props, eventRegistry) {
if (isCustomElement(type, props) || typeof props.is === 'string') {
return;
}
warnUnknownProperties(type, props, eventRegistry);
}