From 3b240b7eb155ed4bdaefdb4e4b819a01a9573ea4 Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Mon, 20 May 2024 15:11:54 +0300
Subject: [PATCH 01/36] feat(clerk-js): Experimental controlled UserButton
---
.../src/ui/components/UserButton/UserButton.tsx | 4 +++-
packages/clerk-js/src/ui/elements/Popover.tsx | 2 ++
packages/clerk-js/src/ui/hooks/usePopover.ts | 15 +++++++++++++--
packages/types/src/clerk.ts | 4 ++++
4 files changed, 22 insertions(+), 3 deletions(-)
diff --git a/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx b/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx
index 941d8fa1da6..90ed3bc33d1 100644
--- a/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx
+++ b/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx
@@ -8,9 +8,11 @@ import { UserButtonPopover } from './UserButtonPopover';
import { UserButtonTrigger } from './UserButtonTrigger';
const _UserButton = withFloatingTree(() => {
- const { defaultOpen } = useUserButtonContext();
+ const { defaultOpen, __experimental_open, __experimental_onOpenChanged } = useUserButtonContext();
const { floating, reference, styles, toggle, isOpen, nodeId, context } = usePopover({
defaultOpen,
+ open: __experimental_open,
+ onOpenChanged: __experimental_onOpenChanged,
placement: 'bottom-end',
offset: 8,
});
diff --git a/packages/clerk-js/src/ui/elements/Popover.tsx b/packages/clerk-js/src/ui/elements/Popover.tsx
index d3f5ab43bf5..f79fc27732c 100644
--- a/packages/clerk-js/src/ui/elements/Popover.tsx
+++ b/packages/clerk-js/src/ui/elements/Popover.tsx
@@ -24,6 +24,7 @@ export const Popover = (props: PopoverProps) => {
context={context}
initialFocus={initialFocus}
order={order}
+ closeOnFocusOut={false}
>
<>{children}>
@@ -40,6 +41,7 @@ export const Popover = (props: PopoverProps) => {
context={context}
initialFocus={initialFocus}
order={order}
+ closeOnFocusOut={false}
>
<>{children}>
diff --git a/packages/clerk-js/src/ui/hooks/usePopover.ts b/packages/clerk-js/src/ui/hooks/usePopover.ts
index 21f65a2b7d9..1387e763f63 100644
--- a/packages/clerk-js/src/ui/hooks/usePopover.ts
+++ b/packages/clerk-js/src/ui/hooks/usePopover.ts
@@ -4,6 +4,8 @@ import React, { useEffect } from 'react';
type UsePopoverProps = {
defaultOpen?: boolean;
+ open?: boolean;
+ onOpenChanged?: (open: boolean | ((prevState: boolean) => boolean)) => void;
placement?: UseFloatingOptions['placement'];
offset?: Parameters[0];
shoudFlip?: boolean;
@@ -22,9 +24,17 @@ type UsePopoverProps = {
export type UsePopoverReturn = ReturnType;
export const usePopover = (props: UsePopoverProps = {}) => {
- const { bubbles = false, shoudFlip = true, outsidePress, adjustToReferenceWidth = false, referenceElement } = props;
- const [isOpen, setIsOpen] = React.useState(props.defaultOpen || false);
+ const { bubbles = true, shoudFlip = true, outsidePress, adjustToReferenceWidth = false, referenceElement } = props;
+ const [isOpen_internal, setIsOpen_internal] = React.useState(props.defaultOpen || false);
+
+ const isOpen = typeof props.open === 'undefined' ? isOpen_internal : props.open;
+ const setIsOpen = typeof props.onOpenChanged === 'undefined' ? setIsOpen_internal : props.onOpenChanged;
const nodeId = useFloatingNodeId();
+
+ if (typeof props.defaultOpen !== 'undefined' && typeof props.open !== 'undefined') {
+ console.warn('Both defaultOpen and open are set. `defaultOpen` will be ignored');
+ }
+
const { update, refs, strategy, x, y, context } = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
@@ -56,6 +66,7 @@ export const usePopover = (props: UsePopoverProps = {}) => {
useDismiss(context, {
bubbles,
outsidePress,
+ //outsidePress: typeof props.open === 'undefined' ? outsidePress : false,
});
useEffect(() => {
diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts
index 2b03f2b6735..a90719e91da 100644
--- a/packages/types/src/clerk.ts
+++ b/packages/types/src/clerk.ts
@@ -909,6 +909,10 @@ export type UserButtonProps = UserButtonProfileMode & {
* Controls the default state of the UserButton
*/
defaultOpen?: boolean;
+
+ __experimental_open?: boolean;
+
+ __experimental_onOpenChanged?: (open: boolean | ((prevState: boolean) => boolean)) => void;
/**
* Full URL or path to navigate after sign out is complete
* @deprecated Configure `afterSignOutUrl` as a global configuration, either in or in await Clerk.load()
From 14f15ee60fd1337c32d59acb65c8a968d62f6aff Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Mon, 20 May 2024 21:53:46 +0300
Subject: [PATCH 02/36] chore(clerk-js): Add changeset
---
.changeset/shaggy-kids-fail.md | 6 ++++++
1 file changed, 6 insertions(+)
create mode 100644 .changeset/shaggy-kids-fail.md
diff --git a/.changeset/shaggy-kids-fail.md b/.changeset/shaggy-kids-fail.md
new file mode 100644
index 00000000000..cd12a0ebc89
--- /dev/null
+++ b/.changeset/shaggy-kids-fail.md
@@ -0,0 +1,6 @@
+---
+'@clerk/clerk-js': minor
+'@clerk/types': minor
+---
+
+Add experimental support for a controlled UserButton.
From 92bbb9188f69d4ccf86b860be82ca69406a37319 Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Tue, 21 May 2024 12:07:26 +0300
Subject: [PATCH 03/36] feat(clerk-js): Experimental controlled
OrganizationSwitcher
---
.../OrganizationSwitcher/OrganizationSwitcher.tsx | 6 +++++-
packages/types/src/clerk.ts | 4 ++++
2 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx
index b066480dec2..5199113a71e 100644
--- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx
+++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx
@@ -1,6 +1,6 @@
import { useId } from 'react';
-import { AcceptedInvitationsProvider, withCoreUserGuard } from '../../contexts';
+import { AcceptedInvitationsProvider, useOrganizationSwitcherContext, withCoreUserGuard } from '../../contexts';
import { Flow } from '../../customizables';
import { Popover, withCardStateProvider, withFloatingTree } from '../../elements';
import { usePopover } from '../../hooks';
@@ -8,7 +8,11 @@ import { OrganizationSwitcherPopover } from './OrganizationSwitcherPopover';
import { OrganizationSwitcherTrigger } from './OrganizationSwitcherTrigger';
const _OrganizationSwitcher = withFloatingTree(() => {
+ const { defaultOpen, __experimental_open, __experimental_onOpenChanged } = useOrganizationSwitcherContext();
const { floating, reference, styles, toggle, isOpen, nodeId, context } = usePopover({
+ defaultOpen,
+ open: __experimental_open,
+ onOpenChanged: __experimental_onOpenChanged,
placement: 'bottom-start',
offset: 8,
});
diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts
index a90719e91da..1406459bbcb 100644
--- a/packages/types/src/clerk.ts
+++ b/packages/types/src/clerk.ts
@@ -973,6 +973,10 @@ export type OrganizationSwitcherProps = CreateOrganizationMode &
* Controls the default state of the OrganizationSwitcher
*/
defaultOpen?: boolean;
+
+ __experimental_open?: boolean;
+
+ __experimental_onOpenChanged?: (open: boolean | ((prevState: boolean) => boolean)) => void;
/**
* By default, users can switch between organization and their personal account.
* This option controls whether OrganizationSwitcher will include the user's personal account
From 0eb2bdad76093a29f001818aa81cb2548e3e7300 Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Fri, 24 May 2024 19:17:17 +0300
Subject: [PATCH 04/36] feat(clerk-js): Standalone OrganizationSwitcher
---
.../OrganizationSwitcher.tsx | 64 ++++++++++++-------
.../OrganizationSwitcherPopover.tsx | 5 +-
packages/types/src/clerk.ts | 3 +
3 files changed, 47 insertions(+), 25 deletions(-)
diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx
index 5199113a71e..bc39ce2e065 100644
--- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx
+++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx
@@ -1,4 +1,5 @@
-import { useId } from 'react';
+import type { ReactElement } from 'react';
+import { cloneElement, useId } from 'react';
import { AcceptedInvitationsProvider, useOrganizationSwitcherContext, withCoreUserGuard } from '../../contexts';
import { Flow } from '../../customizables';
@@ -7,8 +8,9 @@ import { usePopover } from '../../hooks';
import { OrganizationSwitcherPopover } from './OrganizationSwitcherPopover';
import { OrganizationSwitcherTrigger } from './OrganizationSwitcherTrigger';
-const _OrganizationSwitcher = withFloatingTree(() => {
- const { defaultOpen, __experimental_open, __experimental_onOpenChanged } = useOrganizationSwitcherContext();
+const OrganizationSwitcherWithFloatingTree = withFloatingTree<{ children: ReactElement }>(({ children }) => {
+ const { __experimental_onOpenChanged, __experimental_open, defaultOpen } = useOrganizationSwitcherContext();
+
const { floating, reference, styles, toggle, isOpen, nodeId, context } = usePopover({
defaultOpen,
open: __experimental_open,
@@ -19,34 +21,50 @@ const _OrganizationSwitcher = withFloatingTree(() => {
const switcherButtonMenuId = useId();
+ return (
+ <>
+
+
+ {cloneElement(children, {
+ id: switcherButtonMenuId,
+ close: toggle,
+ ref: floating,
+ style: styles,
+ })}
+
+ >
+ );
+});
+
+const _OrganizationSwitcher = () => {
+ const { __experimental_hideTrigger, __experimental_onActionEnded } = useOrganizationSwitcherContext();
+
return (
-
-
-
-
+ {__experimental_hideTrigger ? (
+
+ ) : (
+
+
+
+ )}
);
-});
+};
export const OrganizationSwitcher = withCoreUserGuard(withCardStateProvider(_OrganizationSwitcher));
diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherPopover.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherPopover.tsx
index 7edecbf80ee..5d65a28fcd3 100644
--- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherPopover.tsx
+++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherPopover.tsx
@@ -21,11 +21,12 @@ import { useRouter } from '../../router';
import type { PropsOfComponent, ThemableCssProp } from '../../styledSystem';
import { OrganizationActionList } from './OtherOrganizationActions';
-type OrganizationSwitcherPopoverProps = { close: () => void } & PropsOfComponent;
+type OrganizationSwitcherPopoverProps = { close?: () => void } & PropsOfComponent;
export const OrganizationSwitcherPopover = React.forwardRef(
(props, ref) => {
- const { close, ...rest } = props;
+ const { close: undefinedClose, ...rest } = props;
+ const close = () => undefinedClose?.();
const card = useCardState();
const { openOrganizationProfile, openCreateOrganization } = useClerk();
const { organization: currentOrg } = useOrganization();
diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts
index 1406459bbcb..7e4fbb7da28 100644
--- a/packages/types/src/clerk.ts
+++ b/packages/types/src/clerk.ts
@@ -977,6 +977,9 @@ export type OrganizationSwitcherProps = CreateOrganizationMode &
__experimental_open?: boolean;
__experimental_onOpenChanged?: (open: boolean | ((prevState: boolean) => boolean)) => void;
+
+ __experimental_hideTrigger?: boolean;
+ __experimental_onActionEnded?: () => void;
/**
* By default, users can switch between organization and their personal account.
* This option controls whether OrganizationSwitcher will include the user's personal account
From 74582c647a0989e29639799c00bd9333d22581b9 Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Wed, 28 Aug 2024 18:06:48 +0300
Subject: [PATCH 05/36] drop unwanted changes
---
.../OrganizationSwitcher.tsx | 19 ++++--------
.../ui/components/UserButton/UserButton.tsx | 6 ++--
packages/clerk-js/src/ui/hooks/usePopover.ts | 1 -
packages/types/src/clerk.ts | 31 +++++++++++--------
4 files changed, 27 insertions(+), 30 deletions(-)
diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx
index bc39ce2e065..f584f2f8ecb 100644
--- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx
+++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx
@@ -9,12 +9,11 @@ import { OrganizationSwitcherPopover } from './OrganizationSwitcherPopover';
import { OrganizationSwitcherTrigger } from './OrganizationSwitcherTrigger';
const OrganizationSwitcherWithFloatingTree = withFloatingTree<{ children: ReactElement }>(({ children }) => {
- const { __experimental_onOpenChanged, __experimental_open, defaultOpen } = useOrganizationSwitcherContext();
-
+ const { onOpenChanged, open, defaultOpen } = useOrganizationSwitcherContext();
const { floating, reference, styles, toggle, isOpen, nodeId, context } = usePopover({
defaultOpen,
- open: __experimental_open,
- onOpenChanged: __experimental_onOpenChanged,
+ open,
+ onOpenChanged,
placement: 'bottom-start',
offset: 8,
});
@@ -47,21 +46,15 @@ const OrganizationSwitcherWithFloatingTree = withFloatingTree<{ children: ReactE
});
const _OrganizationSwitcher = () => {
- const { __experimental_hideTrigger, __experimental_onActionEnded } = useOrganizationSwitcherContext();
-
return (
- {__experimental_hideTrigger ? (
-
- ) : (
-
-
-
- )}
+
+
+
);
diff --git a/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx b/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx
index 90ed3bc33d1..de8c20396e2 100644
--- a/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx
+++ b/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx
@@ -8,11 +8,11 @@ import { UserButtonPopover } from './UserButtonPopover';
import { UserButtonTrigger } from './UserButtonTrigger';
const _UserButton = withFloatingTree(() => {
- const { defaultOpen, __experimental_open, __experimental_onOpenChanged } = useUserButtonContext();
+ const { defaultOpen, open, onOpenChanged } = useUserButtonContext();
const { floating, reference, styles, toggle, isOpen, nodeId, context } = usePopover({
defaultOpen,
- open: __experimental_open,
- onOpenChanged: __experimental_onOpenChanged,
+ open,
+ onOpenChanged,
placement: 'bottom-end',
offset: 8,
});
diff --git a/packages/clerk-js/src/ui/hooks/usePopover.ts b/packages/clerk-js/src/ui/hooks/usePopover.ts
index 1387e763f63..fdc16f1ac4c 100644
--- a/packages/clerk-js/src/ui/hooks/usePopover.ts
+++ b/packages/clerk-js/src/ui/hooks/usePopover.ts
@@ -66,7 +66,6 @@ export const usePopover = (props: UsePopoverProps = {}) => {
useDismiss(context, {
bubbles,
outsidePress,
- //outsidePress: typeof props.open === 'undefined' ? outsidePress : false,
});
useEffect(() => {
diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts
index 7e4fbb7da28..1f144fd2e6d 100644
--- a/packages/types/src/clerk.ts
+++ b/packages/types/src/clerk.ts
@@ -906,13 +906,17 @@ export type UserButtonProps = UserButtonProfileMode & {
*/
showName?: boolean;
/**
- * Controls the default state of the UserButton
+ * Controls the default state of the UserButton.
*/
defaultOpen?: boolean;
-
- __experimental_open?: boolean;
-
- __experimental_onOpenChanged?: (open: boolean | ((prevState: boolean) => boolean)) => void;
+ /**
+ * Controls whether the popover is visible. Should be used in conjunction with onOpenChange.
+ */
+ open?: boolean;
+ /**
+ * Callback called when the open state of the popover changes.
+ */
+ onOpenChanged?: (open: boolean | ((prevState: boolean) => boolean)) => void;
/**
* Full URL or path to navigate after sign out is complete
* @deprecated Configure `afterSignOutUrl` as a global configuration, either in or in await Clerk.load()
@@ -970,16 +974,17 @@ type CreateOrganizationMode =
export type OrganizationSwitcherProps = CreateOrganizationMode &
OrganizationProfileMode & {
/**
- * Controls the default state of the OrganizationSwitcher
+ * Controls the default state of the OrganizationSwitcher.
*/
defaultOpen?: boolean;
-
- __experimental_open?: boolean;
-
- __experimental_onOpenChanged?: (open: boolean | ((prevState: boolean) => boolean)) => void;
-
- __experimental_hideTrigger?: boolean;
- __experimental_onActionEnded?: () => void;
+ /**
+ * Controls whether the popover is visible. Should be used in conjunction with onOpenChange.
+ */
+ open?: boolean;
+ /**
+ * Callback called when the open state of the popover changes.
+ */
+ onOpenChanged?: (open: boolean | ((prevState: boolean) => boolean)) => void;
/**
* By default, users can switch between organization and their personal account.
* This option controls whether OrganizationSwitcher will include the user's personal account
From d187ba22f0f413b89190401a5fcd23dec6a3dae2 Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Wed, 28 Aug 2024 19:17:18 +0300
Subject: [PATCH 06/36] Revert "drop unwanted changes"
This reverts commit d27b4ce69f69988f57ed34b775a4cd3176857056.
---
.../OrganizationSwitcher.tsx | 19 ++++++++----
.../ui/components/UserButton/UserButton.tsx | 6 ++--
packages/clerk-js/src/ui/hooks/usePopover.ts | 1 +
packages/types/src/clerk.ts | 31 ++++++++-----------
4 files changed, 30 insertions(+), 27 deletions(-)
diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx
index f584f2f8ecb..bc39ce2e065 100644
--- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx
+++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx
@@ -9,11 +9,12 @@ import { OrganizationSwitcherPopover } from './OrganizationSwitcherPopover';
import { OrganizationSwitcherTrigger } from './OrganizationSwitcherTrigger';
const OrganizationSwitcherWithFloatingTree = withFloatingTree<{ children: ReactElement }>(({ children }) => {
- const { onOpenChanged, open, defaultOpen } = useOrganizationSwitcherContext();
+ const { __experimental_onOpenChanged, __experimental_open, defaultOpen } = useOrganizationSwitcherContext();
+
const { floating, reference, styles, toggle, isOpen, nodeId, context } = usePopover({
defaultOpen,
- open,
- onOpenChanged,
+ open: __experimental_open,
+ onOpenChanged: __experimental_onOpenChanged,
placement: 'bottom-start',
offset: 8,
});
@@ -46,15 +47,21 @@ const OrganizationSwitcherWithFloatingTree = withFloatingTree<{ children: ReactE
});
const _OrganizationSwitcher = () => {
+ const { __experimental_hideTrigger, __experimental_onActionEnded } = useOrganizationSwitcherContext();
+
return (
-
-
-
+ {__experimental_hideTrigger ? (
+
+ ) : (
+
+
+
+ )}
);
diff --git a/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx b/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx
index de8c20396e2..90ed3bc33d1 100644
--- a/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx
+++ b/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx
@@ -8,11 +8,11 @@ import { UserButtonPopover } from './UserButtonPopover';
import { UserButtonTrigger } from './UserButtonTrigger';
const _UserButton = withFloatingTree(() => {
- const { defaultOpen, open, onOpenChanged } = useUserButtonContext();
+ const { defaultOpen, __experimental_open, __experimental_onOpenChanged } = useUserButtonContext();
const { floating, reference, styles, toggle, isOpen, nodeId, context } = usePopover({
defaultOpen,
- open,
- onOpenChanged,
+ open: __experimental_open,
+ onOpenChanged: __experimental_onOpenChanged,
placement: 'bottom-end',
offset: 8,
});
diff --git a/packages/clerk-js/src/ui/hooks/usePopover.ts b/packages/clerk-js/src/ui/hooks/usePopover.ts
index fdc16f1ac4c..1387e763f63 100644
--- a/packages/clerk-js/src/ui/hooks/usePopover.ts
+++ b/packages/clerk-js/src/ui/hooks/usePopover.ts
@@ -66,6 +66,7 @@ export const usePopover = (props: UsePopoverProps = {}) => {
useDismiss(context, {
bubbles,
outsidePress,
+ //outsidePress: typeof props.open === 'undefined' ? outsidePress : false,
});
useEffect(() => {
diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts
index 1f144fd2e6d..7e4fbb7da28 100644
--- a/packages/types/src/clerk.ts
+++ b/packages/types/src/clerk.ts
@@ -906,17 +906,13 @@ export type UserButtonProps = UserButtonProfileMode & {
*/
showName?: boolean;
/**
- * Controls the default state of the UserButton.
+ * Controls the default state of the UserButton
*/
defaultOpen?: boolean;
- /**
- * Controls whether the popover is visible. Should be used in conjunction with onOpenChange.
- */
- open?: boolean;
- /**
- * Callback called when the open state of the popover changes.
- */
- onOpenChanged?: (open: boolean | ((prevState: boolean) => boolean)) => void;
+
+ __experimental_open?: boolean;
+
+ __experimental_onOpenChanged?: (open: boolean | ((prevState: boolean) => boolean)) => void;
/**
* Full URL or path to navigate after sign out is complete
* @deprecated Configure `afterSignOutUrl` as a global configuration, either in or in await Clerk.load()
@@ -974,17 +970,16 @@ type CreateOrganizationMode =
export type OrganizationSwitcherProps = CreateOrganizationMode &
OrganizationProfileMode & {
/**
- * Controls the default state of the OrganizationSwitcher.
+ * Controls the default state of the OrganizationSwitcher
*/
defaultOpen?: boolean;
- /**
- * Controls whether the popover is visible. Should be used in conjunction with onOpenChange.
- */
- open?: boolean;
- /**
- * Callback called when the open state of the popover changes.
- */
- onOpenChanged?: (open: boolean | ((prevState: boolean) => boolean)) => void;
+
+ __experimental_open?: boolean;
+
+ __experimental_onOpenChanged?: (open: boolean | ((prevState: boolean) => boolean)) => void;
+
+ __experimental_hideTrigger?: boolean;
+ __experimental_onActionEnded?: () => void;
/**
* By default, users can switch between organization and their personal account.
* This option controls whether OrganizationSwitcher will include the user's personal account
From 7cd9c0b8fccf838b04b36e6a3ba1597149b7996f Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Wed, 28 Aug 2024 20:40:07 +0300
Subject: [PATCH 07/36] support hideTrigger in userButton
---
.../ui/components/UserButton/UserButton.tsx | 45 +++++++++++++------
.../UserButton/UserButtonPopover.tsx | 5 ++-
packages/types/src/clerk.ts | 3 ++
3 files changed, 37 insertions(+), 16 deletions(-)
diff --git a/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx b/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx
index 90ed3bc33d1..108f7a6236a 100644
--- a/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx
+++ b/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx
@@ -1,4 +1,4 @@
-import { useId } from 'react';
+import { cloneElement, type ReactElement, useId } from 'react';
import { useUserButtonContext, withCoreUserGuard } from '../../contexts';
import { Flow } from '../../customizables';
@@ -7,23 +7,21 @@ import { usePopover } from '../../hooks';
import { UserButtonPopover } from './UserButtonPopover';
import { UserButtonTrigger } from './UserButtonTrigger';
-const _UserButton = withFloatingTree(() => {
- const { defaultOpen, __experimental_open, __experimental_onOpenChanged } = useUserButtonContext();
+const UserButtonWithFloatingTree = withFloatingTree<{ children: ReactElement }>(({ children }) => {
+ const { __experimental_onOpenChanged, __experimental_open, defaultOpen } = useUserButtonContext();
+
const { floating, reference, styles, toggle, isOpen, nodeId, context } = usePopover({
defaultOpen,
open: __experimental_open,
onOpenChanged: __experimental_onOpenChanged,
- placement: 'bottom-end',
+ placement: 'bottom-start',
offset: 8,
});
const userButtonMenuId = useId();
return (
-
+ <>
{
context={context}
isOpen={isOpen}
>
-
+ {cloneElement(children, {
+ id: userButtonMenuId,
+ close: toggle,
+ ref: floating,
+ style: styles,
+ })}
+ >
+ );
+});
+
+const _UserButton = withFloatingTree(() => {
+ const { __experimental_hideTrigger, __experimental_onActionEnded } = useUserButtonContext();
+
+ return (
+
+ {__experimental_hideTrigger ? (
+
+ ) : (
+
+
+
+ )}
);
});
diff --git a/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx b/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx
index 10b737e30b9..b7d9ce9c613 100644
--- a/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx
+++ b/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx
@@ -9,10 +9,11 @@ import type { PropsOfComponent } from '../../styledSystem';
import { MultiSessionActions, SignOutAllActions, SingleSessionActions } from './SessionActions';
import { useMultisessionActions } from './useMultisessionActions';
-type UserButtonPopoverProps = { close: () => void } & PropsOfComponent;
+type UserButtonPopoverProps = { close?: () => void } & PropsOfComponent;
export const UserButtonPopover = React.forwardRef((props, ref) => {
- const { close, ...rest } = props;
+ const { close: optionalClose, ...rest } = props;
+ const close = () => optionalClose?.();
const { session } = useSession() as { session: ActiveSessionResource };
const { authConfig } = useEnvironment();
const { user } = useUser();
diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts
index 7e4fbb7da28..afa5bf85753 100644
--- a/packages/types/src/clerk.ts
+++ b/packages/types/src/clerk.ts
@@ -913,6 +913,9 @@ export type UserButtonProps = UserButtonProfileMode & {
__experimental_open?: boolean;
__experimental_onOpenChanged?: (open: boolean | ((prevState: boolean) => boolean)) => void;
+
+ __experimental_hideTrigger?: boolean;
+ __experimental_onActionEnded?: () => void;
/**
* Full URL or path to navigate after sign out is complete
* @deprecated Configure `afterSignOutUrl` as a global configuration, either in or in await Clerk.load()
From 75e093357f5e2f2d236f88cae8bf5af317332450 Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Wed, 28 Aug 2024 20:44:16 +0300
Subject: [PATCH 08/36] remove experimental open
---
.../OrganizationSwitcher/OrganizationSwitcher.tsx | 4 +---
.../src/ui/components/UserButton/UserButton.tsx | 4 +---
packages/clerk-js/src/ui/elements/Popover.tsx | 2 --
packages/clerk-js/src/ui/hooks/usePopover.ts | 13 ++-----------
packages/types/src/clerk.ts | 8 --------
5 files changed, 4 insertions(+), 27 deletions(-)
diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx
index bc39ce2e065..fc49d325ebf 100644
--- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx
+++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx
@@ -9,12 +9,10 @@ import { OrganizationSwitcherPopover } from './OrganizationSwitcherPopover';
import { OrganizationSwitcherTrigger } from './OrganizationSwitcherTrigger';
const OrganizationSwitcherWithFloatingTree = withFloatingTree<{ children: ReactElement }>(({ children }) => {
- const { __experimental_onOpenChanged, __experimental_open, defaultOpen } = useOrganizationSwitcherContext();
+ const { defaultOpen } = useOrganizationSwitcherContext();
const { floating, reference, styles, toggle, isOpen, nodeId, context } = usePopover({
defaultOpen,
- open: __experimental_open,
- onOpenChanged: __experimental_onOpenChanged,
placement: 'bottom-start',
offset: 8,
});
diff --git a/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx b/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx
index 108f7a6236a..be18299211b 100644
--- a/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx
+++ b/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx
@@ -8,12 +8,10 @@ import { UserButtonPopover } from './UserButtonPopover';
import { UserButtonTrigger } from './UserButtonTrigger';
const UserButtonWithFloatingTree = withFloatingTree<{ children: ReactElement }>(({ children }) => {
- const { __experimental_onOpenChanged, __experimental_open, defaultOpen } = useUserButtonContext();
+ const { defaultOpen } = useUserButtonContext();
const { floating, reference, styles, toggle, isOpen, nodeId, context } = usePopover({
defaultOpen,
- open: __experimental_open,
- onOpenChanged: __experimental_onOpenChanged,
placement: 'bottom-start',
offset: 8,
});
diff --git a/packages/clerk-js/src/ui/elements/Popover.tsx b/packages/clerk-js/src/ui/elements/Popover.tsx
index f79fc27732c..d3f5ab43bf5 100644
--- a/packages/clerk-js/src/ui/elements/Popover.tsx
+++ b/packages/clerk-js/src/ui/elements/Popover.tsx
@@ -24,7 +24,6 @@ export const Popover = (props: PopoverProps) => {
context={context}
initialFocus={initialFocus}
order={order}
- closeOnFocusOut={false}
>
<>{children}>
@@ -41,7 +40,6 @@ export const Popover = (props: PopoverProps) => {
context={context}
initialFocus={initialFocus}
order={order}
- closeOnFocusOut={false}
>
<>{children}>
diff --git a/packages/clerk-js/src/ui/hooks/usePopover.ts b/packages/clerk-js/src/ui/hooks/usePopover.ts
index 1387e763f63..12f0d20678b 100644
--- a/packages/clerk-js/src/ui/hooks/usePopover.ts
+++ b/packages/clerk-js/src/ui/hooks/usePopover.ts
@@ -4,8 +4,6 @@ import React, { useEffect } from 'react';
type UsePopoverProps = {
defaultOpen?: boolean;
- open?: boolean;
- onOpenChanged?: (open: boolean | ((prevState: boolean) => boolean)) => void;
placement?: UseFloatingOptions['placement'];
offset?: Parameters[0];
shoudFlip?: boolean;
@@ -24,17 +22,10 @@ type UsePopoverProps = {
export type UsePopoverReturn = ReturnType;
export const usePopover = (props: UsePopoverProps = {}) => {
- const { bubbles = true, shoudFlip = true, outsidePress, adjustToReferenceWidth = false, referenceElement } = props;
- const [isOpen_internal, setIsOpen_internal] = React.useState(props.defaultOpen || false);
-
- const isOpen = typeof props.open === 'undefined' ? isOpen_internal : props.open;
- const setIsOpen = typeof props.onOpenChanged === 'undefined' ? setIsOpen_internal : props.onOpenChanged;
+ const { bubbles = false, shoudFlip = true, outsidePress, adjustToReferenceWidth = false, referenceElement } = props;
+ const [isOpen, setIsOpen] = React.useState(props.defaultOpen || false);
const nodeId = useFloatingNodeId();
- if (typeof props.defaultOpen !== 'undefined' && typeof props.open !== 'undefined') {
- console.warn('Both defaultOpen and open are set. `defaultOpen` will be ignored');
- }
-
const { update, refs, strategy, x, y, context } = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts
index afa5bf85753..7b017d16dec 100644
--- a/packages/types/src/clerk.ts
+++ b/packages/types/src/clerk.ts
@@ -910,10 +910,6 @@ export type UserButtonProps = UserButtonProfileMode & {
*/
defaultOpen?: boolean;
- __experimental_open?: boolean;
-
- __experimental_onOpenChanged?: (open: boolean | ((prevState: boolean) => boolean)) => void;
-
__experimental_hideTrigger?: boolean;
__experimental_onActionEnded?: () => void;
/**
@@ -977,10 +973,6 @@ export type OrganizationSwitcherProps = CreateOrganizationMode &
*/
defaultOpen?: boolean;
- __experimental_open?: boolean;
-
- __experimental_onOpenChanged?: (open: boolean | ((prevState: boolean) => boolean)) => void;
-
__experimental_hideTrigger?: boolean;
__experimental_onActionEnded?: () => void;
/**
From d6f58e3a099d78a531e8680b8889fa345712ed5e Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Wed, 28 Aug 2024 20:53:38 +0300
Subject: [PATCH 09/36] update naming and write jsdoc
---
.../OrganizationSwitcher.tsx | 6 ++--
.../ui/components/UserButton/UserButton.tsx | 6 ++--
packages/types/src/clerk.ts | 34 ++++++++++++++++---
3 files changed, 36 insertions(+), 10 deletions(-)
diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx
index fc49d325ebf..d6295c92120 100644
--- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx
+++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx
@@ -45,7 +45,7 @@ const OrganizationSwitcherWithFloatingTree = withFloatingTree<{ children: ReactE
});
const _OrganizationSwitcher = () => {
- const { __experimental_hideTrigger, __experimental_onActionEnded } = useOrganizationSwitcherContext();
+ const { __experimental_standalone, __experimental_onDismiss } = useOrganizationSwitcherContext();
return (
{
sx={{ display: 'inline-flex' }}
>
- {__experimental_hideTrigger ? (
-
+ {__experimental_standalone ? (
+
) : (
diff --git a/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx b/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx
index be18299211b..e87cb48ae19 100644
--- a/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx
+++ b/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx
@@ -44,15 +44,15 @@ const UserButtonWithFloatingTree = withFloatingTree<{ children: ReactElement }>(
});
const _UserButton = withFloatingTree(() => {
- const { __experimental_hideTrigger, __experimental_onActionEnded } = useUserButtonContext();
+ const { __experimental_standalone, __experimental_onDismiss } = useUserButtonContext();
return (
- {__experimental_hideTrigger ? (
-
+ {__experimental_standalone ? (
+
) : (
diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts
index 7b017d16dec..2742a6258d1 100644
--- a/packages/types/src/clerk.ts
+++ b/packages/types/src/clerk.ts
@@ -910,8 +910,22 @@ export type UserButtonProps = UserButtonProfileMode & {
*/
defaultOpen?: boolean;
- __experimental_hideTrigger?: boolean;
- __experimental_onActionEnded?: () => void;
+ /**
+ * If true the UserButton will only render the popover.
+ * Enables developers to implement a custom dialog.
+ * @experimental This API is experimental and may change at any moment.
+ * @default false
+ */
+ __experimental_standalone?: boolean;
+
+ /**
+ * Notifies the caller that it's safe to unmount UserButton.
+ * It only fires when used in conjunction with `__experimental_standalone`.
+ * @experimental This API is experimental and may change at any moment.
+ * @default undefined
+ */
+ __experimental_onDismiss?: () => void;
+
/**
* Full URL or path to navigate after sign out is complete
* @deprecated Configure `afterSignOutUrl` as a global configuration, either in or in await Clerk.load()
@@ -972,9 +986,21 @@ export type OrganizationSwitcherProps = CreateOrganizationMode &
* Controls the default state of the OrganizationSwitcher
*/
defaultOpen?: boolean;
+ /**
+ * If true the OrganizationSwitcher will only render the popover.
+ * Enables developers to implement a custom dialog.
+ * @experimental This API is experimental and may change at any moment.
+ * @default false
+ */
+ __experimental_standalone?: boolean;
- __experimental_hideTrigger?: boolean;
- __experimental_onActionEnded?: () => void;
+ /**
+ * Notifies the caller that it's safe to unmount OrganizationSwitcher.
+ * It only fires when used in conjunction with `__experimental_standalone`.
+ * @experimental This API is experimental and may change at any moment.
+ * @default undefined
+ */
+ __experimental_onDismiss?: () => void;
/**
* By default, users can switch between organization and their personal account.
* This option controls whether OrganizationSwitcher will include the user's personal account
From 2f690803e1dab20cca76fd5a4013a2ccf43123fe Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Wed, 28 Aug 2024 20:58:18 +0300
Subject: [PATCH 10/36] minor cleanup
---
packages/clerk-js/src/ui/components/UserButton/UserButton.tsx | 2 +-
packages/clerk-js/src/ui/hooks/usePopover.ts | 2 --
2 files changed, 1 insertion(+), 3 deletions(-)
diff --git a/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx b/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx
index e87cb48ae19..bf6ac843dc2 100644
--- a/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx
+++ b/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx
@@ -12,7 +12,7 @@ const UserButtonWithFloatingTree = withFloatingTree<{ children: ReactElement }>(
const { floating, reference, styles, toggle, isOpen, nodeId, context } = usePopover({
defaultOpen,
- placement: 'bottom-start',
+ placement: 'bottom-end',
offset: 8,
});
diff --git a/packages/clerk-js/src/ui/hooks/usePopover.ts b/packages/clerk-js/src/ui/hooks/usePopover.ts
index 12f0d20678b..21f65a2b7d9 100644
--- a/packages/clerk-js/src/ui/hooks/usePopover.ts
+++ b/packages/clerk-js/src/ui/hooks/usePopover.ts
@@ -25,7 +25,6 @@ export const usePopover = (props: UsePopoverProps = {}) => {
const { bubbles = false, shoudFlip = true, outsidePress, adjustToReferenceWidth = false, referenceElement } = props;
const [isOpen, setIsOpen] = React.useState(props.defaultOpen || false);
const nodeId = useFloatingNodeId();
-
const { update, refs, strategy, x, y, context } = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
@@ -57,7 +56,6 @@ export const usePopover = (props: UsePopoverProps = {}) => {
useDismiss(context, {
bubbles,
outsidePress,
- //outsidePress: typeof props.open === 'undefined' ? outsidePress : false,
});
useEffect(() => {
From 22691cbfa67f73864e924279e545eb24f2304cf8 Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Fri, 30 Aug 2024 20:05:52 +0300
Subject: [PATCH 11/36] add unit tests
---
.../__tests__/OrganizationSwitcher.test.tsx | 47 ++++++++++++++++++-
.../UserButton/__tests__/UserButton.test.tsx | 28 +++++++++++
2 files changed, 74 insertions(+), 1 deletion(-)
diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx
index 60a9fb997ba..2ee55c9210a 100644
--- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx
+++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx
@@ -1,5 +1,6 @@
import type { MembershipRole } from '@clerk/types';
import { describe } from '@jest/globals';
+import { waitFor } from '@testing-library/react';
import { act, render } from '../../../../testUtils';
import { bindCreateFixtures } from '../../../utils/test/createFixtures';
@@ -119,10 +120,54 @@ describe('OrganizationSwitcher', () => {
props.setProps({ hidePersonal: true });
const { getByText, getByRole, userEvent } = render( , { wrapper });
- await userEvent.click(getByRole('button'));
+ await userEvent.click(getByRole('button', { name: 'Open organization switcher' }));
expect(getByText('Create organization')).toBeInTheDocument();
});
+ it('renders organization switcher popover as standalone', async () => {
+ const { wrapper, props } = await createFixtures(f => {
+ f.withOrganizations();
+ f.withUser({ email_addresses: ['test@clerk.com'], create_organization_enabled: true });
+ });
+ props.setProps({
+ __experimental_standalone: true,
+ });
+ const { getByText, queryByRole } = render( , { wrapper });
+ await waitFor(() => {
+ expect(queryByRole('button', { name: 'Open organization switcher' })).toBeNull();
+ expect(getByText('Personal account')).toBeInTheDocument();
+ });
+ });
+
+ it('calls onDismiss when "Manage Organization" is clicked', async () => {
+ const { wrapper, fixtures, props } = await createFixtures(f => {
+ f.withOrganizations();
+ f.withUser({
+ email_addresses: ['test@clerk.com'],
+ organization_memberships: [{ name: 'Org1', role: 'basic_member' }],
+ create_organization_enabled: true,
+ });
+ });
+
+ const onDismiss = jest.fn();
+ props.setProps({
+ __experimental_standalone: true,
+ __experimental_onDismiss: onDismiss,
+ });
+
+ fixtures.clerk.organization?.getRoles.mockRejectedValue(null);
+ fixtures.clerk.user?.getOrganizationMemberships.mockResolvedValueOnce([]);
+
+ const { getByRole, userEvent, queryByRole, getByText } = render( , { wrapper });
+ await waitFor(() => {
+ expect(queryByRole('button', { name: 'Open organization switcher' })).toBeNull();
+ expect(getByText('Personal account')).toBeInTheDocument();
+ });
+ await userEvent.click(getByRole('menuitem', { name: 'Create organization' }));
+ expect(fixtures.clerk.openCreateOrganization).toHaveBeenCalled();
+ expect(onDismiss).toHaveBeenCalledTimes(1);
+ });
+
it('lists all organizations the user belongs to', async () => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
diff --git a/packages/clerk-js/src/ui/components/UserButton/__tests__/UserButton.test.tsx b/packages/clerk-js/src/ui/components/UserButton/__tests__/UserButton.test.tsx
index f9f2bd3142c..ac13dc57add 100644
--- a/packages/clerk-js/src/ui/components/UserButton/__tests__/UserButton.test.tsx
+++ b/packages/clerk-js/src/ui/components/UserButton/__tests__/UserButton.test.tsx
@@ -21,6 +21,34 @@ describe('UserButton', () => {
expect(queryByRole('button')).not.toBeNull();
});
+ it('renders popover as standalone when there is a user', async () => {
+ const { wrapper, props } = await createFixtures(f => {
+ f.withUser({ email_addresses: ['test@clerk.com'] });
+ });
+ props.setProps({
+ __experimental_standalone: true,
+ });
+ const { getByText, queryByRole } = render( , { wrapper });
+ expect(queryByRole('button', { name: 'Open user button' })).toBeNull();
+ getByText('Manage account');
+ });
+
+ it('calls onDismiss when it is used as standalone', async () => {
+ const { wrapper, props, fixtures } = await createFixtures(f => {
+ f.withUser({ email_addresses: ['test@clerk.com'] });
+ });
+ const onDismiss = jest.fn();
+ props.setProps({
+ __experimental_standalone: true,
+ __experimental_onDismiss: onDismiss,
+ });
+ const { getByText, userEvent, queryByRole } = render( , { wrapper });
+ expect(queryByRole('button', { name: 'Open user button' })).toBeNull();
+ await userEvent.click(getByText('Manage account'));
+ expect(fixtures.clerk.openUserProfile).toHaveBeenCalled();
+ expect(onDismiss).toHaveBeenCalledTimes(1);
+ });
+
it('opens the user button popover when clicked', async () => {
const { wrapper } = await createFixtures(f => {
f.withUser({
From 08a1b3ec08230b6b9bb06d0c7c93ad655f5e917f Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Fri, 30 Aug 2024 20:07:53 +0300
Subject: [PATCH 12/36] update changeset
---
.changeset/shaggy-kids-fail.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/.changeset/shaggy-kids-fail.md b/.changeset/shaggy-kids-fail.md
index cd12a0ebc89..d24776a1cc7 100644
--- a/.changeset/shaggy-kids-fail.md
+++ b/.changeset/shaggy-kids-fail.md
@@ -3,4 +3,5 @@
'@clerk/types': minor
---
-Add experimental support for a controlled UserButton.
+Add experimental support for a standalone mode for UserButton and OrganizationSwitcher.
+When `__experimental_standalone: true` the component will not render its trigger and instead it will render only the contents of the popover in place.
From 57e39d90f058cd7858c2948f2a3b67df7fe23b53 Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Wed, 4 Sep 2024 17:49:38 +0300
Subject: [PATCH 13/36] wip
---
.../src/ui/components/UserButton/UserButton.tsx | 10 ++++++----
.../src/ui/components/UserButton/UserButtonPopover.tsx | 6 +++++-
.../src/ui/contexts/ClerkUIComponentsContext.tsx | 4 ++++
packages/react/src/components/uiComponents.tsx | 7 +++++++
packages/types/src/clerk.ts | 4 ++++
5 files changed, 26 insertions(+), 5 deletions(-)
diff --git a/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx b/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx
index bf6ac843dc2..98efe1669ff 100644
--- a/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx
+++ b/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx
@@ -43,8 +43,8 @@ const UserButtonWithFloatingTree = withFloatingTree<{ children: ReactElement }>(
);
});
-const _UserButton = withFloatingTree(() => {
- const { __experimental_standalone, __experimental_onDismiss } = useUserButtonContext();
+const _UserButton = () => {
+ const { __experimental_standalone, __experimental_onDismiss, __experimental_open } = useUserButtonContext();
return (
{
sx={{ display: 'inline-flex' }}
>
{__experimental_standalone ? (
-
+ __experimental_open ? (
+
+ ) : null
) : (
@@ -60,6 +62,6 @@ const _UserButton = withFloatingTree(() => {
)}
);
-});
+};
export const UserButton = withCoreUserGuard(withCardStateProvider(_UserButton));
diff --git a/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx b/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx
index b7d9ce9c613..e9c32b6c1a0 100644
--- a/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx
+++ b/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx
@@ -25,7 +25,11 @@ export const UserButtonPopover = React.forwardRef
diff --git a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx
index 3d064bb273b..b54f06d0759 100644
--- a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx
+++ b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx
@@ -227,6 +227,8 @@ export const useUserProfileContext = (): UserProfileContextType => {
throw new Error('Clerk: useUserProfileContext called outside of the mounted UserProfile component.');
}
+ console.log('----- profile', customPages);
+
const pages = useMemo(() => {
return createUserProfileCustomPages(customPages || [], clerk);
}, [customPages]);
@@ -265,6 +267,8 @@ export const useUserButtonContext = () => {
const { displayConfig } = useEnvironment();
const options = useOptions();
+ console.log(ctx?.userProfileProps?.customPages);
+
if (componentName !== 'UserButton') {
throw new Error('Clerk: useUserButtonContext called outside of the mounted UserButton component.');
}
diff --git a/packages/react/src/components/uiComponents.tsx b/packages/react/src/components/uiComponents.tsx
index 1f68a59d898..9e1aa9da3bc 100644
--- a/packages/react/src/components/uiComponents.tsx
+++ b/packages/react/src/components/uiComponents.tsx
@@ -111,6 +111,7 @@ class Portal extends React.PureComponent {
private portalRef = React.createRef();
componentDidUpdate(_prevProps: Readonly) {
+ console.log('-----Did update');
if (!isMountProps(_prevProps) || !isMountProps(this.props)) {
return;
}
@@ -123,13 +124,17 @@ class Portal extends React.PureComponent {
// instead, we simply use the length of customPages to determine if it changed or not
const customPagesChanged = prevProps.customPages?.length !== newProps.customPages?.length;
const customMenuItemsChanged = prevProps.customMenuItems?.length !== newProps.customMenuItems?.length;
+
+ console.log(prevProps, newProps, isDeeplyEqual(prevProps, newProps));
if (!isDeeplyEqual(prevProps, newProps) || customPagesChanged || customMenuItemsChanged) {
+ console.log('update');
this.props.updateProps({ node: this.portalRef.current, props: this.props.props });
}
}
componentDidMount() {
if (this.portalRef.current) {
+ console.log('---- props on mount', this.props.props);
if (isMountProps(this.props)) {
this.props.mount(this.portalRef.current, this.props.props);
}
@@ -223,6 +228,8 @@ const _UserButton = withClerk(
const userProfileProps = Object.assign(props.userProfileProps || {}, { customPages });
const { customMenuItems, customMenuItemsPortals } = useUserButtonCustomMenuItems(props.children);
+ console.log('---- _UserButton', props, userProfileProps);
+
return (
void;
+ __experimental_open?: boolean;
+
+ __experimental_onOpenChanged?: (open: boolean | ((prevState: boolean) => boolean)) => void;
+
/**
* Full URL or path to navigate after sign out is complete
* @deprecated Configure `afterSignOutUrl` as a global configuration, either in or in await Clerk.load()
From 640d7f4fe5dde2b4f42a0defbb512c2600b95854 Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Thu, 5 Sep 2024 20:33:48 +0300
Subject: [PATCH 14/36] poc state
---
.../ui/components/UserButton/UserButton.tsx | 13 +-
.../UserButton/UserButtonPopover.tsx | 6 +-
packages/clerk-js/src/ui/hooks/usePopover.ts | 12 +-
.../react/src/components/uiComponents.tsx | 181 +++++++++++++++---
.../react/src/utils/useCustomMenuItems.tsx | 3 +-
packages/react/src/utils/useCustomPages.tsx | 4 +-
packages/types/src/clerk.ts | 23 ++-
7 files changed, 198 insertions(+), 44 deletions(-)
diff --git a/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx b/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx
index 98efe1669ff..fe6a234e021 100644
--- a/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx
+++ b/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx
@@ -44,16 +44,21 @@ const UserButtonWithFloatingTree = withFloatingTree<{ children: ReactElement }>(
});
const _UserButton = () => {
- const { __experimental_standalone, __experimental_onDismiss, __experimental_open } = useUserButtonContext();
+ const { __experimental_asStandalone } = useUserButtonContext();
return (
- {__experimental_standalone ? (
- __experimental_open ? (
-
+ {/*{__experimental_standalone ? (*/}
+ {/* __experimental_open ? (*/}
+ {/* */}
+ {/* ) : null*/}
+ {/* )*/}
+ {__experimental_asStandalone ? (
+ __experimental_asStandalone.open ? (
+
) : null
) : (
diff --git a/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx b/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx
index e9c32b6c1a0..dbaf7b8e260 100644
--- a/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx
+++ b/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx
@@ -9,11 +9,13 @@ import type { PropsOfComponent } from '../../styledSystem';
import { MultiSessionActions, SignOutAllActions, SingleSessionActions } from './SessionActions';
import { useMultisessionActions } from './useMultisessionActions';
-type UserButtonPopoverProps = { close?: () => void } & PropsOfComponent;
+type UserButtonPopoverProps = {
+ close?: (open: boolean | ((prevState: boolean) => boolean)) => void;
+} & PropsOfComponent;
export const UserButtonPopover = React.forwardRef((props, ref) => {
const { close: optionalClose, ...rest } = props;
- const close = () => optionalClose?.();
+ const close = () => optionalClose?.(false);
const { session } = useSession() as { session: ActiveSessionResource };
const { authConfig } = useEnvironment();
const { user } = useUser();
diff --git a/packages/clerk-js/src/ui/hooks/usePopover.ts b/packages/clerk-js/src/ui/hooks/usePopover.ts
index 21f65a2b7d9..5cf7ce4e4d3 100644
--- a/packages/clerk-js/src/ui/hooks/usePopover.ts
+++ b/packages/clerk-js/src/ui/hooks/usePopover.ts
@@ -4,6 +4,8 @@ import React, { useEffect } from 'react';
type UsePopoverProps = {
defaultOpen?: boolean;
+ open?: boolean;
+ onOpenChanged?: (open: boolean | ((prevState: boolean) => boolean)) => void;
placement?: UseFloatingOptions['placement'];
offset?: Parameters[0];
shoudFlip?: boolean;
@@ -23,8 +25,16 @@ export type UsePopoverReturn = ReturnType;
export const usePopover = (props: UsePopoverProps = {}) => {
const { bubbles = false, shoudFlip = true, outsidePress, adjustToReferenceWidth = false, referenceElement } = props;
- const [isOpen, setIsOpen] = React.useState(props.defaultOpen || false);
+ const [isOpen_internal, setIsOpen_internal] = React.useState(props.defaultOpen || false);
+
+ const isOpen = typeof props.open === 'undefined' ? isOpen_internal : props.open;
+ const setIsOpen = typeof props.onOpenChanged === 'undefined' ? setIsOpen_internal : props.onOpenChanged;
const nodeId = useFloatingNodeId();
+
+ if (typeof props.defaultOpen !== 'undefined' && typeof props.open !== 'undefined') {
+ console.warn('Both defaultOpen and open are set. `defaultOpen` will be ignored');
+ }
+
const { update, refs, strategy, x, y, context } = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
diff --git a/packages/react/src/components/uiComponents.tsx b/packages/react/src/components/uiComponents.tsx
index 9e1aa9da3bc..726807c2a4a 100644
--- a/packages/react/src/components/uiComponents.tsx
+++ b/packages/react/src/components/uiComponents.tsx
@@ -14,7 +14,7 @@ import type {
Without,
} from '@clerk/types';
import type { PropsWithChildren } from 'react';
-import React, { createElement } from 'react';
+import React, { createContext, createElement, useContext } from 'react';
import {
organizationProfileLinkRenderedError,
@@ -50,6 +50,7 @@ type UserButtonExportType = typeof _UserButton & {
MenuItems: typeof MenuItems;
Action: typeof MenuAction;
Link: typeof MenuLink;
+ Body: () => React.JSX.Element;
};
type UserButtonPropsWithoutCustomPages = Without & {
@@ -107,11 +108,25 @@ const isOpenProps = (props: any): props is OpenProps => {
return 'open' in props;
};
-class Portal extends React.PureComponent {
+const CustomPortalsRenderer = (props: MountProps) => {
+ if (!isMountProps(props)) {
+ return null;
+ }
+
+ return (
+ <>
+ {props?.customPagesPortals?.map((portal, index) => createElement(portal, { key: index }))}
+ {props?.customMenuItemsPortals?.map((portal, index) => createElement(portal, { key: index }))}
+ >
+ );
+};
+
+class Portal extends React.PureComponent<
+ PropsWithChildren<(MountProps | OpenProps) & { renderHtmlElement?: boolean }>
+> {
private portalRef = React.createRef();
componentDidUpdate(_prevProps: Readonly) {
- console.log('-----Did update');
if (!isMountProps(_prevProps) || !isMountProps(this.props)) {
return;
}
@@ -127,14 +142,19 @@ class Portal extends React.PureComponent {
console.log(prevProps, newProps, isDeeplyEqual(prevProps, newProps));
if (!isDeeplyEqual(prevProps, newProps) || customPagesChanged || customMenuItemsChanged) {
- console.log('update');
- this.props.updateProps({ node: this.portalRef.current, props: this.props.props });
+ // if (!this.props.props.withOutlet) {
+ if (this.portalRef.current) {
+ this.props.updateProps({ node: this.portalRef.current, props: this.props.props });
+ }
+ // }
}
}
componentDidMount() {
+ // if (this.props.props.withOutlet) {
+ // return;
+ // }
if (this.portalRef.current) {
- console.log('---- props on mount', this.props.props);
if (isMountProps(this.props)) {
this.props.mount(this.portalRef.current, this.props.props);
}
@@ -146,6 +166,9 @@ class Portal extends React.PureComponent {
}
componentWillUnmount() {
+ // if (this.props.props.withOutlet) {
+ // return;
+ // }
if (this.portalRef.current) {
if (isMountProps(this.props)) {
this.props.unmount(this.portalRef.current);
@@ -157,18 +180,92 @@ class Portal extends React.PureComponent {
}
render() {
+ const { renderHtmlElement = true } = this.props;
return (
<>
-
- {isMountProps(this.props) &&
- this.props?.customPagesPortals?.map((portal, index) => createElement(portal, { key: index }))}
- {isMountProps(this.props) &&
- this.props?.customMenuItemsPortals?.map((portal, index) => createElement(portal, { key: index }))}
+ {/*{!this.props.props.withOutlet &&
}*/}
+ {renderHtmlElement &&
}
+ {/*{isMountProps(this.props) &&*/}
+ {/* this.props?.customPagesPortals?.map((portal, index) => createElement(portal, { key: index }))}*/}
+ {/*{isMountProps(this.props) &&*/}
+ {/* this.props?.customMenuItemsPortals?.map((portal, index) => createElement(portal, { key: index }))}*/}
+
+ {this.props.children}
+
+ {/*{this.props.props.withOutlet*/}
+ {/* ? React.Children.map(this.props.props.children, (child, index) => {*/}
+ {/* return child;*/}
+ {/* // Clone each child and pass additional props*/}
+ {/* return React.cloneElement(child, {*/}
+ {/* key: index, // always set a unique key when mapping*/}
+ {/* // additionalProp: `Value ${index + 1}`, // adding new props or modifying existing ones*/}
+ {/* ...this.props,*/}
+ {/* });*/}
+ {/* })*/}
+ {/* : null}*/}
>
);
}
}
+// class Portal2 extends React.PureComponent {
+// private portalRef = React.createRef();
+//
+// componentDidUpdate(_prevProps: Readonly) {
+// if (!isMountProps(_prevProps) || !isMountProps(this.props)) {
+// return;
+// }
+// console.log('portal2 update');
+//
+// // Remove children and customPages from props before comparing
+// // children might hold circular references which deepEqual can't handle
+// // and the implementation of customPages or customMenuItems relies on props getting new references
+// const prevProps = without(_prevProps.props, 'customPages', 'customMenuItems', 'children');
+// const newProps = without(this.props.props, 'customPages', 'customMenuItems', 'children');
+// // instead, we simply use the length of customPages to determine if it changed or not
+// const customPagesChanged = prevProps.customPages?.length !== newProps.customPages?.length;
+// const customMenuItemsChanged = prevProps.customMenuItems?.length !== newProps.customMenuItems?.length;
+//
+// console.log(prevProps, newProps, isDeeplyEqual(prevProps, newProps));
+// if (!isDeeplyEqual(prevProps, newProps) || customPagesChanged || customMenuItemsChanged) {
+// this.props.updateProps({ node: this.portalRef.current, props: this.props.props });
+// }
+// }
+//
+// componentDidMount() {
+// console.log('portal2 mounted');
+// if (this.portalRef.current) {
+// if (isMountProps(this.props)) {
+// this.props.mount(this.portalRef.current, this.props.props);
+// }
+//
+// if (isOpenProps(this.props)) {
+// this.props.open(this.props.props);
+// }
+// }
+// }
+//
+// componentWillUnmount() {
+// console.log('portal2 unmounted');
+// if (this.portalRef.current) {
+// if (isMountProps(this.props)) {
+// this.props.unmount(this.portalRef.current);
+// }
+// if (isOpenProps(this.props)) {
+// this.props.close();
+// }
+// }
+// }
+//
+// render() {
+// return (
+// <>
+//
+// >
+// );
+// }
+// }
+
export const SignIn = withClerk(({ clerk, ...props }: WithClerkProp) => {
return (
);
}, 'SignUp');
-export function UserProfilePage({ children }: PropsWithChildren) {
+export function UserProfilePage(_: PropsWithChildren) {
logErrorInDevMode(userProfilePageRenderedError);
- return <>{children}>;
+ return null;
}
-export function UserProfileLink({ children }: PropsWithChildren) {
+export function UserProfileLink(_: PropsWithChildren) {
logErrorInDevMode(userProfileLinkRenderedError);
- return <>{children}>;
+ return null;
}
const _UserProfile = withClerk(
@@ -222,41 +319,64 @@ export const UserProfile: UserProfileExportType = Object.assign(_UserProfile, {
Link: UserProfileLink,
});
+// @ts-ignore
+const UserButtonContext = createContext({});
+
const _UserButton = withClerk(
({ clerk, ...props }: WithClerkProp>) => {
const { customPages, customPagesPortals } = useUserProfileCustomPages(props.children);
const userProfileProps = Object.assign(props.userProfileProps || {}, { customPages });
const { customMenuItems, customMenuItemsPortals } = useUserButtonCustomMenuItems(props.children);
- console.log('---- _UserButton', props, userProfileProps);
+ const passableProps = {
+ mount: clerk.mountUserButton,
+ unmount: clerk.unmountUserButton,
+ updateProps: (clerk as any).__unstable__updateProps,
+ props: { ...props, userProfileProps, customMenuItems },
+ customPagesPortals: customPagesPortals,
+ customMenuItemsPortals: customMenuItemsPortals,
+ };
return (
-
+
+
+ {props.children}
+
+
+
);
},
'UserButton',
);
-export function MenuItems({ children }: PropsWithChildren) {
+export function MenuItems(_: PropsWithChildren) {
logErrorInDevMode(userButtonMenuItemsRenderedError);
- return <>{children}>;
+ return null;
}
-export function MenuAction({ children }: PropsWithChildren) {
+export function MenuAction(_: PropsWithChildren) {
logErrorInDevMode(userButtonMenuActionRenderedError);
- return <>{children}>;
+ return null;
}
-export function MenuLink({ children }: PropsWithChildren) {
+export function MenuLink(_: PropsWithChildren) {
logErrorInDevMode(userButtonMenuLinkRenderedError);
- return <>{children}>;
+ return null;
+}
+
+export function UserButtonOutlet() {
+ const props = useContext(UserButtonContext);
+ return ;
}
export const UserButton: UserButtonExportType = Object.assign(_UserButton, {
@@ -265,6 +385,7 @@ export const UserButton: UserButtonExportType = Object.assign(_UserButton, {
MenuItems,
Action: MenuAction,
Link: MenuLink,
+ Body: UserButtonOutlet as () => React.JSX.Element,
});
export const __experimental_UserVerification = withClerk(
diff --git a/packages/react/src/utils/useCustomMenuItems.tsx b/packages/react/src/utils/useCustomMenuItems.tsx
index 239a1e0bbf8..30b8a741c8d 100644
--- a/packages/react/src/utils/useCustomMenuItems.tsx
+++ b/packages/react/src/utils/useCustomMenuItems.tsx
@@ -6,7 +6,6 @@ import React from 'react';
import { MenuAction, MenuItems, MenuLink, UserProfileLink, UserProfilePage } from '../components/uiComponents';
import {
customMenuItemsIgnoredComponent,
- userButtonIgnoredComponent,
userButtonMenuItemLinkWrongProps,
userButtonMenuItemsActionWrongsProps,
} from '../errors/messages';
@@ -60,7 +59,7 @@ const useCustomMenuItems = ({
!isThatComponent(child, UserProfilePageComponent)
) {
if (child) {
- logErrorInDevMode(userButtonIgnoredComponent);
+ // logErrorInDevMode(userButtonIgnoredComponent);
}
return;
}
diff --git a/packages/react/src/utils/useCustomPages.tsx b/packages/react/src/utils/useCustomPages.tsx
index 4274fe34fca..140a21c5751 100644
--- a/packages/react/src/utils/useCustomPages.tsx
+++ b/packages/react/src/utils/useCustomPages.tsx
@@ -10,7 +10,7 @@ import {
UserProfileLink,
UserProfilePage,
} from '../components/uiComponents';
-import { customLinkWrongProps, customPagesIgnoredComponent, customPageWrongProps } from '../errors/messages';
+import { customLinkWrongProps, customPageWrongProps } from '../errors/messages';
import type { UserProfilePageProps } from '../types';
import { isThatComponent } from './componentValidation';
import type { UseCustomElementPortalParams, UseCustomElementPortalReturn } from './useCustomElementPortal';
@@ -67,7 +67,7 @@ const useCustomPages = ({
!isThatComponent(child, MenuItemsComponent)
) {
if (child) {
- logErrorInDevMode(customPagesIgnoredComponent(componentName));
+ // logErrorInDevMode(customPagesIgnoredComponent(componentName));
}
return;
}
diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts
index aa9ca6fcf93..79af737d913 100644
--- a/packages/types/src/clerk.ts
+++ b/packages/types/src/clerk.ts
@@ -983,6 +983,11 @@ export type UserButtonProps = UserButtonProfileMode & {
*/
defaultOpen?: boolean;
+ __experimental_asStandalone?: {
+ open: boolean;
+ onOpenChanged: (open: boolean | ((prevState: boolean) => boolean)) => void;
+ };
+
/**
* If true the UserButton will only render the popover.
* Enables developers to implement a custom dialog.
@@ -999,9 +1004,21 @@ export type UserButtonProps = UserButtonProfileMode & {
*/
__experimental_onDismiss?: () => void;
- __experimental_open?: boolean;
-
- __experimental_onOpenChanged?: (open: boolean | ((prevState: boolean) => boolean)) => void;
+ // /**
+ // * The controlled open state of the popover. When defined the trigger will not be
+ // * Should be used in conjunction with `__experimental_open`.
+ // * @experimental This API is experimental and may change at any moment.
+ // * @default undefined
+ // */
+ // __experimental_open?: boolean;
+ //
+ // /**
+ // * Event handler called when the open state of the dialog changes.
+ // * It only fires when used in conjunction with `__experimental_open`.
+ // * @experimental This API is experimental and may change at any moment.
+ // * @default undefined
+ // */
+ // __experimental_onOpenChanged?: (open: boolean | ((prevState: boolean) => boolean)) => void;
/**
* Full URL or path to navigate after sign out is complete
From 4988ea4742587d63b9df1d2a07375889ed0ef10c Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Thu, 5 Sep 2024 20:51:36 +0300
Subject: [PATCH 15/36] ready for snapshot
---
.../OrganizationSwitcher.tsx | 8 +-
.../OrganizationSwitcherPopover.tsx | 6 +-
.../ui/components/UserButton/UserButton.tsx | 5 -
.../react/src/components/uiComponents.tsx | 189 +++++++-----------
packages/types/src/clerk.ts | 75 ++++---
5 files changed, 114 insertions(+), 169 deletions(-)
diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx
index d6295c92120..60661213674 100644
--- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx
+++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx
@@ -45,7 +45,7 @@ const OrganizationSwitcherWithFloatingTree = withFloatingTree<{ children: ReactE
});
const _OrganizationSwitcher = () => {
- const { __experimental_standalone, __experimental_onDismiss } = useOrganizationSwitcherContext();
+ const { __experimental_asStandalone } = useOrganizationSwitcherContext();
return (
{
sx={{ display: 'inline-flex' }}
>
- {__experimental_standalone ? (
-
+ {__experimental_asStandalone ? (
+ __experimental_asStandalone.open ? (
+
+ ) : null
) : (
diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherPopover.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherPopover.tsx
index 5d65a28fcd3..7c3711a174d 100644
--- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherPopover.tsx
+++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherPopover.tsx
@@ -21,12 +21,14 @@ import { useRouter } from '../../router';
import type { PropsOfComponent, ThemableCssProp } from '../../styledSystem';
import { OrganizationActionList } from './OtherOrganizationActions';
-type OrganizationSwitcherPopoverProps = { close?: () => void } & PropsOfComponent;
+type OrganizationSwitcherPopoverProps = {
+ close?: (open: boolean | ((prevState: boolean) => boolean)) => void;
+} & PropsOfComponent;
export const OrganizationSwitcherPopover = React.forwardRef(
(props, ref) => {
const { close: undefinedClose, ...rest } = props;
- const close = () => undefinedClose?.();
+ const close = () => undefinedClose?.(false);
const card = useCardState();
const { openOrganizationProfile, openCreateOrganization } = useClerk();
const { organization: currentOrg } = useOrganization();
diff --git a/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx b/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx
index fe6a234e021..f079674a4d9 100644
--- a/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx
+++ b/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx
@@ -51,11 +51,6 @@ const _UserButton = () => {
flow='userButton'
sx={{ display: 'inline-flex' }}
>
- {/*{__experimental_standalone ? (*/}
- {/* __experimental_open ? (*/}
- {/* */}
- {/* ) : null*/}
- {/* )*/}
{__experimental_asStandalone ? (
__experimental_asStandalone.open ? (
diff --git a/packages/react/src/components/uiComponents.tsx b/packages/react/src/components/uiComponents.tsx
index 726807c2a4a..fcff7529f8a 100644
--- a/packages/react/src/components/uiComponents.tsx
+++ b/packages/react/src/components/uiComponents.tsx
@@ -65,12 +65,21 @@ type OrganizationProfileExportType = typeof _OrganizationProfile & {
type OrganizationSwitcherExportType = typeof _OrganizationSwitcher & {
OrganizationProfilePage: typeof OrganizationProfilePage;
OrganizationProfileLink: typeof OrganizationProfileLink;
+ Body: () => React.JSX.Element;
};
type OrganizationSwitcherPropsWithoutCustomPages = Without & {
organizationProfileProps?: Pick;
};
+const isMountProps = (props: any): props is MountProps => {
+ return 'mount' in props;
+};
+
+const isOpenProps = (props: any): props is OpenProps => {
+ return 'open' in props;
+};
+
// README: should be a class pure component in order for mount and unmount
// lifecycle props to be invoked correctly. Replacing the class component with a
// functional component wrapped with a React.memo is not identical to the original
@@ -100,29 +109,8 @@ type OrganizationSwitcherPropsWithoutCustomPages = Without {
- return 'mount' in props;
-};
-
-const isOpenProps = (props: any): props is OpenProps => {
- return 'open' in props;
-};
-
-const CustomPortalsRenderer = (props: MountProps) => {
- if (!isMountProps(props)) {
- return null;
- }
-
- return (
- <>
- {props?.customPagesPortals?.map((portal, index) => createElement(portal, { key: index }))}
- {props?.customMenuItemsPortals?.map((portal, index) => createElement(portal, { key: index }))}
- >
- );
-};
-
class Portal extends React.PureComponent<
- PropsWithChildren<(MountProps | OpenProps) & { renderHtmlElement?: boolean }>
+ PropsWithChildren<(MountProps | OpenProps) & { hideRootHtmlElement?: boolean }>
> {
private portalRef = React.createRef();
@@ -142,18 +130,13 @@ class Portal extends React.PureComponent<
console.log(prevProps, newProps, isDeeplyEqual(prevProps, newProps));
if (!isDeeplyEqual(prevProps, newProps) || customPagesChanged || customMenuItemsChanged) {
- // if (!this.props.props.withOutlet) {
if (this.portalRef.current) {
this.props.updateProps({ node: this.portalRef.current, props: this.props.props });
}
- // }
}
}
componentDidMount() {
- // if (this.props.props.withOutlet) {
- // return;
- // }
if (this.portalRef.current) {
if (isMountProps(this.props)) {
this.props.mount(this.portalRef.current, this.props.props);
@@ -166,9 +149,6 @@ class Portal extends React.PureComponent<
}
componentWillUnmount() {
- // if (this.props.props.withOutlet) {
- // return;
- // }
if (this.portalRef.current) {
if (isMountProps(this.props)) {
this.props.unmount(this.portalRef.current);
@@ -180,91 +160,28 @@ class Portal extends React.PureComponent<
}
render() {
- const { renderHtmlElement = true } = this.props;
+ const { hideRootHtmlElement = false } = this.props;
return (
<>
- {/*{!this.props.props.withOutlet &&
}*/}
- {renderHtmlElement &&
}
- {/*{isMountProps(this.props) &&*/}
- {/* this.props?.customPagesPortals?.map((portal, index) => createElement(portal, { key: index }))}*/}
- {/*{isMountProps(this.props) &&*/}
- {/* this.props?.customMenuItemsPortals?.map((portal, index) => createElement(portal, { key: index }))}*/}
-
+ {!hideRootHtmlElement &&
}
{this.props.children}
-
- {/*{this.props.props.withOutlet*/}
- {/* ? React.Children.map(this.props.props.children, (child, index) => {*/}
- {/* return child;*/}
- {/* // Clone each child and pass additional props*/}
- {/* return React.cloneElement(child, {*/}
- {/* key: index, // always set a unique key when mapping*/}
- {/* // additionalProp: `Value ${index + 1}`, // adding new props or modifying existing ones*/}
- {/* ...this.props,*/}
- {/* });*/}
- {/* })*/}
- {/* : null}*/}
>
);
}
}
-// class Portal2 extends React.PureComponent {
-// private portalRef = React.createRef();
-//
-// componentDidUpdate(_prevProps: Readonly) {
-// if (!isMountProps(_prevProps) || !isMountProps(this.props)) {
-// return;
-// }
-// console.log('portal2 update');
-//
-// // Remove children and customPages from props before comparing
-// // children might hold circular references which deepEqual can't handle
-// // and the implementation of customPages or customMenuItems relies on props getting new references
-// const prevProps = without(_prevProps.props, 'customPages', 'customMenuItems', 'children');
-// const newProps = without(this.props.props, 'customPages', 'customMenuItems', 'children');
-// // instead, we simply use the length of customPages to determine if it changed or not
-// const customPagesChanged = prevProps.customPages?.length !== newProps.customPages?.length;
-// const customMenuItemsChanged = prevProps.customMenuItems?.length !== newProps.customMenuItems?.length;
-//
-// console.log(prevProps, newProps, isDeeplyEqual(prevProps, newProps));
-// if (!isDeeplyEqual(prevProps, newProps) || customPagesChanged || customMenuItemsChanged) {
-// this.props.updateProps({ node: this.portalRef.current, props: this.props.props });
-// }
-// }
-//
-// componentDidMount() {
-// console.log('portal2 mounted');
-// if (this.portalRef.current) {
-// if (isMountProps(this.props)) {
-// this.props.mount(this.portalRef.current, this.props.props);
-// }
-//
-// if (isOpenProps(this.props)) {
-// this.props.open(this.props.props);
-// }
-// }
-// }
-//
-// componentWillUnmount() {
-// console.log('portal2 unmounted');
-// if (this.portalRef.current) {
-// if (isMountProps(this.props)) {
-// this.props.unmount(this.portalRef.current);
-// }
-// if (isOpenProps(this.props)) {
-// this.props.close();
-// }
-// }
-// }
-//
-// render() {
-// return (
-// <>
-//
-// >
-// );
-// }
-// }
+const CustomPortalsRenderer = (props: MountProps) => {
+ if (!isMountProps(props)) {
+ return null;
+ }
+
+ return (
+ <>
+ {props?.customPagesPortals?.map((portal, index) => createElement(portal, { key: index }))}
+ {props?.customMenuItemsPortals?.map((portal, index) => createElement(portal, { key: index }))}
+ >
+ );
+};
export const SignIn = withClerk(({ clerk, ...props }: WithClerkProp) => {
return (
@@ -348,7 +265,7 @@ const _UserButton = withClerk(
>
{props.children}
@@ -402,14 +319,14 @@ export const __experimental_UserVerification = withClerk(
'__experimental_UserVerification',
);
-export function OrganizationProfilePage({ children }: PropsWithChildren) {
+export function OrganizationProfilePage(_: PropsWithChildren) {
logErrorInDevMode(organizationProfilePageRenderedError);
- return <>{children}>;
+ return null;
}
-export function OrganizationProfileLink({ children }: PropsWithChildren) {
+export function OrganizationProfileLink(_: PropsWithChildren) {
logErrorInDevMode(organizationProfileLinkRenderedError);
- return <>{children}>;
+ return null;
}
const _OrganizationProfile = withClerk(
@@ -444,27 +361,61 @@ export const CreateOrganization = withClerk(({ clerk, ...props }: WithClerkProp<
);
}, 'CreateOrganization');
+// @ts-ignore
+const OrganizationSwitcherContext = createContext({});
+
const _OrganizationSwitcher = withClerk(
({ clerk, ...props }: WithClerkProp>) => {
const { customPages, customPagesPortals } = useOrganizationProfileCustomPages(props.children);
const organizationProfileProps = Object.assign(props.organizationProfileProps || {}, { customPages });
+ const passableProps = {
+ mount: clerk.mountOrganizationSwitcher,
+ unmount: clerk.unmountOrganizationSwitcher,
+ updateProps: (clerk as any).__unstable__updateProps,
+ props: { ...props, organizationProfileProps },
+ customPagesPortals: customPagesPortals,
+ };
+
return (
-
+ //
+
+
+
+ {props.children}
+
+
+
);
},
'OrganizationSwitcher',
);
+export function OrganizationSwitcherOutlet() {
+ const props = useContext(OrganizationSwitcherContext);
+ return ;
+}
+
export const OrganizationSwitcher: OrganizationSwitcherExportType = Object.assign(_OrganizationSwitcher, {
OrganizationProfilePage,
OrganizationProfileLink,
+ Body: OrganizationSwitcherOutlet,
});
export const OrganizationList = withClerk(({ clerk, ...props }: WithClerkProp) => {
diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts
index 79af737d913..9635a3e811e 100644
--- a/packages/types/src/clerk.ts
+++ b/packages/types/src/clerk.ts
@@ -983,42 +983,28 @@ export type UserButtonProps = UserButtonProfileMode & {
*/
defaultOpen?: boolean;
- __experimental_asStandalone?: {
- open: boolean;
- onOpenChanged: (open: boolean | ((prevState: boolean) => boolean)) => void;
- };
-
/**
* If true the UserButton will only render the popover.
* Enables developers to implement a custom dialog.
* @experimental This API is experimental and may change at any moment.
- * @default false
- */
- __experimental_standalone?: boolean;
-
- /**
- * Notifies the caller that it's safe to unmount UserButton.
- * It only fires when used in conjunction with `__experimental_standalone`.
- * @experimental This API is experimental and may change at any moment.
* @default undefined
*/
- __experimental_onDismiss?: () => void;
-
- // /**
- // * The controlled open state of the popover. When defined the trigger will not be
- // * Should be used in conjunction with `__experimental_open`.
- // * @experimental This API is experimental and may change at any moment.
- // * @default undefined
- // */
- // __experimental_open?: boolean;
- //
- // /**
- // * Event handler called when the open state of the dialog changes.
- // * It only fires when used in conjunction with `__experimental_open`.
- // * @experimental This API is experimental and may change at any moment.
- // * @default undefined
- // */
- // __experimental_onOpenChanged?: (open: boolean | ((prevState: boolean) => boolean)) => void;
+ __experimental_asStandalone?: {
+ /**
+ * The controlled open state of the popover. When defined the trigger will not be rendered
+ * Should be used in conjunction with `__experimental_asStandalone.onOpenChanged`.
+ * @experimental This API is experimental and may change at any moment.
+ */
+ open: boolean;
+
+ /**
+ * Event handler called when the open state of the dialog changes.
+ * It only fires when used in conjunction with `__experimental_asStandalone.open`.
+ * @experimental This API is experimental and may change at any moment.
+ * @default undefined
+ */
+ onOpenChanged: (open: boolean | ((prevState: boolean) => boolean)) => void;
+ };
/**
* Full URL or path to navigate after sign out is complete
@@ -1080,21 +1066,30 @@ export type OrganizationSwitcherProps = CreateOrganizationMode &
* Controls the default state of the OrganizationSwitcher
*/
defaultOpen?: boolean;
- /**
- * If true the OrganizationSwitcher will only render the popover.
- * Enables developers to implement a custom dialog.
- * @experimental This API is experimental and may change at any moment.
- * @default false
- */
- __experimental_standalone?: boolean;
/**
- * Notifies the caller that it's safe to unmount OrganizationSwitcher.
- * It only fires when used in conjunction with `__experimental_standalone`.
+ * If true the UserButton will only render the popover.
+ * Enables developers to implement a custom dialog.
* @experimental This API is experimental and may change at any moment.
* @default undefined
*/
- __experimental_onDismiss?: () => void;
+ __experimental_asStandalone?: {
+ /**
+ * The controlled open state of the popover. When defined the trigger will not be rendered
+ * Should be used in conjunction with `__experimental_asStandalone.onOpenChanged`.
+ * @experimental This API is experimental and may change at any moment.
+ */
+ open: boolean;
+
+ /**
+ * Event handler called when the open state of the dialog changes.
+ * It only fires when used in conjunction with `__experimental_asStandalone.open`.
+ * @experimental This API is experimental and may change at any moment.
+ * @default undefined
+ */
+ onOpenChanged: (open: boolean | ((prevState: boolean) => boolean)) => void;
+ };
+
/**
* By default, users can switch between organization and their personal account.
* This option controls whether OrganizationSwitcher will include the user's personal account
From 0c424926d8c898413fc381dd263898a61da3e0fd Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Thu, 5 Sep 2024 21:00:05 +0300
Subject: [PATCH 16/36] skip tests
---
.../__tests__/OrganizationSwitcher.test.tsx | 4 ++--
.../ui/components/UserButton/__tests__/UserButton.test.tsx | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx
index 2ee55c9210a..79d7b063ab9 100644
--- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx
+++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx
@@ -124,7 +124,7 @@ describe('OrganizationSwitcher', () => {
expect(getByText('Create organization')).toBeInTheDocument();
});
- it('renders organization switcher popover as standalone', async () => {
+ it.skip('renders organization switcher popover as standalone', async () => {
const { wrapper, props } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.com'], create_organization_enabled: true });
@@ -139,7 +139,7 @@ describe('OrganizationSwitcher', () => {
});
});
- it('calls onDismiss when "Manage Organization" is clicked', async () => {
+ it.skip('calls onDismiss when "Manage Organization" is clicked', async () => {
const { wrapper, fixtures, props } = await createFixtures(f => {
f.withOrganizations();
f.withUser({
diff --git a/packages/clerk-js/src/ui/components/UserButton/__tests__/UserButton.test.tsx b/packages/clerk-js/src/ui/components/UserButton/__tests__/UserButton.test.tsx
index ac13dc57add..629d3cf527e 100644
--- a/packages/clerk-js/src/ui/components/UserButton/__tests__/UserButton.test.tsx
+++ b/packages/clerk-js/src/ui/components/UserButton/__tests__/UserButton.test.tsx
@@ -21,7 +21,7 @@ describe('UserButton', () => {
expect(queryByRole('button')).not.toBeNull();
});
- it('renders popover as standalone when there is a user', async () => {
+ it.skip('renders popover as standalone when there is a user', async () => {
const { wrapper, props } = await createFixtures(f => {
f.withUser({ email_addresses: ['test@clerk.com'] });
});
@@ -33,7 +33,7 @@ describe('UserButton', () => {
getByText('Manage account');
});
- it('calls onDismiss when it is used as standalone', async () => {
+ it.skip('calls onDismiss when it is used as standalone', async () => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withUser({ email_addresses: ['test@clerk.com'] });
});
From f3edf189708ace17598ae7b93b426da1727fd9d5 Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Thu, 5 Sep 2024 21:00:25 +0300
Subject: [PATCH 17/36] update bundlewatch.config.json
---
packages/clerk-js/bundlewatch.config.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json
index c7986ec5694..a4aa084bf64 100644
--- a/packages/clerk-js/bundlewatch.config.json
+++ b/packages/clerk-js/bundlewatch.config.json
@@ -2,7 +2,7 @@
"files": [
{ "path": "./dist/clerk.browser.js", "maxSize": "64kB" },
{ "path": "./dist/clerk.headless.js", "maxSize": "43kB" },
- { "path": "./dist/ui-common*.js", "maxSize": "85KB" },
+ { "path": "./dist/ui-common*.js", "maxSize": "85.1KB" },
{ "path": "./dist/vendors*.js", "maxSize": "70KB" },
{ "path": "./dist/coinbase*.js", "maxSize": "58KB" },
{ "path": "./dist/createorganization*.js", "maxSize": "5KB" },
From f99c981adab319660ccd53ecc5c891037b1cb434 Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Mon, 14 Oct 2024 14:15:20 +0300
Subject: [PATCH 18/36] wip prefetch on provider
---
packages/clerk-js/src/core/clerk.ts | 7 +++++++
packages/clerk-js/src/ui/Components.tsx | 10 ++++++++++
.../src/ui/components/prefetch-organization-list.tsx | 8 ++++++++
packages/clerk-js/src/ui/lazyModules/providers.tsx | 7 +++++++
packages/react/src/components/uiComponents.tsx | 11 +++++++++--
packages/react/src/isomorphicClerk.ts | 9 +++++++++
packages/types/src/clerk.ts | 6 ++++++
7 files changed, 56 insertions(+), 2 deletions(-)
create mode 100644 packages/clerk-js/src/ui/components/prefetch-organization-list.tsx
diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts
index 0aa484da6a9..f6edfe7143d 100644
--- a/packages/clerk-js/src/core/clerk.ts
+++ b/packages/clerk-js/src/core/clerk.ts
@@ -700,6 +700,13 @@ export class Clerk implements ClerkInterface {
void this.#componentControls?.ensureMounted().then(controls => controls.unmountComponent({ node }));
};
+ public __internal_prefetchOrganizationSwitcher = () => {
+ this.assertComponentsReady(this.#componentControls);
+ void this.#componentControls
+ ?.ensureMounted({ preloadHint: 'OrganizationSwitcher' })
+ .then(controls => controls.prefetch('organizationSwitcher'));
+ };
+
public mountOrganizationList = (node: HTMLDivElement, props?: OrganizationListProps) => {
this.assertComponentsReady(this.#componentControls);
if (disabledOrganizationsFeature(this, this.environment)) {
diff --git a/packages/clerk-js/src/ui/Components.tsx b/packages/clerk-js/src/ui/Components.tsx
index e35ab93326d..bf29a412c53 100644
--- a/packages/clerk-js/src/ui/Components.tsx
+++ b/packages/clerk-js/src/ui/Components.tsx
@@ -37,6 +37,7 @@ import {
LazyModalRenderer,
LazyOneTapRenderer,
LazyProviders,
+ OrganizationSwitcherPrefetch,
} from './lazyModules/providers';
import type { AvailableComponentProps } from './types';
@@ -85,6 +86,7 @@ export type ComponentControls = {
| 'createOrganization'
| 'userVerification',
) => void;
+ prefetch: (component: 'organizationSwitcher') => void;
// Special case, as the impersonation fab mounts automatically
mountImpersonationFab: () => void;
};
@@ -113,6 +115,7 @@ interface ComponentsState {
userVerificationModal: null | __experimental_UserVerificationProps;
organizationProfileModal: null | OrganizationProfileProps;
createOrganizationModal: null | CreateOrganizationProps;
+ organizationSwitcherPrefetch: boolean;
nodes: Map;
impersonationFab: boolean;
}
@@ -190,6 +193,7 @@ const Components = (props: ComponentsProps) => {
userVerificationModal: null,
organizationProfileModal: null,
createOrganizationModal: null,
+ organizationSwitcherPrefetch: false,
nodes: new Map(),
impersonationFab: false,
});
@@ -258,6 +262,10 @@ const Components = (props: ComponentsProps) => {
setState(s => ({ ...s, impersonationFab: true }));
};
+ componentsControls.prefetch = component => {
+ setState(s => ({ ...s, [`${component}Prefetch`]: true }));
+ };
+
props.onComponentsMounted();
}, []);
@@ -409,6 +417,8 @@ const Components = (props: ComponentsProps) => {
)}
+
+ {state.organizationSwitcherPrefetch && }
);
diff --git a/packages/clerk-js/src/ui/components/prefetch-organization-list.tsx b/packages/clerk-js/src/ui/components/prefetch-organization-list.tsx
new file mode 100644
index 00000000000..e7ca34f17c0
--- /dev/null
+++ b/packages/clerk-js/src/ui/components/prefetch-organization-list.tsx
@@ -0,0 +1,8 @@
+import { useOrganizationList } from '@clerk/shared/react';
+
+import { organizationListParams } from './OrganizationSwitcher/utils';
+
+export function OrganizationSwitcherPrefetch() {
+ useOrganizationList(organizationListParams);
+ return null;
+}
diff --git a/packages/clerk-js/src/ui/lazyModules/providers.tsx b/packages/clerk-js/src/ui/lazyModules/providers.tsx
index 70c3dcf56e8..1439e17c8a2 100644
--- a/packages/clerk-js/src/ui/lazyModules/providers.tsx
+++ b/packages/clerk-js/src/ui/lazyModules/providers.tsx
@@ -16,6 +16,11 @@ const Portal = lazy(() => import('./../portal').then(m => ({ default: m.Portal }
const VirtualBodyRootPortal = lazy(() => import('./../portal').then(m => ({ default: m.VirtualBodyRootPortal })));
const FlowMetadataProvider = lazy(() => import('./../elements').then(m => ({ default: m.FlowMetadataProvider })));
const Modal = lazy(() => import('./../elements').then(m => ({ default: m.Modal })));
+const OrganizationSwitcherPrefetch = lazy(() =>
+ import(/* webpackChunkName: "prefetchorganizationlist" */ '../components/prefetch-organization-list').then(m => ({
+ default: m.OrganizationSwitcherPrefetch,
+ })),
+);
type LazyProvidersProps = React.PropsWithChildren<{ clerk: any; environment: any; options: any; children: any }>;
@@ -155,3 +160,5 @@ export const LazyOneTapRenderer = (props: LazyOneTapRendererProps) => {
);
};
+
+export { OrganizationSwitcherPrefetch };
diff --git a/packages/react/src/components/uiComponents.tsx b/packages/react/src/components/uiComponents.tsx
index fcff7529f8a..99621b86b80 100644
--- a/packages/react/src/components/uiComponents.tsx
+++ b/packages/react/src/components/uiComponents.tsx
@@ -361,8 +361,10 @@ export const CreateOrganization = withClerk(({ clerk, ...props }: WithClerkProp<
);
}, 'CreateOrganization');
-// @ts-ignore
-const OrganizationSwitcherContext = createContext({});
+const OrganizationSwitcherContext = createContext(
+ // @ts-expect-error We are defining the values below
+ {},
+);
const _OrganizationSwitcher = withClerk(
({ clerk, ...props }: WithClerkProp>) => {
@@ -377,6 +379,11 @@ const _OrganizationSwitcher = withClerk(
customPagesPortals: customPagesPortals,
};
+ /**
+ * Prefetch organization list
+ */
+ clerk.__internal_prefetchOrganizationSwitcher();
+
return (
// {
+ const callback = () => this.clerkjs?.__internal_prefetchOrganizationSwitcher();
+ if (this.clerkjs && this.#loaded) {
+ void callback();
+ } else {
+ this.premountMethodCalls.set('__internal_prefetchOrganizationSwitcher', callback);
+ }
+ };
+
mountOrganizationList = (node: HTMLDivElement, props: OrganizationListProps): void => {
if (this.clerkjs && this.#loaded) {
this.clerkjs.mountOrganizationList(node, props);
diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts
index b93c05a471a..8bacbb9065b 100644
--- a/packages/types/src/clerk.ts
+++ b/packages/types/src/clerk.ts
@@ -348,6 +348,8 @@ export interface Clerk {
*/
unmountOrganizationSwitcher: (targetNode: HTMLDivElement) => void;
+ __internal_prefetchOrganizationSwitcher: () => void;
+
/**
* Mount an organization list component at the target element.
* @param targetNode Target to mount the OrganizationList component.
@@ -1042,6 +1044,8 @@ export type UserButtonProps = UserButtonProfileMode & {
*/
defaultOpen?: boolean;
+ __experimental_asProvider?: boolean;
+
/**
* If true the UserButton will only render the popover.
* Enables developers to implement a custom dialog.
@@ -1126,6 +1130,8 @@ export type OrganizationSwitcherProps = CreateOrganizationMode &
*/
defaultOpen?: boolean;
+ __experimental_asProvider?: boolean;
+
/**
* If true the UserButton will only render the popover.
* Enables developers to implement a custom dialog.
From 45509a0c688503ab513fb2728f2e9bf8839e46bd Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Mon, 14 Oct 2024 15:29:14 +0300
Subject: [PATCH 19/36] OrganizationSwitcher implement asProvider and
asStandalone
---
.../OrganizationSwitcher.tsx | 4 +-
.../react/src/components/uiComponents.tsx | 49 +++++++++++++------
packages/types/src/clerk.ts | 21 +-------
3 files changed, 36 insertions(+), 38 deletions(-)
diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx
index 60661213674..e0fdd0be568 100644
--- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx
+++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx
@@ -54,9 +54,7 @@ const _OrganizationSwitcher = () => {
>
{__experimental_asStandalone ? (
- __experimental_asStandalone.open ? (
-
- ) : null
+
) : (
diff --git a/packages/react/src/components/uiComponents.tsx b/packages/react/src/components/uiComponents.tsx
index 99621b86b80..f517f69ab86 100644
--- a/packages/react/src/components/uiComponents.tsx
+++ b/packages/react/src/components/uiComponents.tsx
@@ -65,11 +65,26 @@ type OrganizationProfileExportType = typeof _OrganizationProfile & {
type OrganizationSwitcherExportType = typeof _OrganizationSwitcher & {
OrganizationProfilePage: typeof OrganizationProfilePage;
OrganizationProfileLink: typeof OrganizationProfileLink;
- Body: () => React.JSX.Element;
+ /**
+ * The `Outlet` component can be used in conjunction with `asProvider` in order to control rendering
+ * of the OrganizationSwitcher without affecting its configuration or any custom pages
+ * that could be mounted
+ * @experimental This API is experimental and may change at any moment.
+ */
+ __experimental_Outlet: typeof OrganizationSwitcherOutlet;
};
-type OrganizationSwitcherPropsWithoutCustomPages = Without & {
+type OrganizationSwitcherPropsWithoutCustomPages = Without<
+ OrganizationSwitcherProps,
+ 'organizationProfileProps' | '__experimental_asStandalone'
+> & {
organizationProfileProps?: Pick;
+ /**
+ * Adding `asProvider` will defer rendering until the `Outlet` component is mounted.
+ * @experimental This API is experimental and may change at any moment.
+ * @default undefined
+ */
+ __experimental_asProvider?: boolean;
};
const isMountProps = (props: any): props is MountProps => {
@@ -128,7 +143,6 @@ class Portal extends React.PureComponent<
const customPagesChanged = prevProps.customPages?.length !== newProps.customPages?.length;
const customMenuItemsChanged = prevProps.customMenuItems?.length !== newProps.customMenuItems?.length;
- console.log(prevProps, newProps, isDeeplyEqual(prevProps, newProps));
if (!isDeeplyEqual(prevProps, newProps) || customPagesChanged || customMenuItemsChanged) {
if (this.portalRef.current) {
this.props.updateProps({ node: this.portalRef.current, props: this.props.props });
@@ -385,14 +399,6 @@ const _OrganizationSwitcher = withClerk(
clerk.__internal_prefetchOrganizationSwitcher();
return (
- //
-
{props.children}
@@ -414,15 +420,26 @@ const _OrganizationSwitcher = withClerk(
'OrganizationSwitcher',
);
-export function OrganizationSwitcherOutlet() {
- const props = useContext(OrganizationSwitcherContext);
- return ;
+export function OrganizationSwitcherOutlet(
+ outletProps: Without,
+) {
+ const providerProps = useContext(OrganizationSwitcherContext);
+
+ const portalProps = {
+ ...providerProps,
+ props: {
+ ...providerProps.props,
+ ...outletProps,
+ },
+ } satisfies MountProps;
+
+ return ;
}
export const OrganizationSwitcher: OrganizationSwitcherExportType = Object.assign(_OrganizationSwitcher, {
OrganizationProfilePage,
OrganizationProfileLink,
- Body: OrganizationSwitcherOutlet,
+ __experimental_Outlet: OrganizationSwitcherOutlet,
});
export const OrganizationList = withClerk(({ clerk, ...props }: WithClerkProp) => {
diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts
index 8bacbb9065b..3db2a338e30 100644
--- a/packages/types/src/clerk.ts
+++ b/packages/types/src/clerk.ts
@@ -1130,30 +1130,13 @@ export type OrganizationSwitcherProps = CreateOrganizationMode &
*/
defaultOpen?: boolean;
- __experimental_asProvider?: boolean;
-
/**
- * If true the UserButton will only render the popover.
+ * If true, OrganizationSwitcher will only render the popover.
* Enables developers to implement a custom dialog.
* @experimental This API is experimental and may change at any moment.
* @default undefined
*/
- __experimental_asStandalone?: {
- /**
- * The controlled open state of the popover. When defined the trigger will not be rendered
- * Should be used in conjunction with `__experimental_asStandalone.onOpenChanged`.
- * @experimental This API is experimental and may change at any moment.
- */
- open: boolean;
-
- /**
- * Event handler called when the open state of the dialog changes.
- * It only fires when used in conjunction with `__experimental_asStandalone.open`.
- * @experimental This API is experimental and may change at any moment.
- * @default undefined
- */
- onOpenChanged: (open: boolean | ((prevState: boolean) => boolean)) => void;
- };
+ __experimental_asStandalone?: boolean;
/**
* By default, users can switch between organization and their personal account.
From 9283c257608c2d920769869245dc3fe1d9dc36e5 Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Mon, 14 Oct 2024 15:56:23 +0300
Subject: [PATCH 20/36] UserButton implement asProvider and asStandalone
---
.../ui/components/UserButton/UserButton.tsx | 4 +-
.../ui/contexts/ClerkUIComponentsContext.tsx | 2 -
.../react/src/components/uiComponents.tsx | 38 +++++++++++++++----
packages/types/src/clerk.ts | 19 +---------
4 files changed, 33 insertions(+), 30 deletions(-)
diff --git a/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx b/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx
index f079674a4d9..6e256949469 100644
--- a/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx
+++ b/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx
@@ -52,9 +52,7 @@ const _UserButton = () => {
sx={{ display: 'inline-flex' }}
>
{__experimental_asStandalone ? (
- __experimental_asStandalone.open ? (
-
- ) : null
+
) : (
diff --git a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx
index b54f06d0759..2c2e4413a78 100644
--- a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx
+++ b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx
@@ -227,8 +227,6 @@ export const useUserProfileContext = (): UserProfileContextType => {
throw new Error('Clerk: useUserProfileContext called outside of the mounted UserProfile component.');
}
- console.log('----- profile', customPages);
-
const pages = useMemo(() => {
return createUserProfileCustomPages(customPages || [], clerk);
}, [customPages]);
diff --git a/packages/react/src/components/uiComponents.tsx b/packages/react/src/components/uiComponents.tsx
index f517f69ab86..67531e667cc 100644
--- a/packages/react/src/components/uiComponents.tsx
+++ b/packages/react/src/components/uiComponents.tsx
@@ -50,11 +50,26 @@ type UserButtonExportType = typeof _UserButton & {
MenuItems: typeof MenuItems;
Action: typeof MenuAction;
Link: typeof MenuLink;
- Body: () => React.JSX.Element;
+ /**
+ * The `Outlet` component can be used in conjunction with `asProvider` in order to control rendering
+ * of the OrganizationSwitcher without affecting its configuration or any custom pages
+ * that could be mounted
+ * @experimental This API is experimental and may change at any moment.
+ */
+ __experimental_Outlet: typeof UserButtonOutlet;
};
-type UserButtonPropsWithoutCustomPages = Without & {
+type UserButtonPropsWithoutCustomPages = Without<
+ UserButtonProps,
+ 'userProfileProps' | '__experimental_asStandalone'
+> & {
userProfileProps?: Pick;
+ /**
+ * Adding `asProvider` will defer rendering until the `Outlet` component is mounted.
+ * @experimental This API is experimental and may change at any moment.
+ * @default undefined
+ */
+ __experimental_asProvider?: boolean;
};
type OrganizationProfileExportType = typeof _OrganizationProfile & {
@@ -279,7 +294,7 @@ const _UserButton = withClerk(
>
{props.children}
@@ -305,9 +320,18 @@ export function MenuLink(_: PropsWithChildren) {
return null;
}
-export function UserButtonOutlet() {
- const props = useContext(UserButtonContext);
- return ;
+export function UserButtonOutlet(outletProps: Without) {
+ const providerProps = useContext(UserButtonContext);
+
+ const portalProps = {
+ ...providerProps,
+ props: {
+ ...providerProps.props,
+ ...outletProps,
+ },
+ } satisfies MountProps;
+
+ return ;
}
export const UserButton: UserButtonExportType = Object.assign(_UserButton, {
@@ -316,7 +340,7 @@ export const UserButton: UserButtonExportType = Object.assign(_UserButton, {
MenuItems,
Action: MenuAction,
Link: MenuLink,
- Body: UserButtonOutlet as () => React.JSX.Element,
+ __experimental_Outlet: UserButtonOutlet,
});
export const __experimental_UserVerification = withClerk(
diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts
index 3db2a338e30..c520267231d 100644
--- a/packages/types/src/clerk.ts
+++ b/packages/types/src/clerk.ts
@@ -1044,30 +1044,13 @@ export type UserButtonProps = UserButtonProfileMode & {
*/
defaultOpen?: boolean;
- __experimental_asProvider?: boolean;
-
/**
* If true the UserButton will only render the popover.
* Enables developers to implement a custom dialog.
* @experimental This API is experimental and may change at any moment.
* @default undefined
*/
- __experimental_asStandalone?: {
- /**
- * The controlled open state of the popover. When defined the trigger will not be rendered
- * Should be used in conjunction with `__experimental_asStandalone.onOpenChanged`.
- * @experimental This API is experimental and may change at any moment.
- */
- open: boolean;
-
- /**
- * Event handler called when the open state of the dialog changes.
- * It only fires when used in conjunction with `__experimental_asStandalone.open`.
- * @experimental This API is experimental and may change at any moment.
- * @default undefined
- */
- onOpenChanged: (open: boolean | ((prevState: boolean) => boolean)) => void;
- };
+ __experimental_asStandalone?: boolean;
/**
* Full URL or path to navigate after sign out is complete
From b4903a76c65da97831b7837b67501da21b7012a3 Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Mon, 14 Oct 2024 16:15:06 +0300
Subject: [PATCH 21/36] bump bundlewatch for clerk-js
---
packages/clerk-js/bundlewatch.config.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json
index e1b6a6c774d..529b3ec7757 100644
--- a/packages/clerk-js/bundlewatch.config.json
+++ b/packages/clerk-js/bundlewatch.config.json
@@ -1,6 +1,6 @@
{
"files": [
- { "path": "./dist/clerk.browser.js", "maxSize": "64.5kB" },
+ { "path": "./dist/clerk.browser.js", "maxSize": "64.6kB" },
{ "path": "./dist/clerk.headless.js", "maxSize": "43kB" },
{ "path": "./dist/ui-common*.js", "maxSize": "86KB" },
{ "path": "./dist/vendors*.js", "maxSize": "70KB" },
From 0c86be3eea61a1150bf197536b05ce55c6f5e7fa Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Mon, 14 Oct 2024 18:29:41 +0300
Subject: [PATCH 22/36] update e2e tests
---
.../src/custom-user-button-trigger/index.tsx | 100 ++++++++++++++++++
.../src/custom-user-button/index.tsx | 2 +-
.../src/custom-user-profile/index.tsx | 2 +-
integration/templates/react-vite/src/main.tsx | 5 +
integration/tests/custom-pages.test.ts | 68 +++++++++++-
.../react/src/components/uiComponents.tsx | 48 ++++-----
packages/react/src/types.ts | 7 +-
7 files changed, 197 insertions(+), 35 deletions(-)
create mode 100644 integration/templates/react-vite/src/custom-user-button-trigger/index.tsx
diff --git a/integration/templates/react-vite/src/custom-user-button-trigger/index.tsx b/integration/templates/react-vite/src/custom-user-button-trigger/index.tsx
new file mode 100644
index 00000000000..bbcd41b52e9
--- /dev/null
+++ b/integration/templates/react-vite/src/custom-user-button-trigger/index.tsx
@@ -0,0 +1,100 @@
+import { UserButton } from '@clerk/clerk-react';
+import { PropsWithChildren, useContext, useState } from 'react';
+import { PageContext, PageContextProvider } from '../PageContext.tsx';
+
+function Page1() {
+ const { counter, setCounter } = useContext(PageContext);
+
+ return (
+ <>
+ Page 1
+ Counter: {counter}
+ setCounter(a => a + 1)}
+ >
+ Update
+
+ >
+ );
+}
+
+function ToggleChildren(props: PropsWithChildren) {
+ const [isMounted, setMounted] = useState(false);
+
+ return (
+ <>
+ setMounted(v => !v)}
+ >
+ Toggle
+
+ {isMounted ? props.children : null}
+ >
+ );
+}
+
+export default function Page() {
+ return (
+
+
+ 🙃
}
+ url='page-1'
+ >
+
+
+
+ 🙃}
+ url='page-2'
+ >
+ Page 2
+
+ This is leaking
+ 🌐}
+ />
+
+ 🙃}
+ open={'page-1'}
+ />
+
+
+ 🌐}
+ />
+
+ 🌐}
+ />
+
+ 🔔}
+ onClick={() => alert('custom-alert')}
+ />
+
+ 🌐}
+ />
+
+
+
+
+
+ );
+}
diff --git a/integration/templates/react-vite/src/custom-user-button/index.tsx b/integration/templates/react-vite/src/custom-user-button/index.tsx
index e6c800bcaf3..e283cddd76b 100644
--- a/integration/templates/react-vite/src/custom-user-button/index.tsx
+++ b/integration/templates/react-vite/src/custom-user-button/index.tsx
@@ -38,7 +38,7 @@ export default function Page() {
>
Page 2
- 🌐
+ This is leaking
Page 2
- 🌐
+ This is leaking
{
const navigate = useNavigate();
@@ -64,6 +65,10 @@ const router = createBrowserRouter([
path: '/custom-user-button',
element: ,
},
+ {
+ path: '/custom-user-button-trigger',
+ element: ,
+ },
],
},
]);
diff --git a/integration/tests/custom-pages.test.ts b/integration/tests/custom-pages.test.ts
index f3dd4a51937..fb5d7d924a8 100644
--- a/integration/tests/custom-pages.test.ts
+++ b/integration/tests/custom-pages.test.ts
@@ -6,6 +6,7 @@ import { createTestUtils, testAgainstRunningApps } from '../testUtils';
const CUSTOM_PROFILE_PAGE = '/custom-user-profile';
const CUSTOM_BUTTON_PAGE = '/custom-user-button';
+const CUSTOM_BUTTON_TRIGGER_PAGE = '/custom-user-button-trigger';
async function waitForMountedComponent(
component: 'UserButton' | 'UserProfile',
@@ -106,11 +107,29 @@ testAgainstRunningApps({ withPattern: ['react.vite.withEmailCodes'] })(
await u.page.waitForSelector('p[data-page="1"]', { state: 'attached' });
await expect(u.page.locator('p[data-page="1"]')).toHaveText('Counter: 0');
- u.page.locator('button[data-page="1"]').click();
+ await u.page.locator('button[data-page="1"]').click();
await expect(u.page.locator('p[data-page="1"]')).toHaveText('Counter: 1');
});
+ test('does not leak children', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+ await u.po.signIn.goTo();
+ await u.po.signIn.waitForMounted();
+ await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
+ await u.po.expect.toBeSignedIn();
+
+ await waitForMountedComponent(component, u);
+
+ const buttons = await u.page.locator('button.cl-navbarButton__custom-page-0').all();
+ expect(buttons.length).toBe(1);
+ const [profilePage] = buttons;
+ await expect(profilePage.locator('div.cl-navbarButtonIcon__custom-page-0')).toHaveText('🙃');
+ await profilePage.click();
+
+ await expect(u.page.locator('p[data-leaked-child]')).toBeHidden();
+ });
+
test('user profile custom external absolute link', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.po.signIn.goTo();
@@ -149,6 +168,53 @@ testAgainstRunningApps({ withPattern: ['react.vite.withEmailCodes'] })(
});
});
+ test.describe('User Button with experimental asStandalone and asProvider', () => {
+ test('items at the specified order', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+ await u.po.signIn.goTo();
+ await u.po.signIn.waitForMounted();
+ await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
+ await u.po.expect.toBeSignedIn();
+
+ await u.page.goToRelative(CUSTOM_BUTTON_TRIGGER_PAGE);
+ const toggleButton = await u.page.waitForSelector('button[data-toggle-btn]');
+ await toggleButton.click();
+
+ await u.po.userButton.waitForPopover();
+ await u.po.userButton.triggerManageAccount();
+ await u.po.userProfile.waitForMounted();
+
+ const pagesContainer = u.page.locator('div.cl-navbarButtons').first();
+
+ const buttons = await pagesContainer.locator('button').all();
+
+ expect(buttons.length).toBe(6);
+
+ const expectedTexts = ['Profile', '🙃Page 1', 'Security', '🙃Page 2', '🌐Visit Clerk', '🌐Visit User page'];
+ for (let i = 0; i < buttons.length; i++) {
+ await expect(buttons[i]).toHaveText(expectedTexts[i]);
+ }
+ });
+
+ test('children should be leaking when used with asProvider', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+ await u.po.signIn.goTo();
+ await u.po.signIn.waitForMounted();
+ await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
+ await u.po.expect.toBeSignedIn();
+
+ await u.page.goToRelative(CUSTOM_BUTTON_TRIGGER_PAGE);
+ const toggleButton = await u.page.waitForSelector('button[data-toggle-btn]');
+ await toggleButton.click();
+
+ await u.po.userButton.waitForPopover();
+ await u.po.userButton.triggerManageAccount();
+ await u.po.userProfile.waitForMounted();
+
+ await expect(u.page.locator('p[data-leaked-child]')).toBeVisible();
+ });
+ });
+
test.describe('User Button custom items', () => {
test('items at the specified order', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
diff --git a/packages/react/src/components/uiComponents.tsx b/packages/react/src/components/uiComponents.tsx
index 722e1c17baa..f0cc6613050 100644
--- a/packages/react/src/components/uiComponents.tsx
+++ b/packages/react/src/components/uiComponents.tsx
@@ -25,6 +25,7 @@ import {
userProfilePageRenderedError,
} from '../errors/messages';
import type {
+ CustomPortalsRendererProps,
MountProps,
OpenProps,
OrganizationProfileLinkProps,
@@ -198,11 +199,7 @@ class Portal extends React.PureComponent<
}
}
-const CustomPortalsRenderer = (props: MountProps) => {
- if (!isMountProps(props)) {
- return null;
- }
-
+const CustomPortalsRenderer = (props: CustomPortalsRendererProps) => {
return (
<>
{props?.customPagesPortals?.map((portal, index) => createElement(portal, { key: index }))}
@@ -252,8 +249,9 @@ const _UserProfile = withClerk(
unmount={clerk.unmountUserProfile}
updateProps={(clerk as any).__unstable__updateProps}
props={{ ...props, customPages }}
- customPagesPortals={customPagesPortals}
- />
+ >
+
+
);
},
'UserProfile',
@@ -278,25 +276,21 @@ const _UserButton = withClerk(
unmount: clerk.unmountUserButton,
updateProps: (clerk as any).__unstable__updateProps,
props: { ...props, userProfileProps, customMenuItems },
+ };
+ const portalProps = {
customPagesPortals: customPagesPortals,
customMenuItemsPortals: customMenuItemsPortals,
};
return (
-
+
- {props.children}
-
+ {/*This mimics the previous behaviour before asProvider existed*/}
+ {props.__experimental_asProvider ? props.children : null}
+
);
@@ -361,8 +355,9 @@ const _OrganizationProfile = withClerk(
unmount={clerk.unmountOrganizationProfile}
updateProps={(clerk as any).__unstable__updateProps}
props={{ ...props, customPages }}
- customPagesPortals={customPagesPortals}
- />
+ >
+
+
);
},
'OrganizationProfile',
@@ -399,7 +394,6 @@ const _OrganizationSwitcher = withClerk(
unmount: clerk.unmountOrganizationSwitcher,
updateProps: (clerk as any).__unstable__updateProps,
props: { ...props, organizationProfileProps },
- customPagesPortals: customPagesPortals,
};
/**
@@ -408,20 +402,14 @@ const _OrganizationSwitcher = withClerk(
clerk.__internal_prefetchOrganizationSwitcher();
return (
-
+
- {props.children}
-
+ {/*This mimics the previous behaviour before asProvider existed*/}
+ {props.__experimental_asProvider ? props.children : null}
+
);
diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts
index 2ad19fa125c..ec0f8dbd235 100644
--- a/packages/react/src/types.ts
+++ b/packages/react/src/types.ts
@@ -67,14 +67,17 @@ export interface HeadlessBrowserClerkConstructor {
export type WithClerkProp = T & { clerk: LoadedClerk };
+export interface CustomPortalsRendererProps {
+ customPagesPortals?: any[];
+ customMenuItemsPortals?: any[];
+}
+
// Clerk object
export interface MountProps {
mount: (node: HTMLDivElement, props: any) => void;
unmount: (node: HTMLDivElement) => void;
updateProps: (props: any) => void;
props?: any;
- customPagesPortals?: any[];
- customMenuItemsPortals?: any[];
}
export interface OpenProps {
From 0ac130132b6bb2ae3fa6dce600662037b70ba792 Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Mon, 14 Oct 2024 18:41:45 +0300
Subject: [PATCH 23/36] update unit tests
---
.../__tests__/OrganizationSwitcher.test.tsx | 33 ++-----------------
.../UserButton/__tests__/UserButton.test.tsx | 20 ++---------
.../ui/contexts/ClerkUIComponentsContext.tsx | 2 --
3 files changed, 4 insertions(+), 51 deletions(-)
diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx
index 79d7b063ab9..bac49c52af3 100644
--- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx
+++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx
@@ -124,13 +124,13 @@ describe('OrganizationSwitcher', () => {
expect(getByText('Create organization')).toBeInTheDocument();
});
- it.skip('renders organization switcher popover as standalone', async () => {
+ it('renders organization switcher popover as standalone', async () => {
const { wrapper, props } = await createFixtures(f => {
f.withOrganizations();
f.withUser({ email_addresses: ['test@clerk.com'], create_organization_enabled: true });
});
props.setProps({
- __experimental_standalone: true,
+ __experimental_asStandalone: true,
});
const { getByText, queryByRole } = render( , { wrapper });
await waitFor(() => {
@@ -139,35 +139,6 @@ describe('OrganizationSwitcher', () => {
});
});
- it.skip('calls onDismiss when "Manage Organization" is clicked', async () => {
- const { wrapper, fixtures, props } = await createFixtures(f => {
- f.withOrganizations();
- f.withUser({
- email_addresses: ['test@clerk.com'],
- organization_memberships: [{ name: 'Org1', role: 'basic_member' }],
- create_organization_enabled: true,
- });
- });
-
- const onDismiss = jest.fn();
- props.setProps({
- __experimental_standalone: true,
- __experimental_onDismiss: onDismiss,
- });
-
- fixtures.clerk.organization?.getRoles.mockRejectedValue(null);
- fixtures.clerk.user?.getOrganizationMemberships.mockResolvedValueOnce([]);
-
- const { getByRole, userEvent, queryByRole, getByText } = render( , { wrapper });
- await waitFor(() => {
- expect(queryByRole('button', { name: 'Open organization switcher' })).toBeNull();
- expect(getByText('Personal account')).toBeInTheDocument();
- });
- await userEvent.click(getByRole('menuitem', { name: 'Create organization' }));
- expect(fixtures.clerk.openCreateOrganization).toHaveBeenCalled();
- expect(onDismiss).toHaveBeenCalledTimes(1);
- });
-
it('lists all organizations the user belongs to', async () => {
const { wrapper, props, fixtures } = await createFixtures(f => {
f.withOrganizations();
diff --git a/packages/clerk-js/src/ui/components/UserButton/__tests__/UserButton.test.tsx b/packages/clerk-js/src/ui/components/UserButton/__tests__/UserButton.test.tsx
index 4b9035531da..f109431182c 100644
--- a/packages/clerk-js/src/ui/components/UserButton/__tests__/UserButton.test.tsx
+++ b/packages/clerk-js/src/ui/components/UserButton/__tests__/UserButton.test.tsx
@@ -21,34 +21,18 @@ describe('UserButton', () => {
expect(queryByRole('button')).not.toBeNull();
});
- it.skip('renders popover as standalone when there is a user', async () => {
+ it('renders popover as standalone when there is a user', async () => {
const { wrapper, props } = await createFixtures(f => {
f.withUser({ email_addresses: ['test@clerk.com'] });
});
props.setProps({
- __experimental_standalone: true,
+ __experimental_asStandalone: true,
});
const { getByText, queryByRole } = render( , { wrapper });
expect(queryByRole('button', { name: 'Open user button' })).toBeNull();
getByText('Manage account');
});
- it.skip('calls onDismiss when it is used as standalone', async () => {
- const { wrapper, props, fixtures } = await createFixtures(f => {
- f.withUser({ email_addresses: ['test@clerk.com'] });
- });
- const onDismiss = jest.fn();
- props.setProps({
- __experimental_standalone: true,
- __experimental_onDismiss: onDismiss,
- });
- const { getByText, userEvent, queryByRole } = render( , { wrapper });
- expect(queryByRole('button', { name: 'Open user button' })).toBeNull();
- await userEvent.click(getByText('Manage account'));
- expect(fixtures.clerk.openUserProfile).toHaveBeenCalled();
- expect(onDismiss).toHaveBeenCalledTimes(1);
- });
-
it('opens the user button popover when clicked', async () => {
const { wrapper } = await createFixtures(f => {
f.withUser({
diff --git a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx
index e26171b00e8..c9e0434b434 100644
--- a/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx
+++ b/packages/clerk-js/src/ui/contexts/ClerkUIComponentsContext.tsx
@@ -262,8 +262,6 @@ export const useUserButtonContext = () => {
const { displayConfig } = useEnvironment();
const options = useOptions();
- console.log(ctx?.userProfileProps?.customPages);
-
if (componentName !== 'UserButton') {
throw new Error('Clerk: useUserButtonContext called outside of the mounted UserButton component.');
}
From 145534871b323285daf830cc23457053c9c67608 Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Mon, 14 Oct 2024 19:45:09 +0300
Subject: [PATCH 24/36] add jsdoc to
`__experimental_prefetchOrganizationSwitcher` and sanitized children
---
packages/clerk-js/src/core/clerk.ts | 2 +-
.../react/src/components/uiComponents.tsx | 51 ++++++----
packages/react/src/isomorphicClerk.ts | 6 +-
packages/react/src/utils/useCustomPages.tsx | 92 +++++++++++++------
packages/types/src/clerk.ts | 8 +-
5 files changed, 106 insertions(+), 53 deletions(-)
diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts
index 1be5a28897e..dc4b101985d 100644
--- a/packages/clerk-js/src/core/clerk.ts
+++ b/packages/clerk-js/src/core/clerk.ts
@@ -667,7 +667,7 @@ export class Clerk implements ClerkInterface {
void this.#componentControls?.ensureMounted().then(controls => controls.unmountComponent({ node }));
};
- public __internal_prefetchOrganizationSwitcher = () => {
+ public __experimental_prefetchOrganizationSwitcher = () => {
this.assertComponentsReady(this.#componentControls);
void this.#componentControls
?.ensureMounted({ preloadHint: 'OrganizationSwitcher' })
diff --git a/packages/react/src/components/uiComponents.tsx b/packages/react/src/components/uiComponents.tsx
index f0cc6613050..13352a3e40f 100644
--- a/packages/react/src/components/uiComponents.tsx
+++ b/packages/react/src/components/uiComponents.tsx
@@ -36,7 +36,12 @@ import type {
UserProfilePageProps,
WithClerkProp,
} from '../types';
-import { useOrganizationProfileCustomPages, useUserButtonCustomMenuItems, useUserProfileCustomPages } from '../utils';
+import {
+ useOrganizationProfileCustomPages,
+ useSanitizedChildren,
+ useUserButtonCustomMenuItems,
+ useUserProfileCustomPages,
+} from '../utils';
import { withClerk } from './withClerk';
type UserProfileExportType = typeof _UserProfile & {
@@ -230,14 +235,14 @@ export const SignUp = withClerk(({ clerk, ...props }: WithClerkProp
);
}, 'SignUp');
-export function UserProfilePage(_: PropsWithChildren) {
+export function UserProfilePage({ children }: PropsWithChildren) {
logErrorInDevMode(userProfilePageRenderedError);
- return null;
+ return <>{children}>;
}
-export function UserProfileLink(_: PropsWithChildren) {
+export function UserProfileLink({ children }: PropsWithChildren) {
logErrorInDevMode(userProfileLinkRenderedError);
- return null;
+ return <>{children}>;
}
const _UserProfile = withClerk(
@@ -267,9 +272,12 @@ const UserButtonContext = createContext({});
const _UserButton = withClerk(
({ clerk, ...props }: WithClerkProp>) => {
- const { customPages, customPagesPortals } = useUserProfileCustomPages(props.children);
+ const { customPages, customPagesPortals } = useUserProfileCustomPages(props.children, {
+ allowForAnyChildren: !!props.__experimental_asProvider,
+ });
const userProfileProps = Object.assign(props.userProfileProps || {}, { customPages });
const { customMenuItems, customMenuItemsPortals } = useUserButtonCustomMenuItems(props.children);
+ const sanitizedChildren = useSanitizedChildren(props.children);
const passableProps = {
mount: clerk.mountUserButton,
@@ -289,7 +297,7 @@ const _UserButton = withClerk(
hideRootHtmlElement={!!props.__experimental_asProvider}
>
{/*This mimics the previous behaviour before asProvider existed*/}
- {props.__experimental_asProvider ? props.children : null}
+ {props.__experimental_asProvider ? sanitizedChildren : null}
@@ -298,19 +306,19 @@ const _UserButton = withClerk(
'UserButton',
);
-export function MenuItems(_: PropsWithChildren) {
+export function MenuItems({ children }: PropsWithChildren) {
logErrorInDevMode(userButtonMenuItemsRenderedError);
- return null;
+ return <>{children}>;
}
-export function MenuAction(_: PropsWithChildren) {
+export function MenuAction({ children }: PropsWithChildren) {
logErrorInDevMode(userButtonMenuActionRenderedError);
- return null;
+ return <>{children}>;
}
-export function MenuLink(_: PropsWithChildren) {
+export function MenuLink({ children }: PropsWithChildren) {
logErrorInDevMode(userButtonMenuLinkRenderedError);
- return null;
+ return <>{children}>;
}
export function UserButtonOutlet(outletProps: Without) {
@@ -336,14 +344,14 @@ export const UserButton: UserButtonExportType = Object.assign(_UserButton, {
__experimental_Outlet: UserButtonOutlet,
});
-export function OrganizationProfilePage(_: PropsWithChildren) {
+export function OrganizationProfilePage({ children }: PropsWithChildren) {
logErrorInDevMode(organizationProfilePageRenderedError);
- return null;
+ return <>{children}>;
}
-export function OrganizationProfileLink(_: PropsWithChildren) {
+export function OrganizationProfileLink({ children }: PropsWithChildren) {
logErrorInDevMode(organizationProfileLinkRenderedError);
- return null;
+ return <>{children}>;
}
const _OrganizationProfile = withClerk(
@@ -386,8 +394,11 @@ const OrganizationSwitcherContext = createContext(
const _OrganizationSwitcher = withClerk(
({ clerk, ...props }: WithClerkProp>) => {
- const { customPages, customPagesPortals } = useOrganizationProfileCustomPages(props.children);
+ const { customPages, customPagesPortals } = useOrganizationProfileCustomPages(props.children, {
+ allowForAnyChildren: !!props.__experimental_asProvider,
+ });
const organizationProfileProps = Object.assign(props.organizationProfileProps || {}, { customPages });
+ const sanitizedChildren = useSanitizedChildren(props.children);
const passableProps = {
mount: clerk.mountOrganizationSwitcher,
@@ -399,7 +410,7 @@ const _OrganizationSwitcher = withClerk(
/**
* Prefetch organization list
*/
- clerk.__internal_prefetchOrganizationSwitcher();
+ clerk.__experimental_prefetchOrganizationSwitcher();
return (
@@ -408,7 +419,7 @@ const _OrganizationSwitcher = withClerk(
hideRootHtmlElement={!!props.__experimental_asProvider}
>
{/*This mimics the previous behaviour before asProvider existed*/}
- {props.__experimental_asProvider ? props.children : null}
+ {props.__experimental_asProvider ? sanitizedChildren : null}
diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts
index cba1b240571..3a94a521f9a 100644
--- a/packages/react/src/isomorphicClerk.ts
+++ b/packages/react/src/isomorphicClerk.ts
@@ -841,12 +841,12 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
}
};
- __internal_prefetchOrganizationSwitcher = (): void => {
- const callback = () => this.clerkjs?.__internal_prefetchOrganizationSwitcher();
+ __experimental_prefetchOrganizationSwitcher = (): void => {
+ const callback = () => this.clerkjs?.__experimental_prefetchOrganizationSwitcher();
if (this.clerkjs && this.#loaded) {
void callback();
} else {
- this.premountMethodCalls.set('__internal_prefetchOrganizationSwitcher', callback);
+ this.premountMethodCalls.set('__experimental_prefetchOrganizationSwitcher', callback);
}
};
diff --git a/packages/react/src/utils/useCustomPages.tsx b/packages/react/src/utils/useCustomPages.tsx
index 140a21c5751..dd541152807 100644
--- a/packages/react/src/utils/useCustomPages.tsx
+++ b/packages/react/src/utils/useCustomPages.tsx
@@ -10,33 +10,45 @@ import {
UserProfileLink,
UserProfilePage,
} from '../components/uiComponents';
-import { customLinkWrongProps, customPageWrongProps } from '../errors/messages';
+import { customLinkWrongProps, customPagesIgnoredComponent, customPageWrongProps } from '../errors/messages';
import type { UserProfilePageProps } from '../types';
import { isThatComponent } from './componentValidation';
import type { UseCustomElementPortalParams, UseCustomElementPortalReturn } from './useCustomElementPortal';
import { useCustomElementPortal } from './useCustomElementPortal';
-export const useUserProfileCustomPages = (children: React.ReactNode | React.ReactNode[]) => {
+export const useUserProfileCustomPages = (
+ children: React.ReactNode | React.ReactNode[],
+ options?: UseCustomPagesOptions,
+) => {
const reorderItemsLabels = ['account', 'security'];
- return useCustomPages({
- children,
- reorderItemsLabels,
- LinkComponent: UserProfileLink,
- PageComponent: UserProfilePage,
- MenuItemsComponent: MenuItems,
- componentName: 'UserProfile',
- });
+ return useCustomPages(
+ {
+ children,
+ reorderItemsLabels,
+ LinkComponent: UserProfileLink,
+ PageComponent: UserProfilePage,
+ MenuItemsComponent: MenuItems,
+ componentName: 'UserProfile',
+ },
+ options,
+ );
};
-export const useOrganizationProfileCustomPages = (children: React.ReactNode | React.ReactNode[]) => {
+export const useOrganizationProfileCustomPages = (
+ children: React.ReactNode | React.ReactNode[],
+ options?: UseCustomPagesOptions,
+) => {
const reorderItemsLabels = ['general', 'members'];
- return useCustomPages({
- children,
- reorderItemsLabels,
- LinkComponent: OrganizationProfileLink,
- PageComponent: OrganizationProfilePage,
- componentName: 'OrganizationProfile',
- });
+ return useCustomPages(
+ {
+ children,
+ reorderItemsLabels,
+ LinkComponent: OrganizationProfileLink,
+ PageComponent: OrganizationProfilePage,
+ componentName: 'OrganizationProfile',
+ },
+ options,
+ );
};
type UseCustomPagesParams = {
@@ -48,16 +60,40 @@ type UseCustomPagesParams = {
componentName: string;
};
+type UseCustomPagesOptions = {
+ allowForAnyChildren: boolean;
+};
+
type CustomPageWithIdType = UserProfilePageProps & { children?: React.ReactNode };
-const useCustomPages = ({
- children,
- LinkComponent,
- PageComponent,
- MenuItemsComponent,
- reorderItemsLabels,
- componentName,
-}: UseCustomPagesParams) => {
+export const useSanitizedChildren = (children: React.ReactNode) => {
+ const sanitizedChildren: React.ReactNode[] = [];
+
+ const excludedComponents: any[] = [
+ OrganizationProfileLink,
+ OrganizationProfilePage,
+ MenuItems,
+ UserProfilePage,
+ UserProfileLink,
+ ];
+
+ React.Children.forEach(children, child => {
+ if (
+ !excludedComponents.some(component => isThatComponent(child, component))
+ // !isThatComponent(child, PageComponent) &&
+ // !isThatComponent(child, LinkComponent) &&
+ // !isThatComponent(child, MenuItemsComponent)
+ ) {
+ sanitizedChildren.push(child);
+ }
+ });
+
+ return sanitizedChildren;
+};
+
+const useCustomPages = (params: UseCustomPagesParams, options?: UseCustomPagesOptions) => {
+ const { children, LinkComponent, PageComponent, MenuItemsComponent, reorderItemsLabels, componentName } = params;
+ const { allowForAnyChildren = false } = options || {};
const validChildren: CustomPageWithIdType[] = [];
React.Children.forEach(children, child => {
@@ -66,8 +102,8 @@ const useCustomPages = ({
!isThatComponent(child, LinkComponent) &&
!isThatComponent(child, MenuItemsComponent)
) {
- if (child) {
- // logErrorInDevMode(customPagesIgnoredComponent(componentName));
+ if (child && !allowForAnyChildren) {
+ logErrorInDevMode(customPagesIgnoredComponent(componentName));
}
return;
}
diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts
index 98ac89d8ed8..83f9e7135cb 100644
--- a/packages/types/src/clerk.ts
+++ b/packages/types/src/clerk.ts
@@ -329,7 +329,13 @@ export interface Clerk {
*/
unmountOrganizationSwitcher: (targetNode: HTMLDivElement) => void;
- __internal_prefetchOrganizationSwitcher: () => void;
+ /**
+ * Prefetches the data displayed by an organization switcher.
+ * It can be used when `mountOrganizationSwitcher({ asStandalone: true})`, to avoid unwanted loading states.
+ * @experimantal This API is still under active development and may change at any moment.
+ * @param props Optional user verification configuration parameters.
+ */
+ __experimental_prefetchOrganizationSwitcher: () => void;
/**
* Mount an organization list component at the target element.
From b8f2f7e120c519e6ee6fbc83399e9a4a8dfa6ab8 Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Mon, 14 Oct 2024 19:56:51 +0300
Subject: [PATCH 25/36] update bundlewatch.config.json
---
packages/clerk-js/bundlewatch.config.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json
index 529b3ec7757..6993b33629c 100644
--- a/packages/clerk-js/bundlewatch.config.json
+++ b/packages/clerk-js/bundlewatch.config.json
@@ -1,6 +1,6 @@
{
"files": [
- { "path": "./dist/clerk.browser.js", "maxSize": "64.6kB" },
+ { "path": "./dist/clerk.browser.js", "maxSize": "64.7kB" },
{ "path": "./dist/clerk.headless.js", "maxSize": "43kB" },
{ "path": "./dist/ui-common*.js", "maxSize": "86KB" },
{ "path": "./dist/vendors*.js", "maxSize": "70KB" },
From 131e99cc10207f0293207a3d51dc35edee37f326 Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Mon, 14 Oct 2024 20:12:32 +0300
Subject: [PATCH 26/36] cleanup
---
packages/react/src/utils/useCustomMenuItems.tsx | 3 ++-
packages/react/src/utils/useCustomPages.tsx | 7 +------
2 files changed, 3 insertions(+), 7 deletions(-)
diff --git a/packages/react/src/utils/useCustomMenuItems.tsx b/packages/react/src/utils/useCustomMenuItems.tsx
index 30b8a741c8d..239a1e0bbf8 100644
--- a/packages/react/src/utils/useCustomMenuItems.tsx
+++ b/packages/react/src/utils/useCustomMenuItems.tsx
@@ -6,6 +6,7 @@ import React from 'react';
import { MenuAction, MenuItems, MenuLink, UserProfileLink, UserProfilePage } from '../components/uiComponents';
import {
customMenuItemsIgnoredComponent,
+ userButtonIgnoredComponent,
userButtonMenuItemLinkWrongProps,
userButtonMenuItemsActionWrongsProps,
} from '../errors/messages';
@@ -59,7 +60,7 @@ const useCustomMenuItems = ({
!isThatComponent(child, UserProfilePageComponent)
) {
if (child) {
- // logErrorInDevMode(userButtonIgnoredComponent);
+ logErrorInDevMode(userButtonIgnoredComponent);
}
return;
}
diff --git a/packages/react/src/utils/useCustomPages.tsx b/packages/react/src/utils/useCustomPages.tsx
index dd541152807..d50357adaf5 100644
--- a/packages/react/src/utils/useCustomPages.tsx
+++ b/packages/react/src/utils/useCustomPages.tsx
@@ -78,12 +78,7 @@ export const useSanitizedChildren = (children: React.ReactNode) => {
];
React.Children.forEach(children, child => {
- if (
- !excludedComponents.some(component => isThatComponent(child, component))
- // !isThatComponent(child, PageComponent) &&
- // !isThatComponent(child, LinkComponent) &&
- // !isThatComponent(child, MenuItemsComponent)
- ) {
+ if (!excludedComponents.some(component => isThatComponent(child, component))) {
sanitizedChildren.push(child);
}
});
From 727ffedc670660c580971ccf209772ecb3b86244 Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Mon, 14 Oct 2024 20:15:45 +0300
Subject: [PATCH 27/36] bring back pages
---
playground/app-router/src/app/page.tsx | 100 ++++++++++++++++++
.../src/pages/user/[[...index]].tsx | 25 +++++
2 files changed, 125 insertions(+)
create mode 100644 playground/app-router/src/app/page.tsx
create mode 100644 playground/app-router/src/pages/user/[[...index]].tsx
diff --git a/playground/app-router/src/app/page.tsx b/playground/app-router/src/app/page.tsx
new file mode 100644
index 00000000000..2df822ec601
--- /dev/null
+++ b/playground/app-router/src/app/page.tsx
@@ -0,0 +1,100 @@
+import Image from 'next/image';
+import styles from './page.module.css';
+
+declare global {
+ interface UserPublicMetadata {
+ spotifyToken?: string;
+ isNewUser?: boolean;
+ }
+}
+
+export default function Home() {
+ return (
+
+
+
+ Get started by editing
+ src/app/page.tsx
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/playground/app-router/src/pages/user/[[...index]].tsx b/playground/app-router/src/pages/user/[[...index]].tsx
new file mode 100644
index 00000000000..965be25b361
--- /dev/null
+++ b/playground/app-router/src/pages/user/[[...index]].tsx
@@ -0,0 +1,25 @@
+import { SignedIn, UserProfile } from '@clerk/nextjs';
+import { getAuth } from '@clerk/nextjs/server';
+import type { GetServerSideProps, NextPage } from 'next';
+import React from 'react';
+
+export const getServerSideProps: GetServerSideProps = async ({ req }) => {
+ const { userId } = getAuth(req);
+ console.log(userId);
+ return { props: { message: 'hello from server' } };
+};
+
+const UserProfilePage: NextPage = (props: any) => {
+ return (
+
+
/pages/user
+
{props.message}
+
+ SignedIn
+
+
+
+ );
+};
+
+export default UserProfilePage;
\ No newline at end of file
From 2f940fd9dcccee6aa1cfc23645fcb550f10eaad5 Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Mon, 14 Oct 2024 20:26:33 +0300
Subject: [PATCH 28/36] revert necessary changes
---
.../ui/components/UserButton/UserButtonPopover.tsx | 12 +++---------
packages/clerk-js/src/ui/hooks/usePopover.ts | 11 +----------
2 files changed, 4 insertions(+), 19 deletions(-)
diff --git a/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx b/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx
index 368c1302c79..ceb529e1d6a 100644
--- a/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx
+++ b/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx
@@ -9,13 +9,11 @@ import type { PropsOfComponent } from '../../styledSystem';
import { MultiSessionActions, SignOutAllActions, SingleSessionActions } from './SessionActions';
import { useMultisessionActions } from './useMultisessionActions';
-type UserButtonPopoverProps = {
- close?: (open: boolean | ((prevState: boolean) => boolean)) => void;
-} & PropsOfComponent;
+type UserButtonPopoverProps = { close?: () => void } & PropsOfComponent;
export const UserButtonPopover = React.forwardRef((props, ref) => {
const { close: optionalClose, ...rest } = props;
- const close = () => optionalClose?.(false);
+ const close = () => optionalClose?.();
const { session } = useSession() as { session: ActiveSessionResource };
const { authConfig } = useEnvironment();
const { user } = useUser();
@@ -27,11 +25,7 @@ export const UserButtonPopover = React.forwardRef
diff --git a/packages/clerk-js/src/ui/hooks/usePopover.ts b/packages/clerk-js/src/ui/hooks/usePopover.ts
index 5cf7ce4e4d3..662556cd305 100644
--- a/packages/clerk-js/src/ui/hooks/usePopover.ts
+++ b/packages/clerk-js/src/ui/hooks/usePopover.ts
@@ -4,8 +4,6 @@ import React, { useEffect } from 'react';
type UsePopoverProps = {
defaultOpen?: boolean;
- open?: boolean;
- onOpenChanged?: (open: boolean | ((prevState: boolean) => boolean)) => void;
placement?: UseFloatingOptions['placement'];
offset?: Parameters[0];
shoudFlip?: boolean;
@@ -25,16 +23,9 @@ export type UsePopoverReturn = ReturnType;
export const usePopover = (props: UsePopoverProps = {}) => {
const { bubbles = false, shoudFlip = true, outsidePress, adjustToReferenceWidth = false, referenceElement } = props;
- const [isOpen_internal, setIsOpen_internal] = React.useState(props.defaultOpen || false);
-
- const isOpen = typeof props.open === 'undefined' ? isOpen_internal : props.open;
- const setIsOpen = typeof props.onOpenChanged === 'undefined' ? setIsOpen_internal : props.onOpenChanged;
+ const [isOpen, setIsOpen] = React.useState(props.defaultOpen || false);
const nodeId = useFloatingNodeId();
- if (typeof props.defaultOpen !== 'undefined' && typeof props.open !== 'undefined') {
- console.warn('Both defaultOpen and open are set. `defaultOpen` will be ignored');
- }
-
const { update, refs, strategy, x, y, context } = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
From 25168154f39e1a94b8f91a56007e8b707f8d5fe6 Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Tue, 15 Oct 2024 10:10:05 +0300
Subject: [PATCH 29/36] Apply suggestions from code review
Co-authored-by: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
---
integration/tests/custom-pages.test.ts | 2 +-
packages/clerk-js/src/ui/hooks/usePopover.ts | 1 -
packages/react/src/components/uiComponents.tsx | 13 ++++++++++---
3 files changed, 11 insertions(+), 5 deletions(-)
diff --git a/integration/tests/custom-pages.test.ts b/integration/tests/custom-pages.test.ts
index fb5d7d924a8..f7621bb9ffb 100644
--- a/integration/tests/custom-pages.test.ts
+++ b/integration/tests/custom-pages.test.ts
@@ -112,7 +112,7 @@ testAgainstRunningApps({ withPattern: ['react.vite.withEmailCodes'] })(
await expect(u.page.locator('p[data-page="1"]')).toHaveText('Counter: 1');
});
- test('does not leak children', async ({ page, context }) => {
+ test('renders only custom pages and does not display unrelated child components', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.po.signIn.goTo();
await u.po.signIn.waitForMounted();
diff --git a/packages/clerk-js/src/ui/hooks/usePopover.ts b/packages/clerk-js/src/ui/hooks/usePopover.ts
index 662556cd305..21f65a2b7d9 100644
--- a/packages/clerk-js/src/ui/hooks/usePopover.ts
+++ b/packages/clerk-js/src/ui/hooks/usePopover.ts
@@ -25,7 +25,6 @@ export const usePopover = (props: UsePopoverProps = {}) => {
const { bubbles = false, shoudFlip = true, outsidePress, adjustToReferenceWidth = false, referenceElement } = props;
const [isOpen, setIsOpen] = React.useState(props.defaultOpen || false);
const nodeId = useFloatingNodeId();
-
const { update, refs, strategy, x, y, context } = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
diff --git a/packages/react/src/components/uiComponents.tsx b/packages/react/src/components/uiComponents.tsx
index 13352a3e40f..9f24d397380 100644
--- a/packages/react/src/components/uiComponents.tsx
+++ b/packages/react/src/components/uiComponents.tsx
@@ -268,7 +268,11 @@ export const UserProfile: UserProfileExportType = Object.assign(_UserProfile, {
});
// @ts-ignore
-const UserButtonContext = createContext({});
+const UserButtonContext = createContext({
+ mount: () => {},
+ unmount: () => {},
+ updateProps: () => {},
+});
const _UserButton = withClerk(
({ clerk, ...props }: WithClerkProp>) => {
@@ -388,8 +392,11 @@ export const CreateOrganization = withClerk(({ clerk, ...props }: WithClerkProp<
}, 'CreateOrganization');
const OrganizationSwitcherContext = createContext(
- // @ts-expect-error We are defining the values below
- {},
+ {
+ mount: () => {},
+ unmount: () => {},
+ updateProps: () => {},
+ },
);
const _OrganizationSwitcher = withClerk(
From 237af16fc92c96c9056b15142b60faa3c549705d Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Tue, 15 Oct 2024 10:25:30 +0300
Subject: [PATCH 30/36] update changeset
---
.changeset/shaggy-kids-fail.md | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/.changeset/shaggy-kids-fail.md b/.changeset/shaggy-kids-fail.md
index d24776a1cc7..c5f034a8f67 100644
--- a/.changeset/shaggy-kids-fail.md
+++ b/.changeset/shaggy-kids-fail.md
@@ -3,5 +3,10 @@
'@clerk/types': minor
---
-Add experimental support for a standalone mode for UserButton and OrganizationSwitcher.
-When `__experimental_standalone: true` the component will not render its trigger and instead it will render only the contents of the popover in place.
+Add experimental standalone mode for UserButton and OrganizationSwitcher.
+When `__experimental_asStandalone: true` the component will not render its trigger, and instead it will render only the contents of the popover in place.
+
+APIs that changed:
+- (For internal usage) Added `__experimental_prefetchOrganizationSwitcher` as a way to mount an internal component that will render the `useOrganizationList` hook and prefetch the necessary data for the popover of OrganizationSwitcher. This enhances the UX since no loading state will be visible, and keeps CLS to the minimum.
+- New property for `mountOrganizationSwitcher(node, { __experimental_asStandalone: true })`
+- New property for `mountUserButton(node, { __experimental_asStandalone: true })`
From 957661cb599d5cb6a6d295cf5e34dd665d6c1035 Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Tue, 15 Oct 2024 10:30:55 +0300
Subject: [PATCH 31/36] add changeset for react
---
.changeset/clean-mugs-wave.md | 26 ++++++++++++++++++++++++++
1 file changed, 26 insertions(+)
create mode 100644 .changeset/clean-mugs-wave.md
diff --git a/.changeset/clean-mugs-wave.md b/.changeset/clean-mugs-wave.md
new file mode 100644
index 00000000000..c4d26841591
--- /dev/null
+++ b/.changeset/clean-mugs-wave.md
@@ -0,0 +1,26 @@
+---
+"@clerk/clerk-react": minor
+---
+
+Introducing experimental `asProvider`, `asStandalone` and ` ` for UserButton and OrganizationSwitcher.
+- `asProvider` converts UserButton and OrganizationSwitcher to a provider that defers rendering until `Outlet` is mounted.
+- `Outlet` also accepts `asStandalone` which will skip the trigger of these components and display only the UI of was previously inside the popover. This allows developers to create their own triggers.
+
+Example usage:
+```tsx
+
+
+ This is my page available to all children
+
+
+
+```
+
+```tsx
+
+
+ This is my page available to all children
+
+
+
+```
From 7b7f8d588233cf0d9c8f88b9ca3d99e7135dddda Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Tue, 15 Oct 2024 10:45:33 +0300
Subject: [PATCH 32/36] fix formatting
---
packages/react/src/components/uiComponents.tsx | 12 +++++-------
1 file changed, 5 insertions(+), 7 deletions(-)
diff --git a/packages/react/src/components/uiComponents.tsx b/packages/react/src/components/uiComponents.tsx
index 9f24d397380..bc02da7f8cb 100644
--- a/packages/react/src/components/uiComponents.tsx
+++ b/packages/react/src/components/uiComponents.tsx
@@ -391,13 +391,11 @@ export const CreateOrganization = withClerk(({ clerk, ...props }: WithClerkProp<
);
}, 'CreateOrganization');
-const OrganizationSwitcherContext = createContext(
- {
- mount: () => {},
- unmount: () => {},
- updateProps: () => {},
- },
-);
+const OrganizationSwitcherContext = createContext({
+ mount: () => {},
+ unmount: () => {},
+ updateProps: () => {},
+});
const _OrganizationSwitcher = withClerk(
({ clerk, ...props }: WithClerkProp>) => {
From 3ef3bb5e92a243050d58a32b7894101e9a5d9ae7 Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Tue, 15 Oct 2024 15:55:39 +0300
Subject: [PATCH 33/36] Apply suggestions from code review
Co-authored-by: Lennart
---
.changeset/clean-mugs-wave.md | 6 +++---
.changeset/shaggy-kids-fail.md | 8 ++++----
packages/react/src/components/uiComponents.tsx | 12 ++++++------
3 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/.changeset/clean-mugs-wave.md b/.changeset/clean-mugs-wave.md
index c4d26841591..12f8f71c150 100644
--- a/.changeset/clean-mugs-wave.md
+++ b/.changeset/clean-mugs-wave.md
@@ -2,9 +2,9 @@
"@clerk/clerk-react": minor
---
-Introducing experimental `asProvider`, `asStandalone` and ` ` for UserButton and OrganizationSwitcher.
-- `asProvider` converts UserButton and OrganizationSwitcher to a provider that defers rendering until `Outlet` is mounted.
-- `Outlet` also accepts `asStandalone` which will skip the trigger of these components and display only the UI of was previously inside the popover. This allows developers to create their own triggers.
+Introducing experimental `asProvider`, `asStandalone`, and ` ` for ` ` and ` ` components.
+- `asProvider` converts ` ` and ` ` to a provider that defers rendering until ` ` is mounted.
+- ` ` also accepts a `asStandalone` prop. It will skip the trigger of these components and display only the UI which was previously inside the popover. This allows developers to create their own triggers.
Example usage:
```tsx
diff --git a/.changeset/shaggy-kids-fail.md b/.changeset/shaggy-kids-fail.md
index c5f034a8f67..31ee5c991d3 100644
--- a/.changeset/shaggy-kids-fail.md
+++ b/.changeset/shaggy-kids-fail.md
@@ -3,10 +3,10 @@
'@clerk/types': minor
---
-Add experimental standalone mode for UserButton and OrganizationSwitcher.
+Add experimental standalone mode for ` ` and ` `.
When `__experimental_asStandalone: true` the component will not render its trigger, and instead it will render only the contents of the popover in place.
APIs that changed:
-- (For internal usage) Added `__experimental_prefetchOrganizationSwitcher` as a way to mount an internal component that will render the `useOrganizationList` hook and prefetch the necessary data for the popover of OrganizationSwitcher. This enhances the UX since no loading state will be visible, and keeps CLS to the minimum.
-- New property for `mountOrganizationSwitcher(node, { __experimental_asStandalone: true })`
-- New property for `mountUserButton(node, { __experimental_asStandalone: true })`
+- (For internal usage) Added `__experimental_prefetchOrganizationSwitcher` as a way to mount an internal component that will render the `useOrganizationList()` hook and prefetch the necessary data for the popover of ` `. This enhances the UX since no loading state will be visible and keeps CLS to the minimum.
+- New property for `mountOrganizationSwitcher(node, { __experimental_asStandalone: true })`
+- New property for `mountUserButton(node, { __experimental_asStandalone: true })`
diff --git a/packages/react/src/components/uiComponents.tsx b/packages/react/src/components/uiComponents.tsx
index bc02da7f8cb..b6bff376cd3 100644
--- a/packages/react/src/components/uiComponents.tsx
+++ b/packages/react/src/components/uiComponents.tsx
@@ -56,8 +56,8 @@ type UserButtonExportType = typeof _UserButton & {
Action: typeof MenuAction;
Link: typeof MenuLink;
/**
- * The `Outlet` component can be used in conjunction with `asProvider` in order to control rendering
- * of the OrganizationSwitcher without affecting its configuration or any custom pages
+ * The ` ` component can be used in conjunction with `asProvider` in order to control rendering
+ * of the ` ` without affecting its configuration or any custom pages
* that could be mounted
* @experimental This API is experimental and may change at any moment.
*/
@@ -70,7 +70,7 @@ type UserButtonPropsWithoutCustomPages = Without<
> & {
userProfileProps?: Pick;
/**
- * Adding `asProvider` will defer rendering until the `Outlet` component is mounted.
+ * Adding `asProvider` will defer rendering until the ` ` component is mounted.
* @experimental This API is experimental and may change at any moment.
* @default undefined
*/
@@ -86,8 +86,8 @@ type OrganizationSwitcherExportType = typeof _OrganizationSwitcher & {
OrganizationProfilePage: typeof OrganizationProfilePage;
OrganizationProfileLink: typeof OrganizationProfileLink;
/**
- * The `Outlet` component can be used in conjunction with `asProvider` in order to control rendering
- * of the OrganizationSwitcher without affecting its configuration or any custom pages
+ * The ` ` component can be used in conjunction with `asProvider` in order to control rendering
+ * of the ` ` without affecting its configuration or any custom pages
* that could be mounted
* @experimental This API is experimental and may change at any moment.
*/
@@ -100,7 +100,7 @@ type OrganizationSwitcherPropsWithoutCustomPages = Without<
> & {
organizationProfileProps?: Pick;
/**
- * Adding `asProvider` will defer rendering until the `Outlet` component is mounted.
+ * Adding `asProvider` will defer rendering until the ` ` component is mounted.
* @experimental This API is experimental and may change at any moment.
* @default undefined
*/
From a84b075db2a0338d1e52135a84ee19780ca6db52 Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Tue, 15 Oct 2024 16:04:56 +0300
Subject: [PATCH 34/36] address review comments
---
.changeset/clean-mugs-wave.md | 8 ++++----
.../OrganizationSwitcherPopover.tsx | 4 ++--
.../ui/components/UserButton/UserButtonPopover.tsx | 4 ++--
packages/react/src/components/uiComponents.tsx | 1 -
packages/react/src/utils/useCustomPages.tsx | 14 ++++++++++++++
packages/types/src/clerk.ts | 4 ++--
6 files changed, 24 insertions(+), 11 deletions(-)
diff --git a/.changeset/clean-mugs-wave.md b/.changeset/clean-mugs-wave.md
index 12f8f71c150..1b73dbeeb5c 100644
--- a/.changeset/clean-mugs-wave.md
+++ b/.changeset/clean-mugs-wave.md
@@ -8,19 +8,19 @@ Introducing experimental `asProvider`, `asStandalone`, and ` ` for `<
Example usage:
```tsx
-
+
This is my page available to all children
-
+
```
```tsx
-
+
This is my page available to all children
-
+
```
diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherPopover.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherPopover.tsx
index 7b9f8c8ba7e..1b24d09904b 100644
--- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherPopover.tsx
+++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherPopover.tsx
@@ -26,8 +26,8 @@ type OrganizationSwitcherPopoverProps = {
export const OrganizationSwitcherPopover = React.forwardRef(
(props, ref) => {
- const { close: undefinedClose, ...rest } = props;
- const close = () => undefinedClose?.(false);
+ const { close: unsafeClose, ...rest } = props;
+ const close = () => unsafeClose?.(false);
const card = useCardState();
const { openOrganizationProfile, openCreateOrganization } = useClerk();
const { organization: currentOrg } = useOrganization();
diff --git a/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx b/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx
index ceb529e1d6a..19f6ef34633 100644
--- a/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx
+++ b/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx
@@ -12,8 +12,8 @@ import { useMultisessionActions } from './useMultisessionActions';
type UserButtonPopoverProps = { close?: () => void } & PropsOfComponent;
export const UserButtonPopover = React.forwardRef((props, ref) => {
- const { close: optionalClose, ...rest } = props;
- const close = () => optionalClose?.();
+ const { close: unsafeClose, ...rest } = props;
+ const close = () => unsafeClose?.();
const { session } = useSession() as { session: ActiveSessionResource };
const { authConfig } = useEnvironment();
const { user } = useUser();
diff --git a/packages/react/src/components/uiComponents.tsx b/packages/react/src/components/uiComponents.tsx
index b6bff376cd3..d4c589ced15 100644
--- a/packages/react/src/components/uiComponents.tsx
+++ b/packages/react/src/components/uiComponents.tsx
@@ -267,7 +267,6 @@ export const UserProfile: UserProfileExportType = Object.assign(_UserProfile, {
Link: UserProfileLink,
});
-// @ts-ignore
const UserButtonContext = createContext({
mount: () => {},
unmount: () => {},
diff --git a/packages/react/src/utils/useCustomPages.tsx b/packages/react/src/utils/useCustomPages.tsx
index d50357adaf5..c3367d1cc7b 100644
--- a/packages/react/src/utils/useCustomPages.tsx
+++ b/packages/react/src/utils/useCustomPages.tsx
@@ -66,6 +66,20 @@ type UseCustomPagesOptions = {
type CustomPageWithIdType = UserProfilePageProps & { children?: React.ReactNode };
+/**
+ * Exclude any children that is used for identifying Custom Pages or Custom Items.
+ * Passing:
+ * ```tsx
+ *
+ *
+ *
+ *
+ * ```
+ * Gives back
+ * ```tsx
+ *
+ * ````
+ */
export const useSanitizedChildren = (children: React.ReactNode) => {
const sanitizedChildren: React.ReactNode[] = [];
diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts
index 83f9e7135cb..7f7e11cd139 100644
--- a/packages/types/src/clerk.ts
+++ b/packages/types/src/clerk.ts
@@ -1045,7 +1045,7 @@ export type UserButtonProps = UserButtonProfileMode & {
defaultOpen?: boolean;
/**
- * If true the UserButton will only render the popover.
+ * If true the ` ` will only render the popover.
* Enables developers to implement a custom dialog.
* @experimental This API is experimental and may change at any moment.
* @default undefined
@@ -1114,7 +1114,7 @@ export type OrganizationSwitcherProps = CreateOrganizationMode &
defaultOpen?: boolean;
/**
- * If true, OrganizationSwitcher will only render the popover.
+ * If true, ` ` will only render the popover.
* Enables developers to implement a custom dialog.
* @experimental This API is experimental and may change at any moment.
* @default undefined
From 1d67a4520a485f7480616603c4d3b4f7dfe203e3 Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Tue, 15 Oct 2024 16:26:29 +0300
Subject: [PATCH 35/36] update bundlewatch.config.json
---
packages/clerk-js/bundlewatch.config.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json
index 6993b33629c..af372ae8e98 100644
--- a/packages/clerk-js/bundlewatch.config.json
+++ b/packages/clerk-js/bundlewatch.config.json
@@ -1,6 +1,6 @@
{
"files": [
- { "path": "./dist/clerk.browser.js", "maxSize": "64.7kB" },
+ { "path": "./dist/clerk.browser.js", "maxSize": "64.8kB" },
{ "path": "./dist/clerk.headless.js", "maxSize": "43kB" },
{ "path": "./dist/ui-common*.js", "maxSize": "86KB" },
{ "path": "./dist/vendors*.js", "maxSize": "70KB" },
From d4b8a123735a1d94f97e65f143c91bf9e0e0abe3 Mon Sep 17 00:00:00 2001
From: panteliselef
Date: Wed, 16 Oct 2024 12:33:28 +0300
Subject: [PATCH 36/36] when popover with asStandalone avoid entry animations
---
.../OrganizationSwitcherPopover.tsx | 2 ++
.../UserButton/UserButtonPopover.tsx | 2 ++
.../clerk-js/src/ui/elements/PopoverCard.tsx | 36 +++++++++++++------
3 files changed, 29 insertions(+), 11 deletions(-)
diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherPopover.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherPopover.tsx
index 1b24d09904b..1fbb9436e91 100644
--- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherPopover.tsx
+++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherPopover.tsx
@@ -29,6 +29,7 @@ export const OrganizationSwitcherPopover = React.forwardRef unsafeClose?.(false);
const card = useCardState();
+ const { __experimental_asStandalone } = useOrganizationSwitcherContext();
const { openOrganizationProfile, openCreateOrganization } = useClerk();
const { organization: currentOrg } = useOrganization();
const { isLoaded, setActive } = useOrganizationList();
@@ -194,6 +195,7 @@ export const OrganizationSwitcherPopover = React.forwardRef
diff --git a/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx b/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx
index 19f6ef34633..114a6ed8b8e 100644
--- a/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx
+++ b/packages/clerk-js/src/ui/components/UserButton/UserButtonPopover.tsx
@@ -15,6 +15,7 @@ export const UserButtonPopover = React.forwardRef unsafeClose?.();
const { session } = useSession() as { session: ActiveSessionResource };
+ const { __experimental_asStandalone } = useUserButtonContext();
const { authConfig } = useEnvironment();
const { user } = useUser();
const {
@@ -34,6 +35,7 @@ export const UserButtonPopover = React.forwardRef
diff --git a/packages/clerk-js/src/ui/elements/PopoverCard.tsx b/packages/clerk-js/src/ui/elements/PopoverCard.tsx
index d3446c130e8..9bfe49ea425 100644
--- a/packages/clerk-js/src/ui/elements/PopoverCard.tsx
+++ b/packages/clerk-js/src/ui/elements/PopoverCard.tsx
@@ -3,27 +3,41 @@ import React from 'react';
import { useEnvironment } from '../contexts';
import { Col, descriptors, Flex, Flow, useAppearance } from '../customizables';
import type { ElementDescriptor } from '../customizables/elementDescriptors';
-import type { PropsOfComponent } from '../styledSystem';
+import type { PropsOfComponent, ThemableCssProp } from '../styledSystem';
import { animations, common } from '../styledSystem';
import { colors } from '../utils';
import { Card } from '.';
-const PopoverCardRoot = React.forwardRef>((props, ref) => {
- const { elementDescriptor, ...rest } = props;
+const PopoverCardRoot = React.forwardRef<
+ HTMLDivElement,
+ PropsOfComponent & {
+ shouldEntryAnimate?: boolean;
+ }
+>((props, ref) => {
+ const { elementDescriptor, shouldEntryAnimate = true, ...rest } = props;
+
+ const withAnimation: ThemableCssProp = t => ({
+ animation: shouldEntryAnimate
+ ? `${animations.dropdownSlideInScaleAndFade} ${t.transitionDuration.$fast}`
+ : undefined,
+ });
+
return (
({
- width: t.sizes.$94,
- maxWidth: `calc(100vw - ${t.sizes.$8})`,
- zIndex: t.zIndices.$modal,
- borderRadius: t.radii.$xl,
- animation: `${animations.dropdownSlideInScaleAndFade} ${t.transitionDuration.$fast}`,
- outline: 'none',
- })}
+ sx={[
+ t => ({
+ width: t.sizes.$94,
+ maxWidth: `calc(100vw - ${t.sizes.$8})`,
+ zIndex: t.zIndices.$modal,
+ borderRadius: t.radii.$xl,
+ outline: 'none',
+ }),
+ withAnimation,
+ ]}
>
{props.children}