-
Notifications
You must be signed in to change notification settings - Fork 881
/
Copy pathSlot.tsx
160 lines (132 loc) · 5.13 KB
/
Slot.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
import * as React from 'react';
import { composeRefs } from '@radix-ui/react-compose-refs';
/* -------------------------------------------------------------------------------------------------
* Slot
* -----------------------------------------------------------------------------------------------*/
interface SlotProps extends React.HTMLAttributes<HTMLElement> {
children?: React.ReactNode;
}
const Slot = React.forwardRef<HTMLElement, SlotProps>((props, forwardedRef) => {
const { children, ...slotProps } = props;
const childrenArray = React.Children.toArray(children);
const slottable = childrenArray.find(isSlottable);
if (slottable) {
// the new element to render is the one passed as a child of `Slottable`
const newElement = slottable.props.children;
const newChildren = childrenArray.map((child) => {
if (child === slottable) {
// because the new element will be the one rendered, we are only interested
// in grabbing its children (`newElement.props.children`)
if (React.Children.count(newElement) > 1) return React.Children.only(null);
return React.isValidElement(newElement)
? (newElement.props as { children: React.ReactNode }).children
: null;
} else {
return child;
}
});
return (
<SlotClone {...slotProps} ref={forwardedRef}>
{React.isValidElement(newElement)
? React.cloneElement(newElement, undefined, newChildren)
: null}
</SlotClone>
);
}
return (
<SlotClone {...slotProps} ref={forwardedRef}>
{children}
</SlotClone>
);
});
Slot.displayName = 'Slot';
/* -------------------------------------------------------------------------------------------------
* SlotClone
* -----------------------------------------------------------------------------------------------*/
interface SlotCloneProps {
children: React.ReactNode;
}
const SlotClone = React.forwardRef<any, SlotCloneProps>((props, forwardedRef) => {
const { children, ...slotProps } = props;
if (React.isValidElement(children)) {
const childrenRef = getElementRef(children);
return React.cloneElement(children, {
...mergeProps(slotProps, children.props as AnyProps),
// @ts-ignore
ref: forwardedRef ? composeRefs(forwardedRef, childrenRef) : childrenRef,
});
}
return React.Children.count(children) > 1 ? React.Children.only(null) : null;
});
SlotClone.displayName = 'SlotClone';
/* -------------------------------------------------------------------------------------------------
* Slottable
* -----------------------------------------------------------------------------------------------*/
const Slottable = ({ children }: { children: React.ReactNode }) => {
return <>{children}</>;
};
/* ---------------------------------------------------------------------------------------------- */
type AnyProps = Record<string, any>;
function isSlottable(
child: React.ReactNode
): child is React.ReactElement<React.ComponentProps<typeof Slottable>, typeof Slottable> {
return React.isValidElement(child) && child.type === Slottable;
}
function mergeProps(slotProps: AnyProps, childProps: AnyProps) {
// all child props should override
const overrideProps = { ...childProps };
for (const propName in childProps) {
const slotPropValue = slotProps[propName];
const childPropValue = childProps[propName];
const isHandler = /^on[A-Z]/.test(propName);
if (isHandler) {
// if the handler exists on both, we compose them
if (slotPropValue && childPropValue) {
overrideProps[propName] = (...args: unknown[]) => {
childPropValue(...args);
slotPropValue(...args);
};
}
// but if it exists only on the slot, we use only this one
else if (slotPropValue) {
overrideProps[propName] = slotPropValue;
}
}
// if it's `style`, we merge them
else if (propName === 'style') {
overrideProps[propName] = { ...slotPropValue, ...childPropValue };
} else if (propName === 'className') {
overrideProps[propName] = [slotPropValue, childPropValue].filter(Boolean).join(' ');
}
}
return { ...slotProps, ...overrideProps };
}
// Before React 19 accessing `element.props.ref` will throw a warning and suggest using `element.ref`
// After React 19 accessing `element.ref` does the opposite.
// https://github.com/facebook/react/pull/28348
//
// Access the ref using the method that doesn't yield a warning.
function getElementRef(element: React.ReactElement) {
// React <=18 in DEV
let getter = Object.getOwnPropertyDescriptor(element.props, 'ref')?.get;
let mayWarn = getter && 'isReactWarning' in getter && getter.isReactWarning;
if (mayWarn) {
return (element as any).ref;
}
// React 19 in DEV
getter = Object.getOwnPropertyDescriptor(element, 'ref')?.get;
mayWarn = getter && 'isReactWarning' in getter && getter.isReactWarning;
if (mayWarn) {
return (element.props as { ref?: React.Ref<unknown> }).ref;
}
// Not DEV
return (element.props as { ref?: React.Ref<unknown> }).ref || (element as any).ref;
}
const Root = Slot;
export {
Slot,
Slottable,
//
Root,
};
export type { SlotProps };