From a07e817bf9042fe340bcf8d209d259688595d940 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Mon, 27 Jan 2025 17:13:38 -0500 Subject: [PATCH 1/7] fix(clerk-js): Ensure only one action is present within EmailSection UI --- .../components/UserProfile/EmailsSection.tsx | 113 ++++++++++++------ .../src/ui/elements/Action/ActionRoot.tsx | 35 ++++-- 2 files changed, 103 insertions(+), 45 deletions(-) diff --git a/packages/clerk-js/src/ui/components/UserProfile/EmailsSection.tsx b/packages/clerk-js/src/ui/components/UserProfile/EmailsSection.tsx index 3f4558e7178..327654aab41 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/EmailsSection.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/EmailsSection.tsx @@ -1,5 +1,6 @@ import { useUser } from '@clerk/shared/react'; -import type { EmailAddressResource } from '@clerk/types'; +import type { EmailAddressResource, UserResource } from '@clerk/types'; +import { useEffect, useState } from 'react'; import { sortIdentificationBasedOnVerification } from '../../components/UserProfile/utils'; import { Badge, Flex, localizationKeys, Text } from '../../customizables'; @@ -37,47 +38,26 @@ const EmailScreen = (props: EmailScreenProps) => { export const EmailsSection = ({ shouldAllowCreation = true }) => { const { user } = useUser(); - + const [actionRootValue, setActionRootValue] = useState(null); return ( - + {sortIdentificationBasedOnVerification(user?.emailAddresses, user?.primaryEmailAddressId).map(email => ( - - - ({ overflow: 'hidden', gap: t.space.$1 })}> - ({ color: t.colors.$colorText })} - truncate - > - {email.emailAddress} - - {user?.primaryEmailAddressId === email.id && ( - - )} - {email.verification.status !== 'verified' && ( - - )} - - - - - - - - - - - - - - - - + ))} {shouldAllowCreation && ( <> @@ -100,7 +80,65 @@ export const EmailsSection = ({ shouldAllowCreation = true }) => { ); }; -const EmailMenu = ({ email }: { email: EmailAddressResource }) => { +const EmailRow = ({ + user, + email, + actionRootValue, + setActionRootValue, +}: { + user: UserResource | null | undefined; + email: EmailAddressResource; + actionRootValue?: string | null; + setActionRootValue: (value: string | null) => void; +}) => { + const [internalValue, setInternalValue] = useState(null); + + useEffect(() => { + if (actionRootValue === 'add') { + setInternalValue(null); + } + }, [actionRootValue]); + + return ( + + + ({ overflow: 'hidden', gap: t.space.$1 })}> + ({ color: t.colors.$colorText })} + truncate + > + {email.emailAddress} + + {user?.primaryEmailAddressId === email.id && } + {email.verification.status !== 'verified' && ( + + )} + + setActionRootValue(null)} + /> + + + + + + + + + + + + + + + ); +}; + +const EmailMenu = ({ email, onClick }: { email: EmailAddressResource; onClick?: () => void }) => { const card = useCardState(); const { user } = useUser(); const { open } = useActionContext(); @@ -133,7 +171,10 @@ const EmailMenu = ({ email }: { email: EmailAddressResource }) => { { label: localizationKeys('userProfile.start.emailAddressesSection.destructiveAction'), isDestructive: true, - onClick: () => open('remove'), + onClick: () => { + open('remove'); + onClick?.(); + }, }, ] satisfies (PropsOfComponent['actions'][0] | null)[] ).filter(a => a !== null) as PropsOfComponent['actions']; diff --git a/packages/clerk-js/src/ui/elements/Action/ActionRoot.tsx b/packages/clerk-js/src/ui/elements/Action/ActionRoot.tsx index 1c83c70342e..85fe48c86ef 100644 --- a/packages/clerk-js/src/ui/elements/Action/ActionRoot.tsx +++ b/packages/clerk-js/src/ui/elements/Action/ActionRoot.tsx @@ -4,7 +4,11 @@ import { useCallback, useState } from 'react'; import { Animated } from '..'; -type ActionRootProps = PropsWithChildren<{ animate?: boolean }>; +type ActionRootProps = PropsWithChildren<{ + animate?: boolean; + value?: string | null; + onChange?: (value: string | null) => void; +}>; type ActionOpen = (value: string) => void; @@ -15,16 +19,29 @@ export const [ActionContext, useActionContext, _] = createContextAndHook<{ }>('ActionContext'); export const ActionRoot = (props: ActionRootProps) => { - const { animate = true, children } = props; - const [active, setActive] = useState(null); + const { animate = true, children, value: controlledValue, onChange } = props; + const [internalValue, setInternalValue] = useState(null); - const close = useCallback(() => { - setActive(null); - }, []); + const active = controlledValue !== undefined ? controlledValue : internalValue; - const open: ActionOpen = useCallback(value => { - setActive(value); - }, []); + const close = useCallback(() => { + if (onChange) { + onChange(null); + } else { + setInternalValue(null); + } + }, [onChange]); + + const open: ActionOpen = useCallback( + newValue => { + if (onChange) { + onChange(newValue); + } else { + setInternalValue(newValue); + } + }, + [onChange], + ); const body = {children}; From 86a4302ac3c4af15f54b7541fd499e32645e2ac4 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Mon, 27 Jan 2025 17:17:37 -0500 Subject: [PATCH 2/7] add changeset --- .changeset/clean-spoons-travel.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/clean-spoons-travel.md diff --git a/.changeset/clean-spoons-travel.md b/.changeset/clean-spoons-travel.md new file mode 100644 index 00000000000..bff209b78cb --- /dev/null +++ b/.changeset/clean-spoons-travel.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Ensure only one email action is open within `UserProfile` at a time. From 5f83863de52545b9bdd1511d62054f46fba8281a Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Mon, 27 Jan 2025 17:23:42 -0500 Subject: [PATCH 3/7] rename --- .../ui/components/UserProfile/EmailsSection.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/clerk-js/src/ui/components/UserProfile/EmailsSection.tsx b/packages/clerk-js/src/ui/components/UserProfile/EmailsSection.tsx index 327654aab41..a128da4a8f3 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/EmailsSection.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/EmailsSection.tsx @@ -119,7 +119,9 @@ const EmailRow = ({ setActionRootValue(null)} + resetActionRootValue={() => { + setActionRootValue(null); + }} /> @@ -138,7 +140,13 @@ const EmailRow = ({ ); }; -const EmailMenu = ({ email, onClick }: { email: EmailAddressResource; onClick?: () => void }) => { +const EmailMenu = ({ + email, + resetActionRootValue, +}: { + email: EmailAddressResource; + resetActionRootValue?: () => void; +}) => { const card = useCardState(); const { user } = useUser(); const { open } = useActionContext(); @@ -173,7 +181,7 @@ const EmailMenu = ({ email, onClick }: { email: EmailAddressResource; onClick?: isDestructive: true, onClick: () => { open('remove'); - onClick?.(); + resetActionRootValue?.(); }, }, ] satisfies (PropsOfComponent['actions'][0] | null)[] From 8c4019307ea3c334d00cb5f443271f783b07e1e9 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Mon, 27 Jan 2025 17:25:32 -0500 Subject: [PATCH 4/7] rename --- .../src/ui/components/UserProfile/EmailsSection.tsx | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/clerk-js/src/ui/components/UserProfile/EmailsSection.tsx b/packages/clerk-js/src/ui/components/UserProfile/EmailsSection.tsx index a128da4a8f3..3ced415c238 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/EmailsSection.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/EmailsSection.tsx @@ -119,7 +119,7 @@ const EmailRow = ({ { + removeOnClick={() => { setActionRootValue(null); }} /> @@ -140,13 +140,7 @@ const EmailRow = ({ ); }; -const EmailMenu = ({ - email, - resetActionRootValue, -}: { - email: EmailAddressResource; - resetActionRootValue?: () => void; -}) => { +const EmailMenu = ({ email, removeOnClick }: { email: EmailAddressResource; removeOnClick?: () => void }) => { const card = useCardState(); const { user } = useUser(); const { open } = useActionContext(); @@ -181,7 +175,7 @@ const EmailMenu = ({ isDestructive: true, onClick: () => { open('remove'); - resetActionRootValue?.(); + removeOnClick?.(); }, }, ] satisfies (PropsOfComponent['actions'][0] | null)[] From e33f57467d1caf41ecfcd3d206b15f1a4d5fbb5b Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Tue, 28 Jan 2025 13:17:09 -0500 Subject: [PATCH 5/7] add tests --- .../__tests__/EmailsSection.test.tsx | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/packages/clerk-js/src/ui/components/UserProfile/__tests__/EmailsSection.test.tsx b/packages/clerk-js/src/ui/components/UserProfile/__tests__/EmailsSection.test.tsx index acbd48ac551..7cbd7e19521 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/__tests__/EmailsSection.test.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/__tests__/EmailsSection.test.tsx @@ -187,4 +187,62 @@ describe('EmailSection', () => { }); }); }); + + describe('Handles opening/closing actions', () => { + it('closes add email form when remove an email address action is clicked', async () => { + const { wrapper, fixtures } = await createFixtures(withEmails); + const { getByText, userEvent, getByRole, queryByRole } = render( + + + , + { wrapper }, + ); + + fixtures.clerk.user?.emailAddresses[0].destroy.mockResolvedValue(); + + await userEvent.click(getByRole('button', { name: 'Add email address' })); + await waitFor(() => getByRole('heading', { name: /Add email address/i })); + + const item = getByText(emails[0]); + const menuButton = getMenuItemFromText(item); + await act(async () => { + await userEvent.click(menuButton!); + }); + + getByRole('menuitem', { name: /remove email/i }); + await userEvent.click(getByRole('menuitem', { name: /remove email/i })); + await waitFor(() => getByRole('heading', { name: /Remove email address/i })); + + await waitFor(() => expect(queryByRole('heading', { name: /Remove email address/i })).toBeInTheDocument()); + await waitFor(() => expect(queryByRole('heading', { name: /Add email address/i })).not.toBeInTheDocument()); + }); + + it('closes remove email address form when add email address action is clicked', async () => { + const { wrapper, fixtures } = await createFixtures(withEmails); + const { getByText, userEvent, getByRole, queryByRole } = render( + + + , + { wrapper }, + ); + + fixtures.clerk.user?.emailAddresses[0].destroy.mockResolvedValue(); + + const item = getByText(emails[0]); + const menuButton = getMenuItemFromText(item); + await act(async () => { + await userEvent.click(menuButton!); + }); + + getByRole('menuitem', { name: /remove email/i }); + await userEvent.click(getByRole('menuitem', { name: /remove email/i })); + await waitFor(() => getByRole('heading', { name: /Remove email address/i })); + + await userEvent.click(getByRole('button', { name: 'Add email address' })); + await waitFor(() => getByRole('heading', { name: /Add email address/i })); + + await waitFor(() => expect(queryByRole('heading', { name: /Remove email address/i })).not.toBeInTheDocument()); + await waitFor(() => expect(queryByRole('heading', { name: /Add email address/i })).toBeInTheDocument()); + }); + }); }); From f46e918fdec17c8b3ae11a53071da3e939c1719c Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Tue, 28 Jan 2025 14:14:51 -0500 Subject: [PATCH 6/7] Update packages/clerk-js/src/ui/components/UserProfile/EmailsSection.tsx Co-authored-by: Jacek Radko --- .../clerk-js/src/ui/components/UserProfile/EmailsSection.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/src/ui/components/UserProfile/EmailsSection.tsx b/packages/clerk-js/src/ui/components/UserProfile/EmailsSection.tsx index 3ced415c238..5df1a789878 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/EmailsSection.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/EmailsSection.tsx @@ -86,7 +86,7 @@ const EmailRow = ({ actionRootValue, setActionRootValue, }: { - user: UserResource | null | undefined; + user?: UserResource | undefined; email: EmailAddressResource; actionRootValue?: string | null; setActionRootValue: (value: string | null) => void; From 53b4de4bb37d5da0d823de90b35b538e3c54d80d Mon Sep 17 00:00:00 2001 From: Jacek Date: Tue, 28 Jan 2025 13:38:49 -0600 Subject: [PATCH 7/7] fix typing --- .../src/ui/components/UserProfile/EmailsSection.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/clerk-js/src/ui/components/UserProfile/EmailsSection.tsx b/packages/clerk-js/src/ui/components/UserProfile/EmailsSection.tsx index 5df1a789878..013b77dc331 100644 --- a/packages/clerk-js/src/ui/components/UserProfile/EmailsSection.tsx +++ b/packages/clerk-js/src/ui/components/UserProfile/EmailsSection.tsx @@ -39,6 +39,8 @@ const EmailScreen = (props: EmailScreenProps) => { export const EmailsSection = ({ shouldAllowCreation = true }) => { const { user } = useUser(); const [actionRootValue, setActionRootValue] = useState(null); + + if (!user) return null; return ( { onChange={setActionRootValue} > - {sortIdentificationBasedOnVerification(user?.emailAddresses, user?.primaryEmailAddressId).map(email => ( + {sortIdentificationBasedOnVerification(user.emailAddresses, user.primaryEmailAddressId).map(email => ( void;