diff --git a/.changeset/mosaic-profile-providers.md b/.changeset/mosaic-profile-providers.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/mosaic-profile-providers.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/mosaic/src/components/panel/index.ts b/packages/mosaic/src/components/panel/index.ts index 015b71bc43c..5ffa79d605f 100644 --- a/packages/mosaic/src/components/panel/index.ts +++ b/packages/mosaic/src/components/panel/index.ts @@ -1,2 +1,2 @@ -export { Panel } from './panel'; +export { Panel, usePanelTitle } from './panel'; export type { PanelRootProps, PanelSectionsProps, PanelTitleProps } from './panel'; diff --git a/packages/mosaic/src/components/panel/panel.tsx b/packages/mosaic/src/components/panel/panel.tsx index 71de23b56e2..126b2f2b3ea 100644 --- a/packages/mosaic/src/components/panel/panel.tsx +++ b/packages/mosaic/src/components/panel/panel.tsx @@ -1,3 +1,4 @@ +import { useMergeRefs } from '@floating-ui/react'; import * as stylex from '@stylexjs/stylex'; import React from 'react'; @@ -13,13 +14,22 @@ export type PanelRootProps = MosaicComponentProps<'div'>; export type PanelTitleProps = MosaicComponentProps<'div'>; export type PanelSectionsProps = MosaicComponentProps<'div'>; +const PanelTitleContext = React.createContext | null>(null); + +export function usePanelTitle(): () => HTMLDivElement | null { + const titleRef = React.useContext(PanelTitleContext); + return () => titleRef?.current ?? null; +} + const Root = React.forwardRef(function PanelRoot({ render, xstyle, ...rest }, ref) { - return useRender({ + const titleRef = React.useRef(null); + const element = useRender({ defaultTagName: 'div', render, ref, props: mergeStyleProps(themeProps('panel'), stylex.props(reset.base, styles.root, xstyle), rest), }); + return {element}; }); // Inside a profile page, the profile renders the page title, and the ref reaches that instead. @@ -29,16 +39,21 @@ const Title = React.forwardRef(function PanelTi ) { const inProfilePage = React.useContext(ContentPanelContext); const profile = React.useContext(ProfileContext); + const panelTitleRef = React.useContext(PanelTitleContext); + const titleRef = useMergeRefs([ref, panelTitleRef]); const level = useHeadingLevel(); const pageTitleRef = inProfilePage ? profile?.pageTitleRef : undefined; - React.useImperativeHandle(pageTitleRef ? ref : null, () => pageTitleRef?.current as HTMLDivElement); + React.useImperativeHandle(pageTitleRef ? titleRef : null, () => pageTitleRef?.current as HTMLDivElement); return useRender({ defaultTagName: 'div', render, - ref: pageTitleRef ? null : ref, + ref: pageTitleRef ? null : titleRef, enabled: !inProfilePage, props: { - ...mergeStyleProps(themeProps('panel-title'), stylex.props(reset.base, styles.title, xstyle), rest), + ...mergeStyleProps(themeProps('panel-title'), stylex.props(reset.base, styles.title, xstyle), { + tabIndex: -1, + ...rest, + }), children: ( { expect(titleRef.current).toBe(title()); }); + it('makes the page title the panel title inside a profile page', () => { + let panelTitle: () => HTMLDivElement | null = () => null; + function ReadPanelTitle() { + panelTitle = usePanelTitle(); + return null; + } + render( + + + User profile + + Account + + + + + Account + + + + + + + + , + ); + + expect(panelTitle()).toBe(screen.getByRole('heading', { level: 3, name: 'Account' })); + }); + it('returns the tablist to the column when the width comes back', () => { renderSurface(); act(() => observe?.(3)); diff --git a/packages/mosaic/src/features/organization-profile/organization-profile-danger-section/__tests__/organization-profile-danger-section.feature.test.tsx b/packages/mosaic/src/features/organization-profile/organization-profile-danger-section/__tests__/organization-profile-danger-section.feature.test.tsx index 2a99e24ff1c..1e36cd6b3f4 100644 --- a/packages/mosaic/src/features/organization-profile/organization-profile-danger-section/__tests__/organization-profile-danger-section.feature.test.tsx +++ b/packages/mosaic/src/features/organization-profile/organization-profile-danger-section/__tests__/organization-profile-danger-section.feature.test.tsx @@ -14,10 +14,8 @@ import { fapiUser, } from '../../../../__tests__/feature/fapi'; import { renderWithClerk } from '../../../../__tests__/feature/render'; -import { - OrganizationProfileDangerSection, - type OrganizationProfileDangerSectionProps, -} from '../organization-profile-danger-section'; +import { OrganizationProfileProvider } from '../../organization-profile.provider'; +import { OrganizationProfileDangerSection } from '../organization-profile-danger-section'; const acme = fapiOrganization({ id: 'org_1', name: 'Acme', members_count: 20 }); const deletePermission = ['org:sys_profile:delete']; @@ -38,9 +36,9 @@ function signedIn({ } satisfies FakeFapiSeed; } -async function renderSection(seed: FakeFapiSeed = signedIn(), props: OrganizationProfileDangerSectionProps = {}) { +async function renderSection(seed: FakeFapiSeed = signedIn()) { const fapi = serveFapi(seed); - const view = await renderWithClerk(); + const view = await renderWithClerk(); return { ...view, fapi }; } @@ -144,8 +142,13 @@ describe('OrganizationProfileDangerSection', () => { expect(dialog).toHaveTextContent('This removes 1 member and permanently deletes all organization data.'); }); - it('deletes the organization and sends the user to the url the host passed', async () => { - const { fapi, navigate } = await renderSection(signedIn(), { afterLeaveOrganizationUrl: '/organizations' }); + it('deletes the organization and sends the user to the url from the provider', async () => { + const fapi = serveFapi(signedIn()); + const { navigate } = await renderWithClerk( + + + , + ); const user = userEvent.setup(); const dialog = await openDialog(user, await screen.findByRole('button', { name: 'Delete organization' })); diff --git a/packages/mosaic/src/features/organization-profile/organization-profile-danger-section/organization-profile-danger-section.model.ts b/packages/mosaic/src/features/organization-profile/organization-profile-danger-section/organization-profile-danger-section.model.ts index c9782b20e72..f9840626c5d 100644 --- a/packages/mosaic/src/features/organization-profile/organization-profile-danger-section/organization-profile-danger-section.model.ts +++ b/packages/mosaic/src/features/organization-profile/organization-profile-danger-section/organization-profile-danger-section.model.ts @@ -2,6 +2,7 @@ import { useOrganization, useOrganizationList, useSession, useUser } from '@cler import { useMosaicEnvironment } from '../../../hooks/use-mosaic-environment'; import { useMosaicRouter } from '../../../hooks/use-mosaic-router'; +import { useOrganizationProfileOptions } from '../organization-profile.provider'; const organizationListParams = { userMemberships: { infinite: true }, @@ -20,11 +21,8 @@ export type OrganizationProfileDangerSectionModel = deleteOrganization?: () => Promise; }; -export function useOrganizationProfileDangerSectionModel({ - afterLeaveOrganizationUrl, -}: { - afterLeaveOrganizationUrl?: string; -}): OrganizationProfileDangerSectionModel { +export function useOrganizationProfileDangerSectionModel(): OrganizationProfileDangerSectionModel { + const { afterLeaveOrganizationUrl } = useOrganizationProfileOptions(); const { isLoaded, organization, membership } = useOrganization(); const { user } = useUser(); const { session } = useSession(); diff --git a/packages/mosaic/src/features/organization-profile/organization-profile-danger-section/organization-profile-danger-section.tsx b/packages/mosaic/src/features/organization-profile/organization-profile-danger-section/organization-profile-danger-section.tsx index ed15ce8388e..511edd0315c 100644 --- a/packages/mosaic/src/features/organization-profile/organization-profile-danger-section/organization-profile-danger-section.tsx +++ b/packages/mosaic/src/features/organization-profile/organization-profile-danger-section/organization-profile-danger-section.tsx @@ -4,12 +4,11 @@ import { useOrganizationProfileDangerSectionModel } from './organization-profile import { OrganizationProfileDangerSectionView } from './organization-profile-danger-section.view'; export type OrganizationProfileDangerSectionProps = { - afterLeaveOrganizationUrl?: string; fallback?: React.ReactNode; }; export function OrganizationProfileDangerSection(props: OrganizationProfileDangerSectionProps) { - const model = useOrganizationProfileDangerSectionModel(props); + const model = useOrganizationProfileDangerSectionModel(); if (model.status === 'loading') { return props.fallback ?? null; diff --git a/packages/mosaic/src/features/organization-profile/organization-profile-general-panel.feature.test.tsx b/packages/mosaic/src/features/organization-profile/organization-profile-general-panel.feature.test.tsx index db60148b232..7b649c2ab92 100644 --- a/packages/mosaic/src/features/organization-profile/organization-profile-general-panel.feature.test.tsx +++ b/packages/mosaic/src/features/organization-profile/organization-profile-general-panel.feature.test.tsx @@ -15,6 +15,7 @@ import { } from '../../__tests__/feature/fapi'; import { renderWithClerk } from '../../__tests__/feature/render'; import { MosaicProvider } from '../../mosaic-provider'; +import { OrganizationProfileProvider } from './organization-profile.provider'; import { OrganizationProfileDangerSection } from './organization-profile-danger-section/organization-profile-danger-section'; import { OrganizationProfileGeneralPanel } from './organization-profile-general-panel'; import { OrganizationProfileProfileSection } from './organization-profile-profile-section/organization-profile-profile-section'; @@ -112,6 +113,25 @@ describe('OrganizationProfileGeneralPanel', () => { it.todo('shows verified domains when enabled and the member can read them'); }); + describe('leave', () => { + it('sends the user to the provider url after leaving from the default sections', async () => { + const fapi = serveFapi(signedIn()); + const { navigate } = await renderWithClerk( + + + , + ); + const user = userEvent.setup(); + const dialog = await openDialog(user, await screen.findByRole('button', { name: 'Leave organization' })); + + await user.type(within(dialog).getByRole('textbox'), 'Acme'); + await user.click(within(dialog).getByRole('button', { name: 'Leave organization' })); + + await waitFor(() => expect(fapi.memberships).toHaveLength(0)); + await waitFor(() => expect(navigate).toHaveBeenCalledWith('/organizations')); + }); + }); + describe('edit name', () => { it('updates only the changed name and shows the refreshed value', async () => { const { fapi } = await renderPanel(); diff --git a/packages/mosaic/src/features/organization-profile/organization-profile.provider.tsx b/packages/mosaic/src/features/organization-profile/organization-profile.provider.tsx new file mode 100644 index 00000000000..41517a54d49 --- /dev/null +++ b/packages/mosaic/src/features/organization-profile/organization-profile.provider.tsx @@ -0,0 +1,23 @@ +import { createContext, type ReactNode, useContext } from 'react'; + +export interface OrganizationProfileOptions { + afterLeaveOrganizationUrl?: string; +} + +export interface OrganizationProfileProviderProps extends OrganizationProfileOptions { + children: ReactNode; +} + +const OrganizationProfileContext = createContext({}); + +export function OrganizationProfileProvider({ afterLeaveOrganizationUrl, children }: OrganizationProfileProviderProps) { + return ( + + {children} + + ); +} + +export function useOrganizationProfileOptions(): OrganizationProfileOptions { + return useContext(OrganizationProfileContext); +} diff --git a/packages/mosaic/src/features/user-profile/__tests__/user-profile-profile-panel.view.test.tsx b/packages/mosaic/src/features/user-profile/__tests__/user-profile-profile-panel.view.test.tsx index bad8c2a18b7..130e891971b 100644 --- a/packages/mosaic/src/features/user-profile/__tests__/user-profile-profile-panel.view.test.tsx +++ b/packages/mosaic/src/features/user-profile/__tests__/user-profile-profile-panel.view.test.tsx @@ -1,7 +1,6 @@ import { createDeferredPromise } from '@clerk/shared/utils'; import { act, render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { createRef } from 'react'; import { describe, expect, it, vi } from 'vitest'; import { useDestructiveController } from '../../../blocks/destructive/destructive.controller'; @@ -32,19 +31,16 @@ describe('UserProfileProfilePanelView', () => { expect(screen.queryByRole('tab')).toBeNull(); }); - it('keeps the final wallet confirmation mounted until removal settles', async () => { + it('keeps the final wallet confirmation mounted until removal settles, then focuses the title', async () => { const user = userEvent.setup(); - const titleRef = createRef(); const removal = createDeferredPromise(); const onRemoveWeb3Wallet = vi.fn(async () => { await removal.promise; }); const { rerender } = renderView({ - titleRef, children: ( titleRef.current} onRemove={onRemoveWeb3Wallet} /> ), @@ -54,10 +50,9 @@ describe('UserProfileProfilePanelView', () => { await user.click(within(screen.getByRole('alertdialog')).getByRole('button', { name: 'Remove' })); rerender( - + titleRef.current} onRemove={onRemoveWeb3Wallet} /> @@ -70,6 +65,9 @@ describe('UserProfileProfilePanelView', () => { await removal.promise; }); await waitFor(() => expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()); + await waitFor(() => + expect(screen.getByRole('heading', { name: 'Account', level: 2 }).closest('.cl-panel-title')).toHaveFocus(), + ); }); it('renders the danger zone when provided', () => { diff --git a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/__tests__/user-profile-connected-accounts.connect.feature.test.tsx b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/__tests__/user-profile-connected-accounts.connect.feature.test.tsx index 1251159e030..bdaf35be9bc 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/__tests__/user-profile-connected-accounts.connect.feature.test.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/__tests__/user-profile-connected-accounts.connect.feature.test.tsx @@ -15,6 +15,7 @@ import { } from '../../../../__tests__/feature/fapi'; import { renderWithClerk } from '../../../../__tests__/feature/render'; import { MosaicProvider } from '../../../../mosaic-provider'; +import { UserProfileProvider } from '../../user-profile.provider'; import { UserProfileConnectedAccountsSection } from '../user-profile-connected-accounts-section'; import { deferred, @@ -100,7 +101,9 @@ describe('connected accounts', () => { it('sends requested scopes and the current URL to connect', async () => { serveFapi(signedIn([])); const { clerk } = await renderWithClerk( - , + + + , ); const navigate = vi.spyOn(clerk, 'navigate').mockImplementation(() => Promise.resolve()); const request = holdRequests('post', '/v1/me/external_accounts'); @@ -183,7 +186,11 @@ describe('connected accounts', () => { it('preserves modal state when connecting', async () => { serveFapi(signedIn([])); - await renderWithClerk(); + await renderWithClerk( + + + , + ); const request = holdRequests('post', '/v1/me/external_accounts'); await userEvent.setup().click(screen.getByRole('button', { name: 'Connect GitHub' })); @@ -374,7 +381,9 @@ describe('connected accounts', () => { const callback = deferred<{ callbackUrl: string }>(); const open = vi.fn(() => callback.promise); const { clerk } = await renderWithClerk( - , + + + , { __internal_oauthTransport: { getRedirectUrl: () => 'https://app.example/callback', open }, }, diff --git a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/__tests__/user-profile-connected-accounts.fixtures.tsx b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/__tests__/user-profile-connected-accounts.fixtures.tsx index 0e8d0096d89..49ccd2ee2b4 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/__tests__/user-profile-connected-accounts.fixtures.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/__tests__/user-profile-connected-accounts.fixtures.tsx @@ -1,6 +1,5 @@ import { screen } from '@testing-library/react'; import type userEvent from '@testing-library/user-event'; -import { createRef } from 'react'; import { type FakeFapiSeed, serveFapi } from '../../../../__tests__/feature/fake-fapi'; import { @@ -12,7 +11,7 @@ import { fapiVerification, } from '../../../../__tests__/feature/fapi'; import { renderWithClerk } from '../../../../__tests__/feature/render'; -import { UserProfileProfilePanelView } from '../../user-profile-profile-panel.view'; +import { UserProfileProfilePanel } from '../../user-profile-profile-panel'; import { UserProfileConnectedAccountsSection } from '../user-profile-connected-accounts-section'; export const google = fapiExternalAccount({ id: 'idn_google', provider: 'google', username: 'jdoe' }); @@ -65,11 +64,10 @@ export function deferred() { export async function renderSection(accounts = [google], overrides: FakeFapiSeed = {}) { const fapi = serveFapi(signedIn(accounts, overrides)); - const titleRef = createRef(); const view = await renderWithClerk( - - titleRef.current} /> - , + + + , ); return { ...view, fapi }; } diff --git a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/__tests__/user-profile-connected-accounts.reconnect.feature.test.tsx b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/__tests__/user-profile-connected-accounts.reconnect.feature.test.tsx index 1265415da1e..84adc5fdf81 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/__tests__/user-profile-connected-accounts.reconnect.feature.test.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/__tests__/user-profile-connected-accounts.reconnect.feature.test.tsx @@ -11,6 +11,7 @@ import { fapiVerification, } from '../../../../__tests__/feature/fapi'; import { renderWithClerk } from '../../../../__tests__/feature/render'; +import { UserProfileProvider } from '../../user-profile.provider'; import { UserProfileConnectedAccountsSection } from '../user-profile-connected-accounts-section'; import { deferred, @@ -112,7 +113,11 @@ describe('connected accounts', () => { it('preserves modal state when reconnecting', async () => { serveFapi(signedIn([disconnectedGoogle])); - await renderWithClerk(); + await renderWithClerk( + + + , + ); const request = holdRequests('post', '/v1/me/external_accounts'); const user = userEvent.setup(); @@ -128,7 +133,9 @@ describe('connected accounts', () => { it('reauthorizes a connected account when it lacks requested scopes', async () => { serveFapi(signedIn([google])); const { clerk } = await renderWithClerk( - , + + + , ); const navigate = vi.spyOn(clerk, 'navigate').mockImplementation(() => Promise.resolve()); const request = holdRequests('post', '/v1/me/external_accounts/idn_google/reauthorize'); @@ -146,7 +153,9 @@ describe('connected accounts', () => { it('shows a failed reauthorization and allows another attempt', async () => { serveFapi(signedIn([google])); const { clerk } = await renderWithClerk( - , + + + , ); const navigate = vi.spyOn(clerk, 'navigate').mockImplementation(() => Promise.resolve()); const request = holdRequests('post', '/v1/me/external_accounts/idn_google/reauthorize'); @@ -167,7 +176,9 @@ describe('connected accounts', () => { const callback = deferred<{ callbackUrl: string }>(); const open = vi.fn(() => callback.promise); const { clerk } = await renderWithClerk( - , + + + , { __internal_oauthTransport: { getRedirectUrl: () => 'https://app.example/callback', open }, }, @@ -218,7 +229,9 @@ describe('connected accounts', () => { const getRedirectUrl = vi.fn(() => redirect.promise); const open = vi.fn(() => Promise.resolve({ callbackUrl: 'https://app.example/callback' })); const { clerk } = await renderWithClerk( - , + + + , { __internal_oauthTransport: { getRedirectUrl, open }, }, @@ -247,7 +260,9 @@ describe('connected accounts', () => { it('allows another connection when reauthorizing an account fails to navigate', async () => { serveFapi(signedIn([google])); const { clerk } = await renderWithClerk( - , + + + , ); const navigate = vi.spyOn(clerk, 'navigate').mockRejectedValueOnce(new Error('Navigation failed')); const user = userEvent.setup(); diff --git a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/__tests__/user-profile-connected-accounts.removal.feature.test.tsx b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/__tests__/user-profile-connected-accounts.removal.feature.test.tsx index d9c5296a924..e2d1b98c3f2 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/__tests__/user-profile-connected-accounts.removal.feature.test.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/__tests__/user-profile-connected-accounts.removal.feature.test.tsx @@ -5,6 +5,7 @@ import { describe, expect, it, vi } from 'vitest'; import { holdRequests, serveFapi } from '../../../../__tests__/feature/fake-fapi'; import { fapiClient, fapiEnterpriseAccount, fapiSession, fapiUser } from '../../../../__tests__/feature/fapi'; import { renderWithClerk } from '../../../../__tests__/feature/render'; +import { UserProfileProfilePanel } from '../../user-profile-profile-panel'; import { UserProfileConnectedAccountsSection } from '../user-profile-connected-accounts-section'; import { github, google, renderSection, signedIn } from './user-profile-connected-accounts.fixtures'; @@ -101,32 +102,46 @@ describe('connected accounts', () => { expect(fapi.client.sessions[0]?.user.external_accounts.map(account => account.id)).toEqual(['idn_google']); }); - it('hides the section after removing the final enterprise-restricted account', async () => { + it.each([ + ['default sections', ], + [ + 'custom sections', + + + , + ], + ])('hides the section and focuses the panel title after removing the final account with %s', async (_, panel) => { const environment = signedIn().environment; - await renderSection([google], { - client: fapiClient([ - fapiSession({ - id: 'sess_1', - user: fapiUser({ - id: 'user_1', - external_accounts: [google], - enterprise_accounts: [fapiEnterpriseAccount({ id: 'sso_1' })], + serveFapi( + signedIn([google], { + client: fapiClient([ + fapiSession({ + id: 'sess_1', + user: fapiUser({ + id: 'user_1', + external_accounts: [google], + enterprise_accounts: [fapiEnterpriseAccount({ id: 'sso_1' })], + }), }), - }), - ]), - environment: { - ...environment, - user_settings: { - ...environment.user_settings, - enterprise_sso: { enabled: true, self_serve_sso: false, self_serve_directory_sync: false }, + ]), + environment: { + ...environment, + user_settings: { + ...environment.user_settings, + enterprise_sso: { enabled: true, self_serve_sso: false, self_serve_directory_sync: false }, + }, }, - }, - }); + }), + ); + await renderWithClerk(panel); const user = userEvent.setup(); const dialog = await openRemoval(user, 'Google'); await user.click(within(dialog).getByRole('button', { name: 'Remove' })); await waitFor(() => expect(screen.queryByRole('alertdialog')).toBeNull()); expect(screen.queryByRole('group', { name: 'Connected accounts' })).toBeNull(); + await waitFor(() => + expect(screen.getByRole('heading', { name: 'Account', level: 2 }).closest('.cl-panel-title')).toHaveFocus(), + ); }); it.each(['switch', 'sign out'] as const)('closes a stale removal confirmation after %s', async change => { diff --git a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.model.ts b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.model.ts index 2a2af35966e..fd0f508bfbc 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.model.ts +++ b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.model.ts @@ -17,6 +17,7 @@ import type { import { useMosaicEnvironment } from '../../../hooks/use-mosaic-environment'; import { useMosaicRouter } from '../../../hooks/use-mosaic-router'; import { allowsIdentificationCreation } from '../../../utils/allows-identification-creation'; +import { useUserProfileOptions } from '../user-profile.provider'; import type { ConnectedAccountActionResult, ConnectedAccountProviderDisplay, @@ -213,13 +214,8 @@ export type UserProfileConnectedAccountsModel = remove: (accountId: string) => Promise; }; -export function useUserProfileConnectedAccountsModel({ - additionalOAuthScopes, - mode, -}: { - additionalOAuthScopes?: AdditionalOAuthScopes; - mode?: 'modal' | 'mounted'; -}): UserProfileConnectedAccountsModel { +export function useUserProfileConnectedAccountsModel(): UserProfileConnectedAccountsModel { + const { additionalOAuthScopes, mode } = useUserProfileOptions(); const clerk = useClerk(); const { isLoaded, user } = useUser(); const environment = useMosaicEnvironment(); diff --git a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.tsx b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.tsx index d601119b69c..b70c6c0b05f 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.tsx @@ -1,27 +1,16 @@ import type { ReactNode } from 'react'; import { useUserProfileConnectedAccountsController } from './user-profile-connected-accounts-section.controller'; -import type { - AdditionalOAuthScopes, - UserProfileConnectedAccountsModel, -} from './user-profile-connected-accounts-section.model'; +import type { UserProfileConnectedAccountsModel } from './user-profile-connected-accounts-section.model'; import { useUserProfileConnectedAccountsModel } from './user-profile-connected-accounts-section.model'; import { UserProfileConnectedAccountsSectionView } from './user-profile-connected-accounts-section.view'; export type UserProfileConnectedAccountsSectionProps = { - additionalOAuthScopes?: AdditionalOAuthScopes; fallback?: ReactNode; - fallbackFocus?: () => HTMLElement | null; - mode?: 'modal' | 'mounted'; }; -export function UserProfileConnectedAccountsSection({ - additionalOAuthScopes, - fallback, - fallbackFocus, - mode, -}: UserProfileConnectedAccountsSectionProps) { - const model = useUserProfileConnectedAccountsModel({ additionalOAuthScopes, mode }); +export function UserProfileConnectedAccountsSection({ fallback }: UserProfileConnectedAccountsSectionProps) { + const model = useUserProfileConnectedAccountsModel(); if (model.status === 'loading') { return fallback ?? null; } @@ -32,18 +21,11 @@ export function UserProfileConnectedAccountsSection({ ); } -function ConnectedAccounts({ - model, - fallbackFocus, -}: { - model: Extract; - fallbackFocus?: () => HTMLElement | null; -}) { +function ConnectedAccounts({ model }: { model: Extract }) { const controller = useUserProfileConnectedAccountsController({ accounts: model.accounts, availableProviders: model.availableProviders, @@ -54,7 +36,6 @@ function ConnectedAccounts({ return ( ); diff --git a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.types.ts b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.types.ts index adcd5ffc3d5..372ab5c5801 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.types.ts +++ b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.types.ts @@ -20,7 +20,6 @@ export interface UserProfileConnectedAccount extends ConnectedAccountProviderDis } export interface UserProfileConnectedAccountsSectionViewProps { - fallbackFocus?: () => HTMLElement | null; accounts: UserProfileConnectedAccount[]; availableProviders?: UserProfileConnectionProvider[]; pendingId?: string; diff --git a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.view.tsx b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.view.tsx index 7a806d89c06..1c77eed3c8c 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.view.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.view.tsx @@ -1,6 +1,7 @@ import { useMemo, useRef } from 'react'; import { Confirmation } from '../../../blocks/confirmation'; +import { usePanelTitle } from '../../../components/panel'; import { Section } from '../../../components/section'; import { useListRemovalFocus } from '../../../hooks/use-list-removal-focus'; import { fill, useMessages } from '../../../localization'; @@ -21,7 +22,6 @@ export type { export function UserProfileConnectedAccountsSectionView({ accounts, - fallbackFocus, availableProviders = [], pendingId, onConnect, @@ -30,14 +30,12 @@ export function UserProfileConnectedAccountsSectionView({ }: UserProfileConnectedAccountsSectionViewProps) { const m = useMessages('userProfileConnectedAccounts'); const section = useRef(null); + const panelTitle = usePanelTitle(); const removalFocus = useListRemovalFocus({ ids: accounts.map(account => account.id), onRemove, fallback: () => - section.current?.querySelector('button:not([disabled])') ?? - section.current ?? - fallbackFocus?.() ?? - null, + section.current?.querySelector('button:not([disabled])') ?? section.current ?? panelTitle(), }); const removeAccount = useMemo(() => Confirmation.createHandle(), []); const hasRows = accounts.length > 0 || (availableProviders.length > 0 && Boolean(onConnect)); diff --git a/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/__tests__/user-profile-enterprise-accounts.feature.test.tsx b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/__tests__/user-profile-enterprise-accounts.feature.test.tsx index a32b5c4bb94..cbf2647af83 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/__tests__/user-profile-enterprise-accounts.feature.test.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/__tests__/user-profile-enterprise-accounts.feature.test.tsx @@ -15,6 +15,7 @@ import { } from '../../../../__tests__/feature/fapi'; import { renderWithClerk } from '../../../../__tests__/feature/render'; import { MosaicProvider } from '../../../../mosaic-provider'; +import { UserProfileProvider } from '../../user-profile.provider'; import { UserProfileEnterpriseAccountsSection } from '../user-profile-enterprise-accounts-section'; import { useUserProfileEnterpriseAccountsModel } from '../user-profile-enterprise-accounts-section.model'; import { custom, enterpriseAccountSeed as signedIn, enterpriseMember, okta } from './enterprise-accounts.fixtures'; @@ -179,7 +180,11 @@ describe('enterprise accounts', () => { it('preserves modal return state in the redirect URL', async () => { serveFapi(signedIn()); - await renderWithClerk(); + await renderWithClerk( + + + , + ); const request = holdRequests('post', '/v1/me/external_accounts'); await userEvent.setup().click(await screen.findByRole('button', { name: 'Connect Acme Okta' })); diff --git a/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.model.ts b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.model.ts index 00dd76a2054..bcb6055617e 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.model.ts +++ b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.model.ts @@ -5,6 +5,7 @@ import type { EnterpriseAccountResource, EnterpriseConnectionResource } from '@c import { useMosaicEnvironment } from '../../../hooks/use-mosaic-environment'; import { useMosaicRouter } from '../../../hooks/use-mosaic-router'; +import { useUserProfileOptions } from '../user-profile.provider'; import type { EnterpriseAccountActionResult, UserProfileEnterpriseAccount, @@ -71,9 +72,8 @@ export type UserProfileEnterpriseAccountsModel = connect: (connectionId: string) => Promise; }; -export function useUserProfileEnterpriseAccountsModel({ - mode, -}: { mode?: 'modal' | 'mounted' } = {}): UserProfileEnterpriseAccountsModel { +export function useUserProfileEnterpriseAccountsModel(): UserProfileEnterpriseAccountsModel { + const { mode } = useUserProfileOptions(); const clerk = useClerk(); const router = useMosaicRouter(); const { isLoaded, user } = useUser(); diff --git a/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.tsx b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.tsx index 153c981c49f..b1326e319e7 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-enterprise-accounts-section/user-profile-enterprise-accounts-section.tsx @@ -8,11 +8,10 @@ import { UserProfileEnterpriseAccountsSectionView } from './user-profile-enterpr export type UserProfileEnterpriseAccountsSectionProps = { fallback?: ReactNode; - mode?: 'modal' | 'mounted'; }; -export function UserProfileEnterpriseAccountsSection({ fallback, mode }: UserProfileEnterpriseAccountsSectionProps) { - const model = useUserProfileEnterpriseAccountsModel({ mode }); +export function UserProfileEnterpriseAccountsSection({ fallback }: UserProfileEnterpriseAccountsSectionProps) { + const model = useUserProfileEnterpriseAccountsModel(); if (model.status === 'loading') { return fallback ?? null; } diff --git a/packages/mosaic/src/features/user-profile/user-profile-profile-panel.tsx b/packages/mosaic/src/features/user-profile/user-profile-profile-panel.tsx index 9bf7400227d..f2fc7e6d4da 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-profile-panel.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-profile-panel.tsx @@ -1,4 +1,4 @@ -import { type ReactNode, useRef } from 'react'; +import type { ReactNode } from 'react'; import { UserProfileConnectedAccountsSection } from './user-profile-connected-accounts-section/user-profile-connected-accounts-section'; import { UserProfileDangerSection } from './user-profile-danger-section/user-profile-danger-section'; @@ -14,19 +14,16 @@ export interface UserProfileProfilePanelProps { } export function UserProfileProfilePanel({ children }: UserProfileProfilePanelProps) { - const titleRef = useRef(null); - const focusTitle = () => titleRef.current; - return ( - + {children ?? ( <> - + - + )} diff --git a/packages/mosaic/src/features/user-profile/user-profile-profile-panel.view.tsx b/packages/mosaic/src/features/user-profile/user-profile-profile-panel.view.tsx index 021de69f020..5d826497360 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-profile-panel.view.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-profile-panel.view.tsx @@ -1,4 +1,4 @@ -import type { ReactElement, ReactNode, Ref } from 'react'; +import type { ReactElement, ReactNode } from 'react'; import { Panel } from '../../components/panel'; import { themeProps } from '../../props'; @@ -11,19 +11,13 @@ export type { UserProfileNameAttribute } from './user-profile-profile-section/us export type { UserProfileEditNameValue } from './user-profile-profile-section/user-profile-edit-name.dialog'; export interface UserProfileProfilePanelViewProps { - titleRef?: Ref; children?: ReactNode; } -export function UserProfileProfilePanelView({ titleRef, children }: UserProfileProfilePanelViewProps): ReactElement { +export function UserProfileProfilePanelView({ children }: UserProfileProfilePanelViewProps): ReactElement { return ( }> - - Account - + Account {children} ); diff --git a/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallets-section.tsx b/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallets-section.tsx index 14caefc0b56..c7a24967624 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallets-section.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallets-section.tsx @@ -8,10 +8,9 @@ import { UserProfileWeb3WalletsSectionView } from './user-profile-web3-wallets-s export interface UserProfileWeb3WalletsSectionProps { fallback?: ReactNode; - fallbackFocus?: () => HTMLElement | null; } -export function UserProfileWeb3WalletsSection({ fallback, fallbackFocus }: UserProfileWeb3WalletsSectionProps) { +export function UserProfileWeb3WalletsSection({ fallback }: UserProfileWeb3WalletsSectionProps) { const model = useUserProfileWeb3WalletsModel(); if (model.status === 'loading') { return fallback ?? null; @@ -23,23 +22,15 @@ export function UserProfileWeb3WalletsSection({ fallback, fallbackFocus }: UserP ); } -function Web3Wallets({ - model, - fallbackFocus, -}: { - model: ReadyWeb3WalletsModel; - fallbackFocus?: () => HTMLElement | null; -}) { +function Web3Wallets({ model }: { model: ReadyWeb3WalletsModel }) { const controller = useUserProfileWeb3WalletsController(model); return ( <> HTMLElement | null; wallets: UserProfileWeb3Wallet[]; availableProviders?: UserProfileWeb3Provider[]; pendingId?: string; diff --git a/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallets-section.view.tsx b/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallets-section.view.tsx index 644961421bd..6b3d0bc3c7c 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallets-section.view.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallets-section.view.tsx @@ -1,6 +1,7 @@ import { useMemo, useRef } from 'react'; import { Confirmation } from '../../../blocks/confirmation'; +import { usePanelTitle } from '../../../components/panel'; import { Section } from '../../../components/section'; import { useListRemovalFocus } from '../../../hooks/use-list-removal-focus'; import { fill, useMessages } from '../../../localization'; @@ -13,7 +14,6 @@ import type { export function UserProfileWeb3WalletsSectionView({ wallets, - fallbackFocus, availableProviders = [], pendingId, onConnect, @@ -22,14 +22,12 @@ export function UserProfileWeb3WalletsSectionView({ }: UserProfileWeb3WalletsSectionViewProps) { const m = useMessages('userProfileWeb3Wallets'); const section = useRef(null); + const panelTitle = usePanelTitle(); const removalFocus = useListRemovalFocus({ ids: wallets.map(wallet => wallet.id), onRemove, fallback: () => - section.current?.querySelector('button:not([disabled])') ?? - section.current ?? - fallbackFocus?.() ?? - null, + section.current?.querySelector('button:not([disabled])') ?? section.current ?? panelTitle(), }); const removeWallet = useMemo(() => Confirmation.createHandle(), []); const hasRows = wallets.length > 0 || (availableProviders.length > 0 && Boolean(onConnect)); diff --git a/packages/mosaic/src/features/user-profile/user-profile.provider.tsx b/packages/mosaic/src/features/user-profile/user-profile.provider.tsx new file mode 100644 index 00000000000..63316be5304 --- /dev/null +++ b/packages/mosaic/src/features/user-profile/user-profile.provider.tsx @@ -0,0 +1,22 @@ +import { createContext, type ReactNode, useContext } from 'react'; + +import type { AdditionalOAuthScopes } from './user-profile-connected-accounts-section/user-profile-connected-accounts-section.model'; + +export interface UserProfileOptions { + additionalOAuthScopes?: AdditionalOAuthScopes; + mode?: 'modal' | 'mounted'; +} + +export interface UserProfileProviderProps extends UserProfileOptions { + children: ReactNode; +} + +const UserProfileContext = createContext({}); + +export function UserProfileProvider({ additionalOAuthScopes, mode, children }: UserProfileProviderProps) { + return {children}; +} + +export function useUserProfileOptions(): UserProfileOptions { + return useContext(UserProfileContext); +} diff --git a/packages/swingset/src/app/(clerk)/live/organization-danger/page.tsx b/packages/swingset/src/app/(clerk)/live/organization-danger/page.tsx index ad5b8e6467f..4aa0b5b100c 100644 --- a/packages/swingset/src/app/(clerk)/live/organization-danger/page.tsx +++ b/packages/swingset/src/app/(clerk)/live/organization-danger/page.tsx @@ -1,6 +1,7 @@ 'use client'; import { OrganizationProfileDangerSection } from '@clerk/mosaic/features/organization-profile/organization-profile-danger-section/organization-profile-danger-section'; +import { OrganizationProfileProvider } from '@clerk/mosaic/features/organization-profile/organization-profile.provider'; import { MosaicProvider } from '@clerk/mosaic/mosaic-provider'; import { useOrganization, useUser } from '@clerk/nextjs'; import Link from 'next/link'; @@ -34,7 +35,11 @@ export default function OrganizationDangerLivePage() { {isLoaded && isSignedIn && !organization ? (

Pick an active organization to see its danger zone.

) : null} - {isLoaded && isSignedIn ? : null} + {isLoaded && isSignedIn ? ( + + + + ) : null}
); diff --git a/packages/swingset/src/stories/fixtures/user-profile.tsx b/packages/swingset/src/stories/fixtures/user-profile.tsx index 073b6297854..2ecb9b1bbe3 100644 --- a/packages/swingset/src/stories/fixtures/user-profile.tsx +++ b/packages/swingset/src/stories/fixtures/user-profile.tsx @@ -18,7 +18,7 @@ import { UserProfilePasswordSectionView } from '@clerk/mosaic/features/user-prof import { UserProfilePhoneSectionView } from '@clerk/mosaic/features/user-profile/user-profile-phone-section/user-profile-phone-section.view'; import { UserProfileProfileSectionView } from '@clerk/mosaic/features/user-profile/user-profile-profile-section/user-profile-profile-section.view'; import { UserProfileWeb3WalletsSectionView } from '@clerk/mosaic/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallets-section.view'; -import { useRef, useState } from 'react'; +import { useState } from 'react'; import { useChaosFixture } from '@/components/ChaosProvider'; import { chaosEmail, chaosRows, chaosText } from '@/lib/chaos'; @@ -64,7 +64,6 @@ const examplePaymentMethods: UserProfilePaymentMethod[] = [ * stories that need a realistic profile surface without being about it. */ export function useUserProfileFixture() { - const titleRef = useRef(null); const connections = useConnectedAccountsFixture(); const wallets = useWeb3WalletsFixture(); const profile = useUserProfileProfileFixture(); @@ -108,20 +107,13 @@ export function useUserProfileFixture() { const apiKeys = useAPIKeysTableFixture(); const pages: UserProfileViewProps['pages'] = { account: { - titleRef, children: ( <> - titleRef.current} - /> - titleRef.current} - /> + + ), diff --git a/packages/swingset/src/stories/organization-profile-danger-section.mdx b/packages/swingset/src/stories/organization-profile-danger-section.mdx index 85887f130b9..2328528110c 100644 --- a/packages/swingset/src/stories/organization-profile-danger-section.mdx +++ b/packages/swingset/src/stories/organization-profile-danger-section.mdx @@ -5,7 +5,8 @@ import * as Stories from './organization-profile-danger-section.stories'; Leaving and deleting the active organization. Both ask for the organization's name to be typed out. Leaving is offered to every member. Deleting needs the `org:sys_profile:delete` permission and admin delete turned on for the instance. After either one, the section refreshes the user's organization -lists and navigates to `afterLeaveOrganizationUrl`, falling back to the instance's setting. +lists and navigates to the `afterLeaveOrganizationUrl` set on `OrganizationProfileProvider`, falling back to the +instance's setting. Below `26rem` of group width, through `Section.Item`'s `wrap`, each action wraps under its description. diff --git a/packages/swingset/src/stories/organization-profile-general-panel.mdx b/packages/swingset/src/stories/organization-profile-general-panel.mdx index d556647a7ab..85678425b5f 100644 --- a/packages/swingset/src/stories/organization-profile-general-panel.mdx +++ b/packages/swingset/src/stories/organization-profile-general-panel.mdx @@ -34,6 +34,17 @@ import { OrganizationProfileProfileSection } from '@clerk/mosaic/features/organi ; ``` +Wrap the panel in `OrganizationProfileProvider` to set where the user goes after leaving or deleting the organization. + +```tsx +import { OrganizationProfileGeneralPanel } from '@clerk/mosaic/features/organization-profile/organization-profile-general-panel'; +import { OrganizationProfileProvider } from '@clerk/mosaic/features/organization-profile/organization-profile.provider'; + + + +; +``` + ## Without the danger zone The panel shows only the organization details, read-only. diff --git a/packages/swingset/src/stories/panel.component.mdx b/packages/swingset/src/stories/panel.component.mdx index 51ab3c7ef60..c358c47dc08 100644 --- a/packages/swingset/src/stories/panel.component.mdx +++ b/packages/swingset/src/stories/panel.component.mdx @@ -62,6 +62,19 @@ Every part takes `xstyle`, `ref`, and `render` along with its element's props. | Inside `Profile.ContentPanel` | `h3` | `h4` | | Inside `HeadingLevelProvider level={4}` | `h4` | `h5` | +## Focus + +`Panel.Title` takes `tabIndex={-1}`, so it can receive focus without joining the tab order. Content +inside `Panel.Root` reaches the title with `usePanelTitle()`, which returns a function that gives +the title element. Inside a profile page, that is the profile's page title. + +```tsx +import { usePanelTitle } from '@clerk/mosaic/components/panel'; + +const panelTitle = usePanelTitle(); +panelTitle()?.focus(); +``` + ## Styling Each part carries a stable class and reflects no state, so target the classes directly: diff --git a/packages/swingset/src/stories/user-profile-profile-panel.mdx b/packages/swingset/src/stories/user-profile-profile-panel.mdx index 00135378f8e..278ebedae37 100644 --- a/packages/swingset/src/stories/user-profile-profile-panel.mdx +++ b/packages/swingset/src/stories/user-profile-profile-panel.mdx @@ -35,3 +35,14 @@ import { UserProfileProfileSection } from '@clerk/mosaic/features/user-profile/u ; ``` + +Wrap the panel in `UserProfileProvider` to set `additionalOAuthScopes` for connected accounts, or `mode='modal'` when the panel renders inside a modal. + +```tsx +import { UserProfileProfilePanel } from '@clerk/mosaic/features/user-profile/user-profile-profile-panel'; +import { UserProfileProvider } from '@clerk/mosaic/features/user-profile/user-profile.provider'; + + + +; +``` diff --git a/packages/swingset/src/stories/user-profile-profile-panel.stories.tsx b/packages/swingset/src/stories/user-profile-profile-panel.stories.tsx index 2c7ce6ce15d..ec93ff1c53d 100644 --- a/packages/swingset/src/stories/user-profile-profile-panel.stories.tsx +++ b/packages/swingset/src/stories/user-profile-profile-panel.stories.tsx @@ -4,7 +4,6 @@ import { UserProfilePhoneSectionView } from '@clerk/mosaic/features/user-profile import { UserProfileProfilePanelView } from '@clerk/mosaic/features/user-profile/user-profile-profile-panel.view'; import { UserProfileProfileSectionView } from '@clerk/mosaic/features/user-profile/user-profile-profile-section/user-profile-profile-section.view'; import { UserProfileWeb3WalletsSectionView } from '@clerk/mosaic/features/user-profile/user-profile-web3-wallets-section/user-profile-web3-wallets-section.view'; -import { useRef } from 'react'; import type { StoryMeta } from '@/lib/types'; @@ -27,7 +26,6 @@ export const meta: StoryMeta = { }; export function Default(_args: Record) { - const titleRef = useRef(null); const connections = useConnectedAccountsFixture(); const wallets = useWeb3WalletsFixture(); const profile = useUserProfileProfileFixture(); @@ -35,18 +33,12 @@ export function Default(_args: Record) { const phones = useUserProfilePhonesFixture(); return ( - + - titleRef.current} - /> - titleRef.current} - /> + + );