diff --git a/.changeset/honest-chairs-cheat.md b/.changeset/honest-chairs-cheat.md
new file mode 100644
index 00000000000..a845151cc84
--- /dev/null
+++ b/.changeset/honest-chairs-cheat.md
@@ -0,0 +1,2 @@
+---
+---
diff --git a/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-add-email.dialog.test.tsx b/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-add-email.dialog.test.tsx
index 5f7892f3338..86eaa964d24 100644
--- a/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-add-email.dialog.test.tsx
+++ b/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-add-email.dialog.test.tsx
@@ -63,7 +63,8 @@ describe('UserProfileAddEmailDialog', () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
render();
- await waitFor(() => expect(screen.getByRole('textbox', { name: 'Verification code' })).toHaveFocus());
+ await waitFor(() => expect(screen.getByRole('button', { name: 'Close', exact: true })).toHaveFocus());
+ await user.click(screen.getByRole('textbox', { name: 'Verification code' }));
if (method === 'typing') {
await user.keyboard('12345');
diff --git a/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-add-email.integration.test.tsx b/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-add-email.integration.test.tsx
index ceb1810f670..6b82e9d908f 100644
--- a/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-add-email.integration.test.tsx
+++ b/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-add-email.integration.test.tsx
@@ -30,7 +30,9 @@ describe('profile add email', () => {
expect(screen.getByRole('dialog', { name: 'Add email' })).toBeInTheDocument();
await user.type(screen.getByRole('textbox', { name: 'Email' }), 'new@example.com');
await user.click(screen.getByRole('button', { name: 'Send code' }));
- await user.type(await screen.findByRole('textbox', { name: 'Verification code' }), '123456');
+ const codeInput = await screen.findByRole('textbox', { name: 'Verification code' });
+ await waitFor(() => expect(codeInput).toHaveFocus());
+ await user.keyboard('123456');
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
expect(onSend).toHaveBeenCalledExactlyOnceWith('new@example.com');
expect(onVerify).toHaveBeenCalledExactlyOnceWith('new@example.com', '123456');
diff --git a/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-add-phone.dialog.test.tsx b/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-add-phone.dialog.test.tsx
index 8ad5297eb5e..579e56d6e3f 100644
--- a/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-add-phone.dialog.test.tsx
+++ b/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-add-phone.dialog.test.tsx
@@ -55,7 +55,8 @@ describe('UserProfileAddPhoneDialog', () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
render();
- await waitFor(() => expect(screen.getByRole('textbox', { name: 'Verification code' })).toHaveFocus());
+ await waitFor(() => expect(screen.getByRole('button', { name: 'Close', exact: true })).toHaveFocus());
+ await user.click(screen.getByRole('textbox', { name: 'Verification code' }));
if (method === 'typing') {
await user.keyboard('12345');
diff --git a/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-add-phone.integration.test.tsx b/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-add-phone.integration.test.tsx
index 876f2bc2382..38918699fe8 100644
--- a/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-add-phone.integration.test.tsx
+++ b/packages/ui/src/mosaic/features/user-profile/__tests__/user-profile-add-phone.integration.test.tsx
@@ -30,7 +30,9 @@ describe('profile add phone', () => {
expect(screen.getByRole('dialog', { name: 'Add phone number' })).toBeInTheDocument();
await user.type(screen.getByRole('textbox', { name: 'Phone' }), '8015550100');
await user.click(screen.getByRole('button', { name: 'Send code' }));
- await user.type(await screen.findByRole('textbox', { name: 'Verification code' }), '123456');
+ const codeInput = await screen.findByRole('textbox', { name: 'Verification code' });
+ await waitFor(() => expect(codeInput).toHaveFocus());
+ await user.keyboard('123456');
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
expect(onSend).toHaveBeenCalledExactlyOnceWith('+18015550100');
expect(onVerify).toHaveBeenCalledExactlyOnceWith('+18015550100', '123456');
diff --git a/packages/ui/src/mosaic/features/user-profile/user-profile-account-section/user-profile-add-email.dialog.tsx b/packages/ui/src/mosaic/features/user-profile/user-profile-account-section/user-profile-add-email.dialog.tsx
index f7aaeab07a2..9d8008cd997 100644
--- a/packages/ui/src/mosaic/features/user-profile/user-profile-account-section/user-profile-add-email.dialog.tsx
+++ b/packages/ui/src/mosaic/features/user-profile/user-profile-account-section/user-profile-add-email.dialog.tsx
@@ -1,5 +1,5 @@
import * as stylex from '@stylexjs/stylex';
-import type { FormEvent } from 'react';
+import type { FormEvent, Ref } from 'react';
import { useId, useRef } from 'react';
import { Button, SubmitButton } from '../../../components/button';
@@ -7,7 +7,7 @@ import { Card } from '../../../components/card';
import type { DialogTriggerProps } from '../../../components/dialog';
import { Dialog } from '../../../components/dialog';
import { Field } from '../../../components/field';
-import { Flow } from '../../../components/flow';
+import { Flow, useFlowAutoFocus } from '../../../components/flow';
import { Input } from '../../../components/input';
import { Otp } from '../../../components/otp';
import { styles } from '../user-profile-profile-panel.styles';
@@ -32,16 +32,7 @@ export interface UserProfileAddEmailDialogProps {
}
export function UserProfileAddEmailDialog(props: UserProfileAddEmailDialogProps) {
- const emailFormId = useId();
- const verifyFormId = useId();
const emailRef = useRef(null);
- const verifyRef = useRef(null);
- const [beforeSeconds, afterSeconds] = m.verify.resendCountdown.split('{seconds}');
-
- const handleSubmit = (event: FormEvent) => {
- event.preventDefault();
- props.onSubmit();
- };
return (
: null}
- emailRef.current ?? verifyRef.current?.querySelector('input:not([type="hidden"])') ?? true
- }
+ initialFocus={props.step === 'email' ? emailRef : undefined}
>
(
<>
-
- {m.email.title}
- {m.email.description}
-
-
- }
- >
-
- {m.email.label}
- current.onEmailAddressChange(event.target.value)}
- />
- {current.errorMessage ? {current.errorMessage} : null}
-
-
-
-
- {m.email.submit}
-
-
+
-
-
- {m.verify.title}
-
- {fill(m.verify.description, { emailAddress: current.emailAddress })}
-
-
-
- }
- >
-
- {m.verify.label}
-
- {current.errorMessage ? {current.errorMessage} : null}
-
-
-
-
-
- }
- >
- {m.verify.cancel}
-
-
- {m.verify.submit}
-
-
+
+
>
)}
@@ -191,3 +86,162 @@ export function UserProfileAddEmailDialog(props: UserProfileAddEmailDialogProps)
);
}
+
+interface EnterEmailStepProps {
+ inputRef: Ref;
+ emailAddress: string;
+ onEmailAddressChange: (value: string) => void;
+ onSubmit: () => void;
+ isPending?: boolean;
+ errorMessage?: string;
+}
+
+function EnterEmailStep(props: EnterEmailStepProps) {
+ const emailFormId = useId();
+
+ const handleSubmit = (event: FormEvent) => {
+ event.preventDefault();
+ props.onSubmit();
+ };
+
+ return (
+ <>
+
+ {m.email.title}
+ {m.email.description}
+
+
+ }
+ >
+
+ {m.email.label}
+ props.onEmailAddressChange(event.target.value)}
+ />
+ {props.errorMessage ? {props.errorMessage} : null}
+
+
+
+
+ {m.email.submit}
+
+
+ >
+ );
+}
+
+interface VerifyEmailStepProps {
+ emailAddress: string;
+ code: string;
+ onCodeChange: (value: string) => void;
+ onSubmit: (code?: string) => void;
+ onResend: () => void;
+ isPending?: boolean;
+ errorMessage?: string;
+ isResending?: boolean;
+ resendSeconds?: number;
+}
+
+function VerifyEmailStep(props: VerifyEmailStepProps) {
+ const verifyFormId = useId();
+ const [beforeSeconds, afterSeconds] = m.verify.resendCountdown.split('{seconds}');
+
+ const handleSubmit = (event: FormEvent) => {
+ event.preventDefault();
+ props.onSubmit();
+ };
+
+ return (
+ <>
+
+ {m.verify.title}
+ {fill(m.verify.description, { emailAddress: props.emailAddress })}
+
+
+ }
+ >
+
+ {m.verify.label}
+ ()}
+ name='code'
+ value={props.code}
+ onValueChange={props.onCodeChange}
+ onComplete={props.onSubmit}
+ />
+ {props.errorMessage ? {props.errorMessage} : null}
+
+
+
+
+
+ }
+ >
+ {m.verify.cancel}
+
+
+ {m.verify.submit}
+
+
+ >
+ );
+}
diff --git a/packages/ui/src/mosaic/features/user-profile/user-profile-account-section/user-profile-add-phone.dialog.tsx b/packages/ui/src/mosaic/features/user-profile/user-profile-account-section/user-profile-add-phone.dialog.tsx
index 49079616771..00842155302 100644
--- a/packages/ui/src/mosaic/features/user-profile/user-profile-account-section/user-profile-add-phone.dialog.tsx
+++ b/packages/ui/src/mosaic/features/user-profile/user-profile-account-section/user-profile-add-phone.dialog.tsx
@@ -1,5 +1,5 @@
import * as stylex from '@stylexjs/stylex';
-import type { FormEvent } from 'react';
+import type { FormEvent, Ref } from 'react';
import { useId, useRef } from 'react';
import { stringToFormattedPhoneString } from '../../../../utils/phoneUtils';
@@ -8,7 +8,7 @@ import { Card } from '../../../components/card';
import type { DialogTriggerProps } from '../../../components/dialog';
import { Dialog } from '../../../components/dialog';
import { Field } from '../../../components/field';
-import { Flow } from '../../../components/flow';
+import { Flow, useFlowAutoFocus } from '../../../components/flow';
import { Otp } from '../../../components/otp';
import { PhoneInput } from '../../../components/phone-input';
import { styles } from '../user-profile-profile-panel.styles';
@@ -33,16 +33,7 @@ export interface UserProfileAddPhoneDialogProps {
}
export function UserProfileAddPhoneDialog(props: UserProfileAddPhoneDialogProps) {
- const phoneFormId = useId();
- const verifyFormId = useId();
const phoneRef = useRef(null);
- const verifyRef = useRef(null);
- const [beforeSeconds, afterSeconds] = m.verify.resendCountdown.split('{seconds}');
-
- const handleSubmit = (event: FormEvent) => {
- event.preventDefault();
- props.onSubmit();
- };
return (
: null}
- phoneRef.current ?? verifyRef.current?.querySelector('input:not([type="hidden"])') ?? true
- }
+ initialFocus={props.step === 'phone' ? phoneRef : undefined}
>
(
<>
-
- {m.phone.title}
- {m.phone.description}
-
-
- }
- >
-
- {m.phone.label}
-
-
- {current.errorMessage}
-
-
-
-
-
- {m.phone.submit}
-
-
+
-
-
- {m.verify.title}
-
- {fill(m.verify.description, { phoneNumber: stringToFormattedPhoneString(current.phoneNumber) })}
-
-
-
- }
- >
-
- {m.verify.label}
-
-
- {current.errorMessage}
-
-
-
-
-
-
- }
- >
- {m.verify.cancel}
-
-
- {m.verify.submit}
-
-
+
+
>
)}
@@ -194,3 +87,166 @@ export function UserProfileAddPhoneDialog(props: UserProfileAddPhoneDialogProps)
);
}
+
+interface EnterPhoneStepProps {
+ inputRef: Ref;
+ phoneNumber: string;
+ onPhoneNumberChange: (value: string) => void;
+ onSubmit: () => void;
+ isPending?: boolean;
+ errorMessage?: string;
+}
+
+function EnterPhoneStep(props: EnterPhoneStepProps) {
+ const phoneFormId = useId();
+
+ const handleSubmit = (event: FormEvent) => {
+ event.preventDefault();
+ props.onSubmit();
+ };
+
+ return (
+ <>
+
+ {m.phone.title}
+ {m.phone.description}
+
+
+ }
+ >
+
+ {m.phone.label}
+
+
+ {props.errorMessage}
+
+
+
+
+
+ {m.phone.submit}
+
+
+ >
+ );
+}
+
+interface VerifyPhoneStepProps {
+ phoneNumber: string;
+ code: string;
+ onCodeChange: (value: string) => void;
+ onSubmit: (code?: string) => void;
+ onResend: () => void;
+ isPending?: boolean;
+ errorMessage?: string;
+ isResending?: boolean;
+ resendSeconds?: number;
+}
+
+function VerifyPhoneStep(props: VerifyPhoneStepProps) {
+ const verifyFormId = useId();
+ const [beforeSeconds, afterSeconds] = m.verify.resendCountdown.split('{seconds}');
+
+ const handleSubmit = (event: FormEvent) => {
+ event.preventDefault();
+ props.onSubmit();
+ };
+
+ return (
+ <>
+
+ {m.verify.title}
+
+ {fill(m.verify.description, { phoneNumber: stringToFormattedPhoneString(props.phoneNumber) })}
+
+
+
+ }
+ >
+
+ {m.verify.label}
+ ()}
+ name='code'
+ value={props.code}
+ onValueChange={props.onCodeChange}
+ onComplete={props.onSubmit}
+ />
+
+ {props.errorMessage}
+
+
+
+
+
+
+ }
+ >
+ {m.verify.cancel}
+
+
+ {m.verify.submit}
+
+
+ >
+ );
+}