UNPKG

@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
"use strict"; 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; }