-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
MergeProvider
#6610
Comments
Mind expanding a bit more on the exact setup you are using this MergeProvider for? I think it is fine to roll your own manual merge logic by pulling from the Provider above and then applying the merged props to another Provider below. We can definitely consider creating and exposing one from our end if there is popular demand for it. |
What I've got going on is my own custom implementation of Tag(Group), which utilizes the RAC Tag[Group,List]. The RAC Tag supports a slot of "remove" on the ButtonContext to enable the optional functionality of allowing a Tag to be removable. My custom Tag, wraps the RAC Tag, and wraps the MergeProvider around the RAC Tag children. This allows me to provide slot props to the context that get merged with the RAC ButtonContext slot props. Simplified implementation example: import { Button, ButtonContext, Tag } from 'react-aria-components';
const MyTag = ({ children, ...rest }) => {
const values = [
[ButtonContext, {
slots: {
remove: {
className: 'my-custom-tag--remove',
// any other Button props I want to merge in
}
}
}]
];
return (
<Tag {...rest} className="my-custom-tag">
{(renderProps) => (
<MergeProvider values={values}>
<div className="my-custom-tag-inner">
{typeof children === 'function' ? children(renderProps) : children}
</div>
</MergeProvider>
)}
</Tag>
);
}
<MyTag><Button slot="remove" /></MyTag> In this case My own version of import { type AsType } from '@cbc2/types';
import { clsx } from 'clsx';
import { type CSSProperties } from 'react';
import { mergeProps as mergePropsWithoutStyles } from 'react-aria';
import { composeRenderProps } from 'react-aria-components';
import {
type ClassNameRenderProps,
type RenderProps,
type StylePropRenderProps,
} from '../types';
type Props<T extends object> = AsType<T> | null | undefined;
/**
* Recursively process merging of all class name render props
*/
function processClassNameRenderProps<T extends RenderProps<object>>(
value: string,
renderProps: ClassNameRenderProps<object>,
...propsToMerge: Props<T>[]
): string {
if (!propsToMerge.length) {
return '';
}
const [props, ...rest] = propsToMerge;
return clsx(
value,
composeRenderProps<string, ClassNameRenderProps<object>, string>(
props?.className ?? '',
(prev) => processClassNameRenderProps(prev, renderProps, ...rest)
)(renderProps)
);
}
/**
* Compose class name render props to be processed and merged
*/
function mergeRenderClassNames<T extends RenderProps<object>>(
...propsToMerge: Props<T>[]
) {
return composeRenderProps<string, ClassNameRenderProps<object>, string>(
(renderProps) => renderProps.defaultClassName ?? '',
(prev, renderProps) =>
processClassNameRenderProps(prev, renderProps, ...propsToMerge)
);
}
/**
* Merge static class names
*/
function mergePlainClassNames<T extends RenderProps<object>>(
...propsToMerge: Props<T>[]
) {
return clsx(
propsToMerge.reduce<string[]>((acc, props) => {
if (typeof props?.className !== 'string') {
return acc;
}
return [...acc, props.className];
}, [])
);
}
/**
* Determine if a static or composed merge of class names is necesary based on the presence of functions
*/
function mergeClassNames<T extends RenderProps<object>>(
...propsToMerge: Props<T>[]
) {
const anyFunctions = propsToMerge.some(
(props) => typeof props?.className === 'function'
);
const anyPrimitives = propsToMerge.some(
(props) => typeof props?.className === 'string'
);
if (!anyFunctions && !anyPrimitives) {
return null;
}
return anyFunctions
? mergeRenderClassNames(...propsToMerge)
: mergePlainClassNames(...propsToMerge);
}
/**
* Recursively process merging of all style render props
*/
function processStyleRenderProps<T extends RenderProps<object>>(
value: CSSProperties,
renderProps: StylePropRenderProps<object>,
...propsToMerge: Props<T>[]
): CSSProperties {
if (!propsToMerge.length) {
return {};
}
const [props, ...rest] = propsToMerge;
return {
...value,
...composeRenderProps<
CSSProperties,
StylePropRenderProps<object>,
CSSProperties
>(props?.style ?? {}, (prev) =>
processStyleRenderProps(prev, renderProps, ...rest)
)(renderProps),
};
}
/**
* Compose style render props to be processed and merged
*/
function mergeRenderStyles<T extends RenderProps<object>>(
...propsToMerge: Props<T>[]
) {
return composeRenderProps<
CSSProperties,
StylePropRenderProps<object>,
CSSProperties
>(
(renderProps) => renderProps.defaultStyle ?? {},
(prev, renderProps) =>
processStyleRenderProps(prev, renderProps, ...propsToMerge)
);
}
/**
* Merge static styles
*/
function mergePlainStyles<T extends RenderProps<object>>(
...propsToMerge: Props<T>[]
) {
return propsToMerge.reduce<CSSProperties>((acc, props) => {
if (!props?.style) {
return acc;
}
return {
...acc,
...props.style,
};
}, {});
}
/**
* Determine if a static or composed merge of styles is necesary based on the presence of functions
*/
function mergeStyles<T extends RenderProps<object>>(
...propsToMerge: Props<T>[]
) {
const anyFunctions = propsToMerge.some(
(props) => typeof props?.style === 'function'
);
const anyObjects = propsToMerge.some(
(props) => typeof props?.style === 'object' && props.style != null
);
if (!anyFunctions && !anyObjects) {
return null;
}
return anyFunctions
? mergeRenderStyles(...propsToMerge)
: mergePlainStyles(...propsToMerge);
}
/**
* Extends the base margeProps functionality to also merge styles and handle class/style render props
*/
export function mergeProps<T extends object>(...propsToMerge: Props<T>[]): T {
const className = mergeClassNames(...propsToMerge);
const style = mergeStyles(...propsToMerge);
return {
...(mergePropsWithoutStyles(...propsToMerge) as T),
...(className ? { className } : {}),
...(style ? { style } : {}),
};
} |
It also makes it so that if you want to add in additional slots in the same context, you can do so without wiping out the slots from a higher version of the context. So, if I wanted to retain the remove slot and also add my own slot for a different opt in functionality, that would be possible too |
Provide a general summary of the feature here
It is desirable to be able to merge slot props onto an existing slot. In order to do so, you must extract the context and merge your props into the existing slot and then provide the updated context.
🤔 Expected Behavior?
MergeProvider
would consume the context of any context that it is provided and attempt to merge the new values in on top using themergeProps
util😯 Current Behavior
Only
Provider
exists and it doesn't support merging, only overriding💁 Possible Solution
I've created a rough first draft. Keep in mind that I'm using my own
mergeProps
that is built on top of the RACmergeProps
but replaces the behavior for merging ofclassName
andstyle
to support render props.🔦 Context
I'm implementing a custom component built on top of an RAC that currently implements the "remove" slot for a button. I want to maintain all of the RAC props for that slot, but I also want to add my own. If I were to just implement the
Provider
, then the context is overridden and I would lose (or have to completely reimplement) the RAC props. Instead, I'd like to implementMergeProvider
that will automatically merge props for slots that exist in both versions of the context💻 Examples
No response
🧢 Your Company/Team
No response
🕷 Tracking Issue
No response
The text was updated successfully, but these errors were encountered: