@base-ui/react
Version:
Base UI is a library of headless ('unstyled') React components and low-level hooks. You gain complete control over your app's CSS and accessibility features.
192 lines (185 loc) • 6.49 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.makeEventPreventable = makeEventPreventable;
exports.mergeClassNames = mergeClassNames;
exports.mergeProps = mergeProps;
exports.mergePropsN = mergePropsN;
var _mergeObjects = require("@base-ui/utils/mergeObjects");
const EMPTY_PROPS = {};
/* eslint-disable id-denylist */
/**
* Merges multiple sets of React props. It follows the Object.assign pattern where the rightmost object's fields overwrite
* the conflicting ones from others. This doesn't apply to event handlers, `className` and `style` props.
*
* Event handlers are merged and called in right-to-left order (rightmost handler executes first, leftmost last).
* For React synthetic events, the rightmost handler can prevent prior (left-positioned) handlers from executing
* by calling `event.preventBaseUIHandler()`. For non-synthetic events (custom events with primitive/object values),
* all handlers always execute without prevention capability.
*
* The `className` prop is merged by concatenating classes in right-to-left order (rightmost class appears first in the string).
* The `style` prop is merged with rightmost styles overwriting the prior ones.
*
* Props can either be provided as objects or as functions that take the previous props as an argument.
* The function will receive the merged props up to that point (going from left to right):
* so in the case of `(obj1, obj2, fn, obj3)`, `fn` will receive the merged props of `obj1` and `obj2`.
* The function is responsible for chaining event handlers if needed (i.e. we don't run the merge logic).
*
* Event handlers returned by the functions are not automatically prevented when `preventBaseUIHandler` is called.
* They must check `event.baseUIHandlerPrevented` themselves and bail out if it's true.
*
* @important **`ref` is not merged.**
* @param a Props object to merge.
* @param b Props object to merge. The function will overwrite conflicting props from `a`.
* @param c Props object to merge. The function will overwrite conflicting props from previous parameters.
* @param d Props object to merge. The function will overwrite conflicting props from previous parameters.
* @param e Props object to merge. The function will overwrite conflicting props from previous parameters.
* @returns The merged props.
* @public
*/
function mergeProps(a, b, c, d, e) {
// We need to mutably own `merged`
let merged = {
...resolvePropsGetter(a, EMPTY_PROPS)
};
if (b) {
merged = mergeOne(merged, b);
}
if (c) {
merged = mergeOne(merged, c);
}
if (d) {
merged = mergeOne(merged, d);
}
if (e) {
merged = mergeOne(merged, e);
}
return merged;
}
/* eslint-enable id-denylist */
/**
* Merges an arbitrary number of React props using the same logic as {@link mergeProps}.
* This function accepts an array of props instead of individual arguments.
*
* This has slightly lower performance than {@link mergeProps} due to accepting an array
* instead of a fixed number of arguments. Prefer {@link mergeProps} when merging 5 or
* fewer prop sets for better performance.
*
* @param props Array of props to merge.
* @returns The merged props.
* @see mergeProps
* @public
*/
function mergePropsN(props) {
if (props.length === 0) {
return EMPTY_PROPS;
}
if (props.length === 1) {
return resolvePropsGetter(props[0], EMPTY_PROPS);
}
// We need to mutably own `merged`
let merged = {
...resolvePropsGetter(props[0], EMPTY_PROPS)
};
for (let i = 1; i < props.length; i += 1) {
merged = mergeOne(merged, props[i]);
}
return merged;
}
function mergeOne(merged, inputProps) {
if (isPropsGetter(inputProps)) {
return inputProps(merged);
}
return mutablyMergeInto(merged, inputProps);
}
/**
* Merges two sets of props. In case of conflicts, the external props take precedence.
*/
function mutablyMergeInto(mergedProps, externalProps) {
if (!externalProps) {
return mergedProps;
}
// eslint-disable-next-line guard-for-in
for (const propName in externalProps) {
const externalPropValue = externalProps[propName];
switch (propName) {
case 'style':
{
mergedProps[propName] = (0, _mergeObjects.mergeObjects)(mergedProps.style, externalPropValue);
break;
}
case 'className':
{
mergedProps[propName] = mergeClassNames(mergedProps.className, externalPropValue);
break;
}
default:
{
if (isEventHandler(propName, externalPropValue)) {
mergedProps[propName] = mergeEventHandlers(mergedProps[propName], externalPropValue);
} else {
mergedProps[propName] = externalPropValue;
}
}
}
}
return mergedProps;
}
function isEventHandler(key, value) {
// This approach is more efficient than using a regex.
const code0 = key.charCodeAt(0);
const code1 = key.charCodeAt(1);
const code2 = key.charCodeAt(2);
return code0 === 111 /* o */ && code1 === 110 /* n */ && code2 >= 65 /* A */ && code2 <= 90 /* Z */ && (typeof value === 'function' || typeof value === 'undefined');
}
function isPropsGetter(inputProps) {
return typeof inputProps === 'function';
}
function resolvePropsGetter(inputProps, previousProps) {
if (isPropsGetter(inputProps)) {
return inputProps(previousProps);
}
return inputProps ?? EMPTY_PROPS;
}
function mergeEventHandlers(ourHandler, theirHandler) {
if (!theirHandler) {
return ourHandler;
}
if (!ourHandler) {
return theirHandler;
}
return event => {
if (isSyntheticEvent(event)) {
const baseUIEvent = event;
makeEventPreventable(baseUIEvent);
const result = theirHandler(baseUIEvent);
if (!baseUIEvent.baseUIHandlerPrevented) {
ourHandler?.(baseUIEvent);
}
return result;
}
const result = theirHandler(event);
ourHandler?.(event);
return result;
};
}
function makeEventPreventable(event) {
event.preventBaseUIHandler = () => {
event.baseUIHandlerPrevented = true;
};
return event;
}
function mergeClassNames(ourClassName, theirClassName) {
if (theirClassName) {
if (ourClassName) {
// eslint-disable-next-line prefer-template
return theirClassName + ' ' + ourClassName;
}
return theirClassName;
}
return ourClassName;
}
function isSyntheticEvent(event) {
return event != null && typeof event === 'object' && 'nativeEvent' in event;
}