Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new accessible otp input component #2428

Closed
wants to merge 13 commits into from
Closed
126 changes: 126 additions & 0 deletions apps/www/__registry__/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,13 @@ export const Index: Record<string, any> = {
component: React.lazy(() => import("@/registry/default/ui/navigation-menu")),
files: ["registry/default/ui/navigation-menu.tsx"],
},
"otp-input": {
name: "otp-input",
type: "components:ui",
registryDependencies: undefined,
component: React.lazy(() => import("@/registry/default/ui/otp-input")),
files: ["registry/default/ui/otp-input.tsx"],
},
"pagination": {
name: "pagination",
type: "components:ui",
Expand Down Expand Up @@ -775,6 +782,62 @@ export const Index: Record<string, any> = {
component: React.lazy(() => import("@/registry/default/example/navigation-menu-demo")),
files: ["registry/default/example/navigation-menu-demo.tsx"],
},
"otp-input-demo": {
name: "otp-input-demo",
type: "components:example",
registryDependencies: ["otp-input"],
component: React.lazy(() => import("@/registry/default/example/otp-input-demo")),
files: ["registry/default/example/otp-input-demo.tsx"],
},
"otp-input-custom-number": {
name: "otp-input-custom-number",
type: "components:example",
registryDependencies: ["otp-input"],
component: React.lazy(() => import("@/registry/default/example/otp-input-custom-number")),
files: ["registry/default/example/otp-input-custom-number.tsx"],
},
"otp-input-placeholder": {
name: "otp-input-placeholder",
type: "components:example",
registryDependencies: ["otp-input"],
component: React.lazy(() => import("@/registry/default/example/otp-input-placeholder")),
files: ["registry/default/example/otp-input-placeholder.tsx"],
},
"otp-input-separator": {
name: "otp-input-separator",
type: "components:example",
registryDependencies: ["otp-input"],
component: React.lazy(() => import("@/registry/default/example/otp-input-separator")),
files: ["registry/default/example/otp-input-separator.tsx"],
},
"otp-input-controlled": {
name: "otp-input-controlled",
type: "components:example",
registryDependencies: ["otp-input"],
component: React.lazy(() => import("@/registry/default/example/otp-input-controlled")),
files: ["registry/default/example/otp-input-controlled.tsx"],
},
"otp-input-on-paste": {
name: "otp-input-on-paste",
type: "components:example",
registryDependencies: ["otp-input"],
component: React.lazy(() => import("@/registry/default/example/otp-input-on-paste")),
files: ["registry/default/example/otp-input-on-paste.tsx"],
},
"otp-input-form": {
name: "otp-input-form",
type: "components:example",
registryDependencies: ["otp-input"],
component: React.lazy(() => import("@/registry/default/example/otp-input-form")),
files: ["registry/default/example/otp-input-form.tsx"],
},
"otp-input-custom-style": {
name: "otp-input-custom-style",
type: "components:example",
registryDependencies: ["otp-input"],
component: React.lazy(() => import("@/registry/default/example/otp-input-custom-style")),
files: ["registry/default/example/otp-input-custom-style.tsx"],
},
"pagination-demo": {
name: "pagination-demo",
type: "components:example",
Expand Down Expand Up @@ -1392,6 +1455,13 @@ export const Index: Record<string, any> = {
component: React.lazy(() => import("@/registry/new-york/ui/navigation-menu")),
files: ["registry/new-york/ui/navigation-menu.tsx"],
},
"otp-input": {
name: "otp-input",
type: "components:ui",
registryDependencies: undefined,
component: React.lazy(() => import("@/registry/new-york/ui/otp-input")),
files: ["registry/new-york/ui/otp-input.tsx"],
},
"pagination": {
name: "pagination",
type: "components:ui",
Expand Down Expand Up @@ -2001,6 +2071,62 @@ export const Index: Record<string, any> = {
component: React.lazy(() => import("@/registry/new-york/example/navigation-menu-demo")),
files: ["registry/new-york/example/navigation-menu-demo.tsx"],
},
"otp-input-demo": {
name: "otp-input-demo",
type: "components:example",
registryDependencies: ["otp-input"],
component: React.lazy(() => import("@/registry/new-york/example/otp-input-demo")),
files: ["registry/new-york/example/otp-input-demo.tsx"],
},
"otp-input-custom-number": {
name: "otp-input-custom-number",
type: "components:example",
registryDependencies: ["otp-input"],
component: React.lazy(() => import("@/registry/new-york/example/otp-input-custom-number")),
files: ["registry/new-york/example/otp-input-custom-number.tsx"],
},
"otp-input-placeholder": {
name: "otp-input-placeholder",
type: "components:example",
registryDependencies: ["otp-input"],
component: React.lazy(() => import("@/registry/new-york/example/otp-input-placeholder")),
files: ["registry/new-york/example/otp-input-placeholder.tsx"],
},
"otp-input-separator": {
name: "otp-input-separator",
type: "components:example",
registryDependencies: ["otp-input"],
component: React.lazy(() => import("@/registry/new-york/example/otp-input-separator")),
files: ["registry/new-york/example/otp-input-separator.tsx"],
},
"otp-input-controlled": {
name: "otp-input-controlled",
type: "components:example",
registryDependencies: ["otp-input"],
component: React.lazy(() => import("@/registry/new-york/example/otp-input-controlled")),
files: ["registry/new-york/example/otp-input-controlled.tsx"],
},
"otp-input-on-paste": {
name: "otp-input-on-paste",
type: "components:example",
registryDependencies: ["otp-input"],
component: React.lazy(() => import("@/registry/new-york/example/otp-input-on-paste")),
files: ["registry/new-york/example/otp-input-on-paste.tsx"],
},
"otp-input-form": {
name: "otp-input-form",
type: "components:example",
registryDependencies: ["otp-input"],
component: React.lazy(() => import("@/registry/new-york/example/otp-input-form")),
files: ["registry/new-york/example/otp-input-form.tsx"],
},
"otp-input-custom-style": {
name: "otp-input-custom-style",
type: "components:example",
registryDependencies: ["otp-input"],
component: React.lazy(() => import("@/registry/new-york/example/otp-input-custom-style")),
files: ["registry/new-york/example/otp-input-custom-style.tsx"],
},
"pagination-demo": {
name: "pagination-demo",
type: "components:example",
Expand Down
6 changes: 6 additions & 0 deletions apps/www/config/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,12 @@ export const docsConfig: DocsConfig = {
href: "/docs/components/navigation-menu",
items: [],
},
{
title: "OTP Input",
href: "/docs/components/otp-input",
items: [],
label: "New",
},
{
title: "Pagination",
href: "/docs/components/pagination",
Expand Down
182 changes: 182 additions & 0 deletions apps/www/content/docs/components/otp-input.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
---
title: OTP Input
description: Accessible one-time password component with copy paste functionality
component: true
---

<ComponentPreview name="otp-input-demo" />

## About

OTP Input is inspired by the [react-otp-input](https://www.npmjs.com/package/react-otp-input) and [input-otp](https://input-otp.rodz.dev/) libraries to build a simple, accessible and customizable component by composing Shadcn base components.

## Installation

<Tabs defaultValue="cli">

<TabsList>
<TabsTrigger value="cli">CLI</TabsTrigger>
<TabsTrigger value="manual">Manual</TabsTrigger>
</TabsList>
<TabsContent value="cli">

```bash
npx shadcn-ui@latest add otp-input
```

</TabsContent>

<TabsContent value="manual">

<Steps>

<Step>Copy and paste the following code into your project.</Step>

<ComponentSource name="otp-input" />

<Step>Update the import paths to match your project setup.</Step>

</Steps>

</TabsContent>

</Tabs>

## Usage

```tsx
import { OTPInput } from "@/components/ui/otp-input"
```

```tsx
<OTPInput />
```

## Examples

### Basic Usage

<ComponentPreview name="otp-input-demo" />

### With custom number inputs

You can customize the number of inputs by passing the `numInputs` prop.

<ComponentPreview name="otp-input-custom-number" />

### With custom placeholder

By default the placeholder is `_`. You can customize it by passing the `placeholder` prop.

<ComponentPreview name="otp-input-placeholder" />

### With separator

Sometimes you may need to have a separator between each generated input. For this case we have the `renderSeparator` prop.

<ComponentPreview name="otp-input-separator" />

<Callout>
The `renderSeparator` prop takes the index of the current input as an optional
argument. This can be useful if you need a separator in a certain place.
</Callout>

### Controlling the value

You can control the value of the OTP input by passing the `value` and `onChange` props.

<ComponentPreview name="otp-input-controlled" />

### Detecting onPaste event

You can detect the `onPaste` event by passing the `onPaste` prop.

<ComponentPreview name="otp-input-on-paste" />

### Auto focus

You can set the `autoFocus` prop to `true` to automatically focus the first input.

<Callout>
For practical purposes, we did not generate a live example for this case to
avoid scrolling the page to this point because of the `autoFocus`
</Callout>

#### Last Input Focused

You can set the `lastInputFocused` prop to `true` to automatically focus the last input. This can be useful if by default the OTP value is already complete and we want the component to have the focus on the last input so that the user can easily modify the value.

```tsx
import { OTPInput } from "@/components/ui/otp-input"

export default function OtpInputDemo() {
return <OTPInput autoFocus />
}
```

### With Form

You can use the OTPInput component with the Form component.

<ComponentPreview name="otp-input-form" />

### With Server Actions

The component renders by default this line of code

```tsx
<input type="hidden" id={id} name={name} value={otpValue} />
```

This allows to have a hidden input for the form you use with server actions and you can capture it inside the FormData. It is important that you set the `name` prop to be included inside the FormData.

### Custom styles

You can customize the styles of the OTP input by passing the `styles` prop. The `styles` prop is an object that accepts the following keys:

- `container` - The container of the OTP input.
- `input` - The input of the OTP input. It can be a string or a function that accepts the index of the input.

<ComponentPreview name="otp-input-custom-style" />

## API

```tsx
type AllowedInputTypes = "password" | "text" | "number" | "tel"

interface OTPInputProps
extends Pick<
React.InputHTMLAttributes<HTMLInputElement>,
"pattern" | "autoFocus" | "id" | "name"
> {
/** Value of the OTP input */
value?: string
/** Callback to be called when the OTP value changes */
onChange?: (otp: string) => void
/** Callback to be called when pasting content into the component */
onPaste?: (event: React.ClipboardEvent<HTMLDivElement>) => void
/** Callback to be called when the input is focused */
onFocus?: (event: React.FocusEvent<HTMLInputElement>) => void
/** Callback to be called when the input is blurred */
onBlur?: (event: React.FocusEvent<HTMLInputElement>) => void
/** Callback to be called when a key is pressed */
onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void
/** Callback to be called when the input value changes */
onInput?: (event: React.FormEvent<HTMLInputElement>) => void
/** Number of OTP inputs to be rendered */
numInputs?: number
/** Placeholder for the inputs */
placeholder?: string
/** Type of the input */
type?: AllowedInputTypes
/** Function to render the separator */
renderSeparator?: ((index: number) => React.ReactNode) | React.ReactNode
/** Additional styles for the component */
styles?: {
container?: string
input?: string | ((index: number) => React.ReactNode)
}
/** Focus the last input */
lastInputFocused?: boolean
}
```
7 changes: 7 additions & 0 deletions apps/www/public/registry/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,13 @@
],
"type": "components:ui"
},
{
"name": "otp-input",
"files": [
"ui/otp-input.tsx"
],
"type": "components:ui"
},
{
"name": "pagination",
"registryDependencies": [
Expand Down
Loading
Loading