-
-
Notifications
You must be signed in to change notification settings - Fork 91
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[core] Make
mergeReactProps
work with non-native event handlers (#1440
- Loading branch information
1 parent
53c3dc8
commit e896408
Showing
2 changed files
with
153 additions
and
81 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,87 +1,140 @@ | ||
import type * as React from 'react'; | ||
import * as React from 'react'; | ||
import type { BaseUIEvent, WithBaseUIEvent } from './types'; | ||
|
||
/** | ||
* Merges multiple sets of React props such that their event handlers are called in sequence (the user's | ||
* before our internal ones), and allows the user to prevent the internal event handlers from being | ||
* executed by attaching a `preventBaseUIHandler` method. It also merges the `style` prop, whereby | ||
* the user's styles overwrite the internal ones. | ||
* @important **`className` and `ref` are not merged.** | ||
* @param externalProps the user's external props. | ||
* @param internalProps our own internal props. | ||
* Merges multiple sets of React props such that their event handlers are called in sequence | ||
* (the leftmost one being called first), and allows the user to prevent the subsequent event handlers from being | ||
* executed by attaching a `preventBaseUIHandler` method. | ||
* It also merges the `className` and `style` props, whereby the classes are concatenated | ||
* and the leftmost styles overwrite the subsequent ones. | ||
* @important **`ref` is not merged.** | ||
* @param props props to merge. | ||
* @returns the merged props. | ||
*/ | ||
export function mergeReactProps<T extends React.ElementType>( | ||
externalProps: WithBaseUIEvent<React.ComponentPropsWithRef<T>> | undefined, | ||
...internalProps: React.ComponentPropsWithRef<T>[] | ||
...props: (WithBaseUIEvent<React.ComponentPropsWithRef<T>> | undefined)[] | ||
): WithBaseUIEvent<React.ComponentPropsWithRef<T>> { | ||
let mergedInternalProps: WithBaseUIEvent<React.ComponentPropsWithRef<T>> = internalProps[0]; | ||
for (let i = 1; i < internalProps.length; i += 1) { | ||
mergedInternalProps = merge(mergedInternalProps, internalProps[i]); | ||
if (props.length === 0) { | ||
return {} as WithBaseUIEvent<React.ComponentPropsWithRef<T>>; | ||
} | ||
|
||
if (props.length === 1) { | ||
return props[0] ?? ({} as WithBaseUIEvent<React.ComponentPropsWithRef<T>>); | ||
} | ||
|
||
return merge(externalProps, mergedInternalProps as React.ComponentPropsWithRef<T>); | ||
let merged = merge(props[props.length - 2], props[props.length - 1]); | ||
|
||
for (let i = props.length - 3; i >= 0; i -= 1) { | ||
merged = merge(props[i], merged); | ||
} | ||
|
||
return merged ?? ({} as WithBaseUIEvent<React.ComponentPropsWithRef<T>>); | ||
} | ||
|
||
/** | ||
* Merges two sets of props. In case of conflicts, the external props take precedence. | ||
*/ | ||
function merge<T extends React.ElementType>( | ||
externalProps: WithBaseUIEvent<React.ComponentPropsWithRef<T>> | undefined, | ||
internalProps: React.ComponentPropsWithRef<T>, | ||
internalProps: WithBaseUIEvent<React.ComponentPropsWithRef<T>> | undefined, | ||
): WithBaseUIEvent<React.ComponentPropsWithRef<T>> { | ||
if (!externalProps) { | ||
if (!internalProps) { | ||
return {} as WithBaseUIEvent<React.ComponentPropsWithRef<T>>; | ||
} | ||
|
||
return internalProps; | ||
} | ||
|
||
if (!internalProps) { | ||
return externalProps; | ||
} | ||
|
||
return Object.entries(externalProps).reduce( | ||
(acc, [key, value]) => { | ||
if ( | ||
// This approach is more efficient than using a regex. | ||
key[0] === 'o' && | ||
key[1] === 'n' && | ||
key.charCodeAt(2) >= 65 /* A */ && | ||
key.charCodeAt(2) <= 90 /* Z */ && | ||
typeof value === 'function' | ||
) { | ||
acc[key] = (event: React.SyntheticEvent) => { | ||
let isPrevented = false; | ||
|
||
const theirHandler = value; | ||
const ourHandler = internalProps[key]; | ||
|
||
const baseUIEvent = event as BaseUIEvent<typeof event>; | ||
|
||
baseUIEvent.preventBaseUIHandler = () => { | ||
isPrevented = true; | ||
}; | ||
|
||
const result = theirHandler(baseUIEvent); | ||
|
||
if (!isPrevented) { | ||
ourHandler?.(baseUIEvent); | ||
} | ||
|
||
return result; | ||
}; | ||
} else if (key === 'style') { | ||
if (value || internalProps.style) { | ||
acc[key] = { ...internalProps.style, ...(value || {}) }; | ||
} | ||
} else if (key === 'className') { | ||
if (value) { | ||
if (internalProps.className) { | ||
// eslint-disable-next-line prefer-template | ||
acc[key] = value + ' ' + internalProps.className; | ||
} else { | ||
acc[key] = value; | ||
} | ||
} else { | ||
acc[key] = internalProps.className; | ||
} | ||
(mergedProps, [propName, externalPropValue]) => { | ||
if (isEventHandler(propName, externalPropValue)) { | ||
mergedProps[propName] = mergeEventHandlers(externalPropValue, internalProps[propName]); | ||
} else if (propName === 'style') { | ||
mergedProps[propName] = mergeStyles( | ||
externalPropValue as React.CSSProperties, | ||
internalProps.style, | ||
); | ||
} else if (propName === 'className') { | ||
mergedProps[propName] = mergeClassNames( | ||
externalPropValue as string, | ||
internalProps.className, | ||
); | ||
} else { | ||
acc[key] = value; | ||
mergedProps[propName] = externalPropValue; | ||
} | ||
|
||
return acc; | ||
return mergedProps; | ||
}, | ||
{ ...internalProps }, | ||
{ ...internalProps } as React.ComponentPropsWithRef<T>, | ||
); | ||
} | ||
|
||
function isEventHandler(key: string, value: unknown) { | ||
// This approach is more efficient than using a regex. | ||
const thirdCharCode = key.charCodeAt(2); | ||
return ( | ||
key[0] === 'o' && | ||
key[1] === 'n' && | ||
thirdCharCode >= 65 /* A */ && | ||
thirdCharCode <= 90 /* Z */ && | ||
typeof value === 'function' | ||
); | ||
} | ||
|
||
function mergeEventHandlers(theirHandler: Function, ourHandler: Function) { | ||
return (event: unknown) => { | ||
if (isSyntheticEvent(event)) { | ||
let isPrevented = false; | ||
const baseUIEvent = event as BaseUIEvent<typeof event>; | ||
|
||
baseUIEvent.preventBaseUIHandler = () => { | ||
isPrevented = true; | ||
}; | ||
|
||
const result = theirHandler(baseUIEvent); | ||
|
||
if (!isPrevented) { | ||
ourHandler?.(baseUIEvent); | ||
} | ||
|
||
return result; | ||
} | ||
|
||
const result = theirHandler(event); | ||
ourHandler?.(event); | ||
return result; | ||
}; | ||
} | ||
|
||
function mergeStyles( | ||
theirStyle: React.CSSProperties | undefined, | ||
ourStyle: React.CSSProperties | undefined, | ||
) { | ||
if (theirStyle || ourStyle) { | ||
return { ...ourStyle, ...theirStyle }; | ||
} | ||
|
||
return undefined; | ||
} | ||
|
||
function mergeClassNames(theirClassName: string | undefined, ourClassName: string | undefined) { | ||
if (theirClassName) { | ||
if (ourClassName) { | ||
// eslint-disable-next-line prefer-template | ||
return theirClassName + ' ' + ourClassName; | ||
} | ||
|
||
return theirClassName; | ||
} | ||
|
||
return ourClassName; | ||
} | ||
|
||
function isSyntheticEvent(event: unknown): event is React.SyntheticEvent { | ||
return event != null && typeof event === 'object' && 'nativeEvent' in event; | ||
} |