@@ -13,7 +13,7 @@ import type {
13
13
Without ,
14
14
} from '@clerk/types' ;
15
15
import type { PropsWithChildren } from 'react' ;
16
- import React , { createElement } from 'react' ;
16
+ import React , { createContext , createElement , useContext } from 'react' ;
17
17
18
18
import {
19
19
organizationProfileLinkRenderedError ,
@@ -25,6 +25,7 @@ import {
25
25
userProfilePageRenderedError ,
26
26
} from '../errors/messages' ;
27
27
import type {
28
+ CustomPortalsRendererProps ,
28
29
MountProps ,
29
30
OpenProps ,
30
31
OrganizationProfileLinkProps ,
@@ -35,7 +36,12 @@ import type {
35
36
UserProfilePageProps ,
36
37
WithClerkProp ,
37
38
} from '../types' ;
38
- import { useOrganizationProfileCustomPages , useUserButtonCustomMenuItems , useUserProfileCustomPages } from '../utils' ;
39
+ import {
40
+ useOrganizationProfileCustomPages ,
41
+ useSanitizedChildren ,
42
+ useUserButtonCustomMenuItems ,
43
+ useUserProfileCustomPages ,
44
+ } from '../utils' ;
39
45
import { withClerk } from './withClerk' ;
40
46
41
47
type UserProfileExportType = typeof _UserProfile & {
@@ -49,10 +55,26 @@ type UserButtonExportType = typeof _UserButton & {
49
55
MenuItems : typeof MenuItems ;
50
56
Action : typeof MenuAction ;
51
57
Link : typeof MenuLink ;
58
+ /**
59
+ * The `<Outlet />` component can be used in conjunction with `asProvider` in order to control rendering
60
+ * of the `<OrganizationSwitcher />` without affecting its configuration or any custom pages
61
+ * that could be mounted
62
+ * @experimental This API is experimental and may change at any moment.
63
+ */
64
+ __experimental_Outlet : typeof UserButtonOutlet ;
52
65
} ;
53
66
54
- type UserButtonPropsWithoutCustomPages = Without < UserButtonProps , 'userProfileProps' > & {
67
+ type UserButtonPropsWithoutCustomPages = Without <
68
+ UserButtonProps ,
69
+ 'userProfileProps' | '__experimental_asStandalone'
70
+ > & {
55
71
userProfileProps ?: Pick < UserProfileProps , 'additionalOAuthScopes' | 'appearance' > ;
72
+ /**
73
+ * Adding `asProvider` will defer rendering until the `<Outlet />` component is mounted.
74
+ * @experimental This API is experimental and may change at any moment.
75
+ * @default undefined
76
+ */
77
+ __experimental_asProvider ?: boolean ;
56
78
} ;
57
79
58
80
type OrganizationProfileExportType = typeof _OrganizationProfile & {
@@ -63,10 +85,34 @@ type OrganizationProfileExportType = typeof _OrganizationProfile & {
63
85
type OrganizationSwitcherExportType = typeof _OrganizationSwitcher & {
64
86
OrganizationProfilePage : typeof OrganizationProfilePage ;
65
87
OrganizationProfileLink : typeof OrganizationProfileLink ;
88
+ /**
89
+ * The `<Outlet />` component can be used in conjunction with `asProvider` in order to control rendering
90
+ * of the `<OrganizationSwitcher />` without affecting its configuration or any custom pages
91
+ * that could be mounted
92
+ * @experimental This API is experimental and may change at any moment.
93
+ */
94
+ __experimental_Outlet : typeof OrganizationSwitcherOutlet ;
66
95
} ;
67
96
68
- type OrganizationSwitcherPropsWithoutCustomPages = Without < OrganizationSwitcherProps , 'organizationProfileProps' > & {
97
+ type OrganizationSwitcherPropsWithoutCustomPages = Without <
98
+ OrganizationSwitcherProps ,
99
+ 'organizationProfileProps' | '__experimental_asStandalone'
100
+ > & {
69
101
organizationProfileProps ?: Pick < OrganizationProfileProps , 'appearance' > ;
102
+ /**
103
+ * Adding `asProvider` will defer rendering until the `<Outlet />` component is mounted.
104
+ * @experimental This API is experimental and may change at any moment.
105
+ * @default undefined
106
+ */
107
+ __experimental_asProvider ?: boolean ;
108
+ } ;
109
+
110
+ const isMountProps = ( props : any ) : props is MountProps => {
111
+ return 'mount' in props ;
112
+ } ;
113
+
114
+ const isOpenProps = ( props : any ) : props is OpenProps => {
115
+ return 'open' in props ;
70
116
} ;
71
117
72
118
// README: <Portal/> should be a class pure component in order for mount and unmount
@@ -98,15 +144,9 @@ type OrganizationSwitcherPropsWithoutCustomPages = Without<OrganizationSwitcherP
98
144
99
145
// Portal.displayName = 'ClerkPortal';
100
146
101
- const isMountProps = ( props : any ) : props is MountProps => {
102
- return 'mount' in props ;
103
- } ;
104
-
105
- const isOpenProps = ( props : any ) : props is OpenProps => {
106
- return 'open' in props ;
107
- } ;
108
-
109
- class Portal extends React . PureComponent < MountProps | OpenProps > {
147
+ class Portal extends React . PureComponent <
148
+ PropsWithChildren < ( MountProps | OpenProps ) & { hideRootHtmlElement ?: boolean } >
149
+ > {
110
150
private portalRef = React . createRef < HTMLDivElement > ( ) ;
111
151
112
152
componentDidUpdate ( _prevProps : Readonly < MountProps | OpenProps > ) {
@@ -122,8 +162,11 @@ class Portal extends React.PureComponent<MountProps | OpenProps> {
122
162
// instead, we simply use the length of customPages to determine if it changed or not
123
163
const customPagesChanged = prevProps . customPages ?. length !== newProps . customPages ?. length ;
124
164
const customMenuItemsChanged = prevProps . customMenuItems ?. length !== newProps . customMenuItems ?. length ;
165
+
125
166
if ( ! isDeeplyEqual ( prevProps , newProps ) || customPagesChanged || customMenuItemsChanged ) {
126
- this . props . updateProps ( { node : this . portalRef . current , props : this . props . props } ) ;
167
+ if ( this . portalRef . current ) {
168
+ this . props . updateProps ( { node : this . portalRef . current , props : this . props . props } ) ;
169
+ }
127
170
}
128
171
}
129
172
@@ -151,18 +194,25 @@ class Portal extends React.PureComponent<MountProps | OpenProps> {
151
194
}
152
195
153
196
render ( ) {
197
+ const { hideRootHtmlElement = false } = this . props ;
154
198
return (
155
199
< >
156
- < div ref = { this . portalRef } />
157
- { isMountProps ( this . props ) &&
158
- this . props ?. customPagesPortals ?. map ( ( portal , index ) => createElement ( portal , { key : index } ) ) }
159
- { isMountProps ( this . props ) &&
160
- this . props ?. customMenuItemsPortals ?. map ( ( portal , index ) => createElement ( portal , { key : index } ) ) }
200
+ { ! hideRootHtmlElement && < div ref = { this . portalRef } /> }
201
+ { this . props . children }
161
202
</ >
162
203
) ;
163
204
}
164
205
}
165
206
207
+ const CustomPortalsRenderer = ( props : CustomPortalsRendererProps ) => {
208
+ return (
209
+ < >
210
+ { props ?. customPagesPortals ?. map ( ( portal , index ) => createElement ( portal , { key : index } ) ) }
211
+ { props ?. customMenuItemsPortals ?. map ( ( portal , index ) => createElement ( portal , { key : index } ) ) }
212
+ </ >
213
+ ) ;
214
+ } ;
215
+
166
216
export const SignIn = withClerk ( ( { clerk, ...props } : WithClerkProp < SignInProps > ) => {
167
217
return (
168
218
< Portal
@@ -204,8 +254,9 @@ const _UserProfile = withClerk(
204
254
unmount = { clerk . unmountUserProfile }
205
255
updateProps = { ( clerk as any ) . __unstable__updateProps }
206
256
props = { { ...props , customPages } }
207
- customPagesPortals = { customPagesPortals }
208
- />
257
+ >
258
+ < CustomPortalsRenderer customPagesPortals = { customPagesPortals } />
259
+ </ Portal >
209
260
) ;
210
261
} ,
211
262
'UserProfile' ,
@@ -216,21 +267,43 @@ export const UserProfile: UserProfileExportType = Object.assign(_UserProfile, {
216
267
Link : UserProfileLink ,
217
268
} ) ;
218
269
270
+ const UserButtonContext = createContext < MountProps > ( {
271
+ mount : ( ) => { } ,
272
+ unmount : ( ) => { } ,
273
+ updateProps : ( ) => { } ,
274
+ } ) ;
275
+
219
276
const _UserButton = withClerk (
220
277
( { clerk, ...props } : WithClerkProp < PropsWithChildren < UserButtonPropsWithoutCustomPages > > ) => {
221
- const { customPages, customPagesPortals } = useUserProfileCustomPages ( props . children ) ;
278
+ const { customPages, customPagesPortals } = useUserProfileCustomPages ( props . children , {
279
+ allowForAnyChildren : ! ! props . __experimental_asProvider ,
280
+ } ) ;
222
281
const userProfileProps = Object . assign ( props . userProfileProps || { } , { customPages } ) ;
223
282
const { customMenuItems, customMenuItemsPortals } = useUserButtonCustomMenuItems ( props . children ) ;
283
+ const sanitizedChildren = useSanitizedChildren ( props . children ) ;
284
+
285
+ const passableProps = {
286
+ mount : clerk . mountUserButton ,
287
+ unmount : clerk . unmountUserButton ,
288
+ updateProps : ( clerk as any ) . __unstable__updateProps ,
289
+ props : { ...props , userProfileProps, customMenuItems } ,
290
+ } ;
291
+ const portalProps = {
292
+ customPagesPortals : customPagesPortals ,
293
+ customMenuItemsPortals : customMenuItemsPortals ,
294
+ } ;
224
295
225
296
return (
226
- < Portal
227
- mount = { clerk . mountUserButton }
228
- unmount = { clerk . unmountUserButton }
229
- updateProps = { ( clerk as any ) . __unstable__updateProps }
230
- props = { { ...props , userProfileProps, customMenuItems } }
231
- customPagesPortals = { customPagesPortals }
232
- customMenuItemsPortals = { customMenuItemsPortals }
233
- />
297
+ < UserButtonContext . Provider value = { passableProps } >
298
+ < Portal
299
+ { ...passableProps }
300
+ hideRootHtmlElement = { ! ! props . __experimental_asProvider }
301
+ >
302
+ { /*This mimics the previous behaviour before asProvider existed*/ }
303
+ { props . __experimental_asProvider ? sanitizedChildren : null }
304
+ < CustomPortalsRenderer { ...portalProps } />
305
+ </ Portal >
306
+ </ UserButtonContext . Provider >
234
307
) ;
235
308
} ,
236
309
'UserButton' ,
@@ -251,12 +324,27 @@ export function MenuLink({ children }: PropsWithChildren<UserButtonLinkProps>) {
251
324
return < > { children } </ > ;
252
325
}
253
326
327
+ export function UserButtonOutlet ( outletProps : Without < UserButtonProps , 'userProfileProps' > ) {
328
+ const providerProps = useContext ( UserButtonContext ) ;
329
+
330
+ const portalProps = {
331
+ ...providerProps ,
332
+ props : {
333
+ ...providerProps . props ,
334
+ ...outletProps ,
335
+ } ,
336
+ } satisfies MountProps ;
337
+
338
+ return < Portal { ...portalProps } /> ;
339
+ }
340
+
254
341
export const UserButton : UserButtonExportType = Object . assign ( _UserButton , {
255
342
UserProfilePage,
256
343
UserProfileLink,
257
344
MenuItems,
258
345
Action : MenuAction ,
259
346
Link : MenuLink ,
347
+ __experimental_Outlet : UserButtonOutlet ,
260
348
} ) ;
261
349
262
350
export function OrganizationProfilePage ( { children } : PropsWithChildren < OrganizationProfilePageProps > ) {
@@ -278,8 +366,9 @@ const _OrganizationProfile = withClerk(
278
366
unmount = { clerk . unmountOrganizationProfile }
279
367
updateProps = { ( clerk as any ) . __unstable__updateProps }
280
368
props = { { ...props , customPages } }
281
- customPagesPortals = { customPagesPortals }
282
- />
369
+ >
370
+ < CustomPortalsRenderer customPagesPortals = { customPagesPortals } />
371
+ </ Portal >
283
372
) ;
284
373
} ,
285
374
'OrganizationProfile' ,
@@ -301,27 +390,68 @@ export const CreateOrganization = withClerk(({ clerk, ...props }: WithClerkProp<
301
390
) ;
302
391
} , 'CreateOrganization' ) ;
303
392
393
+ const OrganizationSwitcherContext = createContext < MountProps > ( {
394
+ mount : ( ) => { } ,
395
+ unmount : ( ) => { } ,
396
+ updateProps : ( ) => { } ,
397
+ } ) ;
398
+
304
399
const _OrganizationSwitcher = withClerk (
305
400
( { clerk, ...props } : WithClerkProp < PropsWithChildren < OrganizationSwitcherPropsWithoutCustomPages > > ) => {
306
- const { customPages, customPagesPortals } = useOrganizationProfileCustomPages ( props . children ) ;
401
+ const { customPages, customPagesPortals } = useOrganizationProfileCustomPages ( props . children , {
402
+ allowForAnyChildren : ! ! props . __experimental_asProvider ,
403
+ } ) ;
307
404
const organizationProfileProps = Object . assign ( props . organizationProfileProps || { } , { customPages } ) ;
405
+ const sanitizedChildren = useSanitizedChildren ( props . children ) ;
406
+
407
+ const passableProps = {
408
+ mount : clerk . mountOrganizationSwitcher ,
409
+ unmount : clerk . unmountOrganizationSwitcher ,
410
+ updateProps : ( clerk as any ) . __unstable__updateProps ,
411
+ props : { ...props , organizationProfileProps } ,
412
+ } ;
413
+
414
+ /**
415
+ * Prefetch organization list
416
+ */
417
+ clerk . __experimental_prefetchOrganizationSwitcher ( ) ;
308
418
309
419
return (
310
- < Portal
311
- mount = { clerk . mountOrganizationSwitcher }
312
- unmount = { clerk . unmountOrganizationSwitcher }
313
- updateProps = { ( clerk as any ) . __unstable__updateProps }
314
- props = { { ...props , organizationProfileProps } }
315
- customPagesPortals = { customPagesPortals }
316
- />
420
+ < OrganizationSwitcherContext . Provider value = { passableProps } >
421
+ < Portal
422
+ { ...passableProps }
423
+ hideRootHtmlElement = { ! ! props . __experimental_asProvider }
424
+ >
425
+ { /*This mimics the previous behaviour before asProvider existed*/ }
426
+ { props . __experimental_asProvider ? sanitizedChildren : null }
427
+ < CustomPortalsRenderer customPagesPortals = { customPagesPortals } />
428
+ </ Portal >
429
+ </ OrganizationSwitcherContext . Provider >
317
430
) ;
318
431
} ,
319
432
'OrganizationSwitcher' ,
320
433
) ;
321
434
435
+ export function OrganizationSwitcherOutlet (
436
+ outletProps : Without < OrganizationSwitcherProps , 'organizationProfileProps' > ,
437
+ ) {
438
+ const providerProps = useContext ( OrganizationSwitcherContext ) ;
439
+
440
+ const portalProps = {
441
+ ...providerProps ,
442
+ props : {
443
+ ...providerProps . props ,
444
+ ...outletProps ,
445
+ } ,
446
+ } satisfies MountProps ;
447
+
448
+ return < Portal { ...portalProps } /> ;
449
+ }
450
+
322
451
export const OrganizationSwitcher : OrganizationSwitcherExportType = Object . assign ( _OrganizationSwitcher , {
323
452
OrganizationProfilePage,
324
453
OrganizationProfileLink,
454
+ __experimental_Outlet : OrganizationSwitcherOutlet ,
325
455
} ) ;
326
456
327
457
export const OrganizationList = withClerk ( ( { clerk, ...props } : WithClerkProp < OrganizationListProps > ) => {
0 commit comments