Skip to content

Commit 7e550b1

Browse files
authored
feat(elements): Add autoSubmit prop to OTP input (#2837)
* chore(elements): Update otp-playground to use step * feat(elements): Add autoSubmit prop to OTP input * chore(elements): Bump version
1 parent 1c10689 commit 7e550b1

File tree

6 files changed

+63
-48
lines changed

6 files changed

+63
-48
lines changed

.changeset/cyan-pumpkins-try.md

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

packages/elements/examples/nextjs/app/otp-playground/page.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
'use client';
22

33
import { Field, Input, Label } from '@clerk/elements/common';
4-
import { SignIn, Start } from '@clerk/elements/sign-in';
4+
import { SignIn, Step } from '@clerk/elements/sign-in';
55
import clsx from 'clsx';
66
import { AnimatePresence, motion } from 'framer-motion';
77

88
export default function Page() {
99
return (
1010
<SignIn path='/otp-playground'>
11-
<Start>
11+
<Step name='start'>
1212
<div className='h-dvh flex items-center justify-center bg-neutral-800'>
1313
<Field name='code'>
1414
<Label className='sr-only'>Label</Label>
@@ -48,7 +48,7 @@ export default function Page() {
4848
/>
4949
</Field>
5050
</div>
51-
</Start>
51+
</Step>
5252
</SignIn>
5353
);
5454
}

packages/elements/examples/nextjs/app/sign-in/[[...sign-in]]/page.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export default function SignInPage() {
104104
<CustomField
105105
label='Email Code'
106106
name='code'
107+
autoSubmit
107108
/>
108109

109110
<CustomSubmit>Verify</CustomSubmit>

packages/elements/examples/nextjs/components/form.tsx

+34-32
Original file line numberDiff line numberDiff line change
@@ -33,40 +33,42 @@ function OTPInputSegment({ value, status }: any) {
3333
);
3434
}
3535

36-
export const CustomField = React.forwardRef<typeof Input, { name: string; label: string; required?: boolean }>(
37-
function CustomField({ name, label, required = false }, forwardedRef) {
38-
const inputProps =
39-
name === 'code'
40-
? {
41-
render: OTPInputSegment,
42-
className: 'flex gap-3',
43-
required,
44-
}
45-
: {
46-
className: 'bg-tertiary rounded-sm px-2 py-1 border border-foreground data-[invalid]:border-red-500',
47-
ref: forwardedRef,
48-
required,
49-
};
36+
export const CustomField = React.forwardRef<
37+
typeof Input,
38+
{ name: string; label: string; required?: boolean; autoSubmit?: boolean }
39+
>(function CustomField({ name, label, required = false, autoSubmit = false }, forwardedRef) {
40+
const inputProps =
41+
name === 'code'
42+
? {
43+
render: OTPInputSegment,
44+
className: 'flex gap-3',
45+
required,
46+
autoSubmit,
47+
}
48+
: {
49+
className: 'bg-tertiary rounded-sm px-2 py-1 border border-foreground data-[invalid]:border-red-500',
50+
ref: forwardedRef,
51+
required,
52+
};
5053

51-
return (
52-
<ElementsField
53-
name={name}
54-
className='flex flex-col gap-4'
55-
>
56-
<div className='flex gap-4 justify-between items-center'>
57-
<Label>{label}</Label>
58-
<Input
59-
name={name}
60-
{...inputProps}
61-
/>
62-
</div>
54+
return (
55+
<ElementsField
56+
name={name}
57+
className='flex flex-col gap-4'
58+
>
59+
<div className='flex gap-4 justify-between items-center'>
60+
<Label>{label}</Label>
61+
<Input
62+
name={name}
63+
{...inputProps}
64+
/>
65+
</div>
6366

64-
<FieldError className='block text-red-400 font-mono' />
65-
<FieldState>{({ state }) => <pre className='opacity-60 text-xs'>Field state: {state}</pre>}</FieldState>
66-
</ElementsField>
67-
);
68-
},
69-
);
67+
<FieldError className='block text-red-400 font-mono' />
68+
<FieldState>{({ state }) => <pre className='opacity-60 text-xs'>Field state: {state}</pre>}</FieldState>
69+
</ElementsField>
70+
);
71+
});
7072

7173
type CustomSubmitElement = React.ElementRef<typeof ElementsSubmit>;
7274

packages/elements/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@clerk/elements",
3-
"version": "0.1.11",
3+
"version": "0.1.12",
44
"description": "Clerk Elements",
55
"keywords": [
66
"clerk",

packages/elements/src/react/common/form/otp.tsx

+22-12
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type OTPInputProps = Exclude<
99
> & {
1010
render?: (props: { value: string; status: 'cursor' | 'selected' | 'none'; index: number }) => React.ReactNode;
1111
length?: number;
12+
autoSubmit?: boolean;
1213
};
1314

1415
type SelectionRange = readonly [start: number, end: number];
@@ -44,7 +45,7 @@ export const OTPInput = React.forwardRef<HTMLInputElement, OTPInputProps>(functi
4445
*/
4546
const OTPInputSegmented = React.forwardRef<HTMLInputElement, Required<Pick<OTPInputProps, 'render'>> & OTPInputProps>(
4647
function OTPInput(props, ref) {
47-
const { className, render, length = OTP_LENGTH_DEFAULT, ...rest } = props;
48+
const { className, render, length = OTP_LENGTH_DEFAULT, autoSubmit = false, ...rest } = props;
4849

4950
const innerRef = React.useRef<HTMLInputElement>(null);
5051
const [selectionRange, setSelectionRange] = React.useState<SelectionRange>(props.autoFocus ? ZERO : OUTSIDE);
@@ -60,6 +61,13 @@ const OTPInputSegmented = React.forwardRef<HTMLInputElement, Required<Pick<OTPIn
6061
setSelectionRange(cur => selectionRangeUpdater(cur, innerRef));
6162
}, [props.value]);
6263

64+
// Fire the requestSubmit callback when the input has the required length and autoSubmit is enabled
65+
React.useEffect(() => {
66+
if (String(props.value).length === length && autoSubmit) {
67+
innerRef.current?.form?.requestSubmit();
68+
}
69+
}, [props.value, length, autoSubmit]);
70+
6371
return (
6472
<div
6573
style={
@@ -86,16 +94,7 @@ const OTPInputSegmented = React.forwardRef<HTMLInputElement, Required<Pick<OTPIn
8694
setSelectionRange(cur => selectionRangeUpdater(cur, innerRef));
8795
rest?.onSelect?.(event);
8896
}}
89-
style={{
90-
display: 'block',
91-
cursor: 'default',
92-
background: 'none',
93-
outline: 'none',
94-
appearance: 'none',
95-
color: 'transparent',
96-
position: 'absolute',
97-
inset: 0,
98-
}}
97+
style={inputStyle}
9998
/>
10099
<div
101100
className={className}
@@ -106,7 +105,7 @@ const OTPInputSegmented = React.forwardRef<HTMLInputElement, Required<Pick<OTPIn
106105
}}
107106
>
108107
{Array.from({ length }).map((_, i) => (
109-
<React.Fragment key={i}>
108+
<React.Fragment key={`otp-segment-${i}`}>
110109
{render({
111110
value: String(props.value)[i] || '',
112111
status:
@@ -152,3 +151,14 @@ function selectionRangeUpdater(cur: SelectionRange, inputRef: React.RefObject<HT
152151

153152
return updated;
154153
}
154+
155+
const inputStyle = {
156+
display: 'block',
157+
cursor: 'default',
158+
background: 'none',
159+
outline: 'none',
160+
appearance: 'none',
161+
color: 'transparent',
162+
position: 'absolute',
163+
inset: 0,
164+
} satisfies React.CSSProperties;

0 commit comments

Comments
 (0)