fix otp paste not filling all fields on mobile
This commit is contained in:
1 parent
c3a70b123d
commit
c1c19ca24b
4 files changed
+76
-13
No files matched your search
@@ -41,7 +41,12 @@ export function LoginForm() {
|
||||
<Logo />
|
||||
</Icon>
|
||||
<Heading>umami</Heading>
|
||||
<Form onSubmit={handleSubmit} error={getErrorMessage(error)} style={{ minWidth: 300 }}>
|
||||
<Form
|
||||
onSubmit={handleSubmit}
|
||||
error={getErrorMessage(error)}
|
||||
defaultValues={{ username: '', password: '' }}
|
||||
style={{ minWidth: 300 }}
|
||||
>
|
||||
<FormField
|
||||
label={t(labels.username)}
|
||||
data-test="input-username"
|
||||
|
||||
@@ -100,7 +100,12 @@ export function LoginTwoFactorPage() {
|
||||
<Text style={{ color: 'var(--color-danger, red)' }}>{lockMessage}</Text>
|
||||
)}
|
||||
|
||||
<Form onSubmit={handleSubmit} error={error ?? undefined} style={{ minWidth: 300 }}>
|
||||
<Form
|
||||
onSubmit={handleSubmit}
|
||||
error={error ?? undefined}
|
||||
defaultValues={{ backupCode: '' }}
|
||||
style={{ minWidth: 300 }}
|
||||
>
|
||||
{!useBackup ? (
|
||||
<Column gap="2">
|
||||
<Text weight="bold">{t(labels.twoFactorEnterCode)}</Text>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { fireEvent } from '@testing-library/react';
|
||||
import { expect, test, vi } from 'vitest';
|
||||
import { render, screen } from '@/test/render';
|
||||
import { OtpInput } from './OtpInput';
|
||||
|
||||
function getDigitInput(index: number) {
|
||||
return screen.getByLabelText(`Digit ${index + 1}`) as HTMLInputElement;
|
||||
}
|
||||
|
||||
test('typing individual digits fills fields in order and advances focus', async () => {
|
||||
const onChange = vi.fn();
|
||||
const { user } = render(<OtpInput value="" onChange={onChange} />);
|
||||
|
||||
await user.click(getDigitInput(0));
|
||||
await user.keyboard('1');
|
||||
|
||||
expect(onChange).toHaveBeenLastCalledWith('1');
|
||||
});
|
||||
|
||||
test('a real paste event distributes the code across all fields and calls onComplete', async () => {
|
||||
const onChange = vi.fn();
|
||||
const onComplete = vi.fn();
|
||||
const { user } = render(<OtpInput value="" onChange={onChange} onComplete={onComplete} />);
|
||||
|
||||
await user.click(getDigitInput(0));
|
||||
await user.paste('123456');
|
||||
|
||||
expect(onChange).toHaveBeenLastCalledWith('123456');
|
||||
expect(onComplete).toHaveBeenCalledWith('123456');
|
||||
});
|
||||
|
||||
test('a multi-character value delivered via onChange (mobile paste fallback) fills all fields', () => {
|
||||
const onChange = vi.fn();
|
||||
const onComplete = vi.fn();
|
||||
render(<OtpInput value="" onChange={onChange} onComplete={onComplete} />);
|
||||
|
||||
const input = getDigitInput(0);
|
||||
fireEvent.change(input, { target: { value: '123456' } });
|
||||
|
||||
expect(onChange).toHaveBeenLastCalledWith('123456');
|
||||
expect(onComplete).toHaveBeenCalledWith('123456');
|
||||
});
|
||||
@@ -27,8 +27,28 @@ export function OtpInput({ value, onChange, onComplete, disabled }: OtpInputProp
|
||||
}
|
||||
};
|
||||
|
||||
const applyPastedValue = (digitsStr: string) => {
|
||||
const pasted = digitsStr.replace(/\D/g, '').slice(0, LENGTH);
|
||||
const next = Array.from({ length: LENGTH }, (_, i) => pasted[i] ?? '');
|
||||
const newVal = next.join('');
|
||||
onChange(newVal);
|
||||
if (newVal.length === LENGTH) {
|
||||
onComplete?.(newVal);
|
||||
focusInput(refs.current[LENGTH - 1]);
|
||||
} else {
|
||||
focusInput(refs.current[pasted.length]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInput = (index: number, raw: string) => {
|
||||
const char = raw.replace(/\D/g, '').slice(-1);
|
||||
const digitsOnly = raw.replace(/\D/g, '');
|
||||
|
||||
if (digitsOnly.length > 1) {
|
||||
applyPastedValue(digitsOnly);
|
||||
return;
|
||||
}
|
||||
|
||||
const char = digitsOnly;
|
||||
if (!char) return;
|
||||
update(index, char);
|
||||
if (index < LENGTH - 1) {
|
||||
@@ -56,16 +76,7 @@ export function OtpInput({ value, onChange, onComplete, disabled }: OtpInputProp
|
||||
|
||||
const handlePaste = (e: ClipboardEvent<HTMLInputElement & HTMLTextAreaElement>) => {
|
||||
e.preventDefault();
|
||||
const pasted = e.clipboardData.getData('text').replace(/\D/g, '').slice(0, LENGTH);
|
||||
const next = Array.from({ length: LENGTH }, (_, i) => pasted[i] ?? '');
|
||||
const newVal = next.join('');
|
||||
onChange(newVal);
|
||||
if (newVal.length === LENGTH) {
|
||||
onComplete?.(newVal);
|
||||
focusInput(refs.current[LENGTH - 1]);
|
||||
} else {
|
||||
focusInput(refs.current[pasted.length]);
|
||||
}
|
||||
applyPastedValue(e.clipboardData.getData('text'));
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
Reference in new issue
Block a user