-
Notifications
You must be signed in to change notification settings - Fork 2.8k
/
Copy pathslots.tsx
246 lines (223 loc) · 9.78 KB
/
slots.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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
import * as React from 'react';
import { mergeCss } from '@uifabric/merge-styles';
import { IStyle, ITheme } from '@fluentui/style-utilities';
import { getRTL, memoizeFunction } from '@uifabric/utilities';
import { assign } from './utilities';
import { IFactoryOptions } from './IComponent';
import {
ISlottableReactType,
ISlot,
ISlots,
ISlotDefinition,
ISlotFactory,
ISlotProp,
ISlottableProps,
ISlotOptions,
IDefaultSlotProps,
IProcessedSlotProps,
ValidProps,
ValidShorthand,
} from './ISlots';
/**
* This function is required for any module that uses slots.
*
* This function is a slot resolver that automatically evaluates slot functions to generate React elements.
* A byproduct of this resolver is that it removes slots from the React hierarchy by bypassing React.createElement.
*
* To use this function on a per-file basis, use the jsx directive targeting withSlots.
* This directive must be the FIRST LINE in the file to work correctly.
* Usage of this pragma also requires withSlots import statement.
*
* See React.createElement
*/
// Can't use typeof on React.createElement since it's overloaded. Approximate createElement's signature for now
// and widen as needed.
export function withSlots<P>(
type: ISlot<P> | React.FunctionComponent<P> | string,
props?: (React.Attributes & P) | null,
...children: React.ReactNode[]
): ReturnType<React.FunctionComponent<P>> {
const slotType = type as ISlot<P>;
if (slotType.isSlot) {
// Since we are bypassing createElement, use React.Children.toArray to make sure children are
// properly assigned keys.
// TODO: should this be mutating? does React mutate children subprop with createElement?
// TODO: will toArray clobber existing keys?
// TODO: React generates warnings because it doesn't detect hidden member _store that is set in createElement.
// Even children passed to createElement without keys don't generate this warning.
// Is there a better way to prevent slots from appearing in hierarchy? toArray doesn't address root issue.
children = React.Children.toArray(children);
// TODO: There is something weird going on here with children embedded in props vs. rest args.
// Comment out these lines to see. Make sure this function is doing the right things.
if (children.length === 0) {
return slotType(props);
}
return slotType({ ...(props as any), children });
} else {
// TODO: Are there some cases where children should NOT be spread? Also, spreading reraises perf question.
// Children had to be spread to avoid breaking KeytipData in Toggle.view:
// react-dom.development.js:18931 Uncaught TypeError: children is not a function
// Without spread, function child is a child array of one element
// TODO: is there a reason this can't be:
// return React.createElement.apply(this, arguments);
return React.createElement(type, props, ...children);
}
}
/**
* This function creates factories that render ouput depending on the user ISlotProp props passed in.
* @param DefaultComponent - Base component to render when not overridden by user props.
* @param options - Factory options, including defaultProp value for shorthand prop mapping.
* @returns ISlotFactory function used for rendering slots.
*/
export function createFactory<TProps extends ValidProps, TShorthandProp extends ValidShorthand = never>(
DefaultComponent: React.ComponentType<TProps>,
options: IFactoryOptions<TProps> = {},
): ISlotFactory<TProps, TShorthandProp> {
const { defaultProp = 'children' } = options;
const result: ISlotFactory<TProps, TShorthandProp> = (
componentProps,
userProps,
userSlotOptions,
defaultStyles,
theme,
) => {
// If they passed in raw JSX, just return that.
if (React.isValidElement(userProps)) {
return userProps;
}
const flattenedUserProps: TProps | undefined = _translateShorthand(defaultProp as string, userProps);
const finalProps = _constructFinalProps(defaultStyles, theme, componentProps, flattenedUserProps);
if (userSlotOptions) {
if (userSlotOptions.component) {
// TODO: Remove cast if possible. This cast is needed because TS errors on the intrinsic portion of ReactType.
// return <userSlotOptions.component {...finalProps} />;
const UserComponent = userSlotOptions.component as React.ComponentType<TProps>;
return <UserComponent {...finalProps} />;
}
if (userSlotOptions.render) {
return userSlotOptions.render(finalProps, DefaultComponent);
}
}
return <DefaultComponent {...finalProps} />;
};
return result;
}
/**
* Default factory for components without explicit factories.
*/
const defaultFactory = memoizeFunction(type => createFactory(type));
/**
* This function generates slots that can be used in JSX given a definition of slots and their corresponding types.
* @param userProps - Props as pass to component.
* @param slots - Slot definition object defining the default slot component for each slot.
* @returns A set of created slots that components can render in JSX.
*/
export function getSlots<TComponentProps extends ISlottableProps<TComponentSlots>, TComponentSlots>(
userProps: TComponentProps,
slots: ISlotDefinition<Required<TComponentSlots>>,
): ISlots<Required<TComponentSlots>> {
const result: ISlots<Required<TComponentSlots>> = {} as ISlots<Required<TComponentSlots>>;
// userProps already has default props mixed in by createComponent. Recast here to gain typing for this function.
const mixedProps = userProps as TComponentProps & IDefaultSlotProps<TComponentSlots>;
for (const name in slots) {
if (slots.hasOwnProperty(name)) {
// This closure method requires the use of withSlots to prevent unnecessary rerenders. This is because React
// detects each closure as a different component (since it is a new instance) from the previous one and then
// forces a rerender of the entire slot subtree. For now, the only way to avoid this is to use withSlots, which
// bypasses the call to React.createElement.
const slot: ISlots<Required<TComponentSlots>>[keyof TComponentSlots] = (componentProps, ...args: any[]) => {
if (args.length > 0) {
// If React.createElement is being incorrectly used with slots, there will be additional arguments.
// We can detect these additional arguments and error on their presence.
throw new Error('Any module using getSlots must use withSlots. Please see withSlots javadoc for more info.');
}
// TODO: having TS infer types here seems to cause infinite loop.
// use explicit types or casting to preserve typing if possible.
// TODO: this should be a lookup on TProps property instead of being TProps directly, which is probably
// causing the infinite loop
return _renderSlot<any, any, any>(
slots[name],
// TODO: this cast to any is hiding a relationship issue between the first two args
componentProps as any,
mixedProps[name],
mixedProps.slots && mixedProps.slots[name],
// _defaultStyles should always be present, but a check for existence is added to make view tests
// easier to use.
mixedProps._defaultStyles && mixedProps._defaultStyles[name],
(mixedProps as any).theme,
);
};
slot.isSlot = true;
result[name] = slot;
}
}
return result;
}
/**
* Helper function that translates shorthand as needed.
* @param defaultProp
* @param slotProps
*/
function _translateShorthand<TProps extends ValidProps, TShorthandProp extends ValidShorthand>(
defaultProp: string,
slotProps: ISlotProp<TProps, TShorthandProp>,
): TProps | undefined {
let transformedProps: TProps | undefined;
if (typeof slotProps === 'string' || typeof slotProps === 'number' || typeof slotProps === 'boolean') {
transformedProps = {
[defaultProp]: slotProps as any,
} as TProps;
} else {
transformedProps = slotProps as TProps;
}
return transformedProps;
}
/**
* Helper function that constructs final styles and props given a series of props ordered by increasing priority.
*/
function _constructFinalProps<TProps extends IProcessedSlotProps>(
defaultStyles: IStyle,
theme?: ITheme,
...allProps: (TProps | undefined)[]
): TProps {
const finalProps: TProps = {} as any;
const classNames: (string | undefined)[] = [];
for (const props of allProps) {
classNames.push(props && props.className);
assign(finalProps, props);
}
finalProps.className = mergeCss([defaultStyles, classNames], { rtl: getRTL(theme) });
return finalProps;
}
/**
* Render a slot given component and user props. Uses component factory if available, otherwise falls back
* to default factory.
* @param ComponentType Factory component type.
* @param componentProps The properties passed into slot from within the component.
* @param userProps The user properties passed in from outside of the component.
*/
function _renderSlot<
TSlotComponent extends ISlottableReactType<TSlotProps, TSlotShorthand>,
TSlotProps extends ValidProps,
TSlotShorthand extends ValidShorthand
>(
ComponentType: TSlotComponent,
componentProps: TSlotProps,
userProps: ISlotProp<TSlotProps, TSlotShorthand>,
slotOptions: ISlotOptions<TSlotProps> | undefined,
defaultStyles: IStyle,
theme?: ITheme,
): ReturnType<React.FunctionComponent> {
if (ComponentType.create !== undefined) {
return ComponentType.create(componentProps, userProps, slotOptions, defaultStyles);
} else {
// TODO: need to resolve typing / generic issues passing through memoizeFunction. for now, cast to 'unknown'
return ((defaultFactory(ComponentType) as unknown) as ISlotFactory<TSlotProps, TSlotShorthand>)(
componentProps,
userProps,
slotOptions,
defaultStyles,
theme,
);
}
}