Skip to content

Commit 0da12ba

Browse files
authored
fix(account-center): support Enter key to submit forms (#8088)
* fix(account-center): support Enter key to submit forms Wrap form containers in <form> elements and use htmlType="submit" on buttons so pressing Enter in text inputs triggers form submission. * refactor: use FormEvent<HTMLFormElement> for type consistency
1 parent dcde58c commit 0da12ba

File tree

3 files changed

+98
-92
lines changed

3 files changed

+98
-92
lines changed

packages/account-center/src/components/CodeVerification/index.tsx

Lines changed: 42 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import VerificationCodeInput, {
66
} from '@experience/shared/components/VerificationCode';
77
import { SignInIdentifier } from '@logto/schemas';
88
import type { TFuncKey } from 'i18next';
9-
import { useCallback, useContext, useEffect, useState } from 'react';
9+
import { useCallback, useContext, useEffect, useState, type FormEvent } from 'react';
1010
import { useTranslation } from 'react-i18next';
1111

1212
import LoadingContext from '@ac/Providers/LoadingContextProvider/LoadingContext';
@@ -103,31 +103,35 @@ const CodeVerification = ({
103103
};
104104
}, [countdown]);
105105

106-
const handleSendCode = useCallback(async () => {
107-
if (!identifier || loading) {
108-
return;
109-
}
106+
const handleSendCode = useCallback(
107+
async (event?: FormEvent<HTMLFormElement>) => {
108+
event?.preventDefault();
109+
if (!identifier || loading) {
110+
return;
111+
}
110112

111-
const [error, result] = await sendCodeRequest(identifier);
113+
const [error, result] = await sendCodeRequest(identifier);
112114

113-
if (error) {
114-
await handleError(error);
115-
return;
116-
}
115+
if (error) {
116+
await handleError(error);
117+
return;
118+
}
117119

118-
if (!result) {
119-
setToast(t('account_center.email_verification.error_send_failed'));
120-
return;
121-
}
120+
if (!result) {
121+
setToast(t('account_center.email_verification.error_send_failed'));
122+
return;
123+
}
122124

123-
setPendingVerificationRecord({
124-
recordId: result.verificationRecordId,
125-
expiresAt: result.expiresAt,
126-
});
127-
setCodeInput([]);
128-
setHasSentCode(true);
129-
startCountdown();
130-
}, [handleError, identifier, loading, sendCodeRequest, setToast, startCountdown, t]);
125+
setPendingVerificationRecord({
126+
recordId: result.verificationRecordId,
127+
expiresAt: result.expiresAt,
128+
});
129+
setCodeInput([]);
130+
setHasSentCode(true);
131+
startCountdown();
132+
},
133+
[handleError, identifier, loading, sendCodeRequest, setToast, startCountdown, t]
134+
);
131135

132136
const handleVerify = useCallback(
133137
async (code: string[]) => {
@@ -197,7 +201,17 @@ const CodeVerification = ({
197201
onBack={onBack}
198202
>
199203
{hasSentCode ? (
200-
<div className={styles.container}>
204+
<form
205+
className={styles.container}
206+
onSubmit={(event) => {
207+
event.preventDefault();
208+
if (!isCodeReady(codeInput)) {
209+
setToast(t('error.invalid_passcode'));
210+
return;
211+
}
212+
void handleVerify(codeInput);
213+
}}
214+
>
201215
<VerificationCodeInput
202216
name={codeInputName}
203217
className={styles.codeInput}
@@ -228,23 +242,16 @@ const CodeVerification = ({
228242
<Button
229243
title="action.confirm"
230244
type="primary"
245+
htmlType="submit"
231246
className={styles.submit}
232247
isLoading={loading}
233-
onClick={() => {
234-
if (!isCodeReady(codeInput)) {
235-
setToast(t('error.invalid_passcode'));
236-
return;
237-
}
238-
239-
void handleVerify(codeInput);
240-
}}
241248
/>
242249
{hasAlternativeMethod && (
243250
<SwitchVerificationMethodLink className={styles.switchLink} onClick={onSwitchMethod} />
244251
)}
245-
</div>
252+
</form>
246253
) : (
247-
<div className={styles.prepare}>
254+
<form className={styles.prepare} onSubmit={handleSendCode}>
248255
<SmartInputField
249256
readOnly
250257
className={styles.identifierInput}
@@ -256,17 +263,15 @@ const CodeVerification = ({
256263
<Button
257264
title="account_center.email_verification.send"
258265
type="primary"
266+
htmlType="submit"
259267
className={styles.prepareAction}
260268
disabled={loading}
261269
isLoading={loading}
262-
onClick={() => {
263-
void handleSendCode();
264-
}}
265270
/>
266271
{hasAlternativeMethod && (
267272
<SwitchVerificationMethodLink className={styles.switchLink} onClick={onSwitchMethod} />
268273
)}
269-
</div>
274+
</form>
270275
)}
271276
</SecondaryPageLayout>
272277
);

packages/account-center/src/pages/CodeFlow/IdentifierSendStep.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Button from '@experience/shared/components/Button';
22
import SmartInputField from '@experience/shared/components/InputFields/SmartInputField';
33
import { type SignInIdentifier } from '@logto/schemas';
44
import { type TFuncKey } from 'i18next';
5-
import { useContext, useEffect, useState } from 'react';
5+
import { useContext, useEffect, useState, type FormEvent } from 'react';
66
import { useTranslation } from 'react-i18next';
77

88
import LoadingContext from '@ac/Providers/LoadingContextProvider/LoadingContext';
@@ -55,7 +55,8 @@ const IdentifierSendStep = ({
5555
setPendingValue(value);
5656
}, [value]);
5757

58-
const handleSend = async () => {
58+
const handleSend = async (event?: FormEvent<HTMLFormElement>) => {
59+
event?.preventDefault();
5960
const target = pendingValue.trim();
6061

6162
if (!target || loading) {
@@ -79,7 +80,7 @@ const IdentifierSendStep = ({
7980

8081
return (
8182
<SecondaryPageLayout title={titleKey} description={descriptionKey}>
82-
<div className={styles.container}>
83+
<form className={styles.container} onSubmit={handleSend}>
8384
<SmartInputField
8485
className={styles.identifierInput}
8586
name={name}
@@ -95,14 +96,12 @@ const IdentifierSendStep = ({
9596
<Button
9697
title="account_center.code_verification.send"
9798
type="primary"
99+
htmlType="submit"
98100
className={styles.submit}
99101
disabled={!pendingValue || loading}
100102
isLoading={loading}
101-
onClick={() => {
102-
void handleSend();
103-
}}
104103
/>
105-
</div>
104+
</form>
106105
</SecondaryPageLayout>
107106
);
108107
};

packages/account-center/src/pages/TotpBinding/index.tsx

Lines changed: 50 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import VerificationCodeInput, {
55
} from '@experience/shared/components/VerificationCode';
66
import { AccountCenterControlValue, MfaFactor, type Mfa } from '@logto/schemas';
77
import qrcode from 'qrcode';
8-
import { useCallback, useContext, useEffect, useState } from 'react';
8+
import { useCallback, useContext, useEffect, useState, type FormEvent } from 'react';
99
import { isMobile } from 'react-device-detect';
1010
import { useTranslation } from 'react-i18next';
1111
import { useNavigate } from 'react-router-dom';
@@ -115,53 +115,57 @@ const TotpBinding = () => {
115115
setToast(t('mfa.secret_key_copied'));
116116
}, [secret, setToast, t]);
117117

118-
const handleSubmit = useCallback(async () => {
119-
if (!verificationId || !secret || loading || !isCodeReady(codeInput)) {
120-
return;
121-
}
118+
const handleSubmit = useCallback(
119+
async (event?: FormEvent<HTMLFormElement>) => {
120+
event?.preventDefault();
121+
if (!verificationId || !secret || loading || !isCodeReady(codeInput)) {
122+
return;
123+
}
122124

123-
setErrorMessage(undefined);
125+
setErrorMessage(undefined);
124126

125-
const codeString = codeInput.join('');
126-
const [error] = await addTotpRequest(verificationId, { secret, code: codeString });
127+
const codeString = codeInput.join('');
128+
const [error] = await addTotpRequest(verificationId, { secret, code: codeString });
127129

128-
if (error) {
129-
await handleError(error, {
130-
'verification_record.permission_denied': async () => {
131-
setVerificationId(undefined);
132-
setToast(t('account_center.verification.verification_required'));
133-
},
134-
'user.totp_already_in_use': async (requestError) => {
135-
setToast(requestError.message);
136-
},
137-
'session.mfa.invalid_totp_code': async () => {
138-
setErrorMessage(t('error.invalid_passcode'));
139-
setCodeInput([]);
140-
},
141-
});
142-
return;
143-
}
130+
if (error) {
131+
await handleError(error, {
132+
'verification_record.permission_denied': async () => {
133+
setVerificationId(undefined);
134+
setToast(t('account_center.verification.verification_required'));
135+
},
136+
'user.totp_already_in_use': async (requestError) => {
137+
setToast(requestError.message);
138+
},
139+
'session.mfa.invalid_totp_code': async () => {
140+
setErrorMessage(t('error.invalid_passcode'));
141+
setCodeInput([]);
142+
},
143+
});
144+
return;
145+
}
144146

145-
if (isBackupCodeEnabled(experienceSettings?.mfa) && !hasBackupCodes) {
146-
void navigate(backupCodeRoute, { replace: true });
147-
return;
148-
}
147+
if (isBackupCodeEnabled(experienceSettings?.mfa) && !hasBackupCodes) {
148+
void navigate(backupCodeRoute, { replace: true });
149+
return;
150+
}
149151

150-
void navigate(totpSuccessRoute, { replace: true });
151-
}, [
152-
addTotpRequest,
153-
codeInput,
154-
experienceSettings?.mfa,
155-
handleError,
156-
hasBackupCodes,
157-
loading,
158-
navigate,
159-
secret,
160-
setToast,
161-
setVerificationId,
162-
t,
163-
verificationId,
164-
]);
152+
void navigate(totpSuccessRoute, { replace: true });
153+
},
154+
[
155+
addTotpRequest,
156+
codeInput,
157+
experienceSettings?.mfa,
158+
handleError,
159+
hasBackupCodes,
160+
loading,
161+
navigate,
162+
secret,
163+
setToast,
164+
setVerificationId,
165+
t,
166+
verificationId,
167+
]
168+
);
165169

166170
if (
167171
!accountCenterSettings?.enabled ||
@@ -251,7 +255,7 @@ const TotpBinding = () => {
251255

252256
{/* Step 2: Verification Code */}
253257
<div className={styles.divider} />
254-
<div className={styles.step}>
258+
<form className={styles.step} onSubmit={handleSubmit}>
255259
<div className={styles.stepTitle}>
256260
<DynamicT
257261
forKey="mfa.step"
@@ -276,13 +280,11 @@ const TotpBinding = () => {
276280
<Button
277281
title="action.continue"
278282
type="primary"
283+
htmlType="submit"
279284
className={styles.submitButton}
280285
isLoading={loading}
281-
onClick={() => {
282-
void handleSubmit();
283-
}}
284286
/>
285-
</div>
287+
</form>
286288
</div>
287289
</SecondaryPageLayout>
288290
);

0 commit comments

Comments
 (0)