/**
* 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
*/
import isAttributeNameSafe from '../shared/isAttributeNameSafe';
import {
enableTrustedTypesIntegration,
enableCustomElementPropertySupport,
} from 'shared/ReactFeatureFlags';
import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
import {getFiberCurrentPropsFromNode} from './ReactDOMComponentTree';
/**
* 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
* attributes have multiple equivalent values.
*/
export function getValueForAttribute(
node: Element,
name: string,
expected: mixed,
): 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;
}
const value = node.getAttribute(name);
if (enableCustomElementPropertySupport) {
if (value === '' && expected === true) {
return true;
}
}
if (__DEV__) {
checkAttributeStringCoercion(expected, name);
}
if (value === '' + (expected: any)) {
return expected;
}
return value;
}
}
export function setValueForAttribute(
node: Element,
name: string,
value: mixed,
) {
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 setValueForKnownAttribute(
node: Element,
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.setAttribute(
name,
enableTrustedTypesIntegration ? (value: any) : '' + (value: any),
);
}
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,
value: mixed,
) {
if (name[0] === 'o' && name[1] === 'n') {
const useCapture = name.endsWith('Capture');
const eventName = name.slice(2, useCapture ? name.length - 7 : undefined);
const prevProps = getFiberCurrentPropsFromNode(node);
const prevValue = prevProps != null ? prevProps[name] : null;
if (typeof prevValue === 'function') {
node.removeEventListener(eventName, prevValue, useCapture);
}
if (typeof value === 'function') {
if (typeof prevValue !== 'function' && prevValue !== null) {
// If we previously assigned a non-function type into this node, then
// remove it when switching to event listener mode.
if (name in (node: any)) {
(node: any)[name] = null;
} else if (node.hasAttribute(name)) {
node.removeAttribute(name);
}
}
// $FlowFixMe[incompatible-cast] value can't be casted to EventListener.
node.addEventListener(eventName, (value: EventListener), useCapture);
return;
}
}
if (name in (node: any)) {
(node: any)[name] = value;
return;
}
if (value === true) {
node.setAttribute(name, '');
return;
}
// From here, it's the same as any attribute
setValueForAttribute(node, name, value);
}