Skip to content

Commit

Permalink
feat: switch input-otp to composition (#3052)
Browse files Browse the repository at this point in the history
* feat: switch input-otp to composition

* feat: add disabled
  • Loading branch information
shadcn authored Mar 19, 2024
1 parent 5ec881d commit f199dd3
Show file tree
Hide file tree
Showing 18 changed files with 265 additions and 183 deletions.
150 changes: 109 additions & 41 deletions apps/www/content/docs/components/input-otp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -114,24 +114,19 @@ import {
```

```tsx
<InputOTP
maxLength={6}
render={({ slots }) => (
<>
<InputOTPGroup>
{slots.slice(0, 3).map((slot, index) => (
<InputOTPSlot key={index} {...slot} />
))}{" "}
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
{slots.slice(3).map((slot, index) => (
<InputOTPSlot key={index} {...slot} />
))}
</InputOTPGroup>
</>
)}
/>
<InputOTP maxLength={6}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
```

## Examples
Expand All @@ -150,14 +145,12 @@ import { REGEXP_ONLY_DIGITS_AND_CHARS } from "input-otp"
<InputOTP
maxLength={6}
pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
render={({ slots }) => (
<InputOTPGroup>
{slots.map((slot, index) => (
<InputOTPSlot key={index} {...slot} />
))}{" "}
</InputOTPGroup>
)}
/>
>
<InputOTPGroup>
<InputOTPSlot index={0} />
{/* ... */}
</InputOTPGroup>
</InputOTP>
```

### Separator
Expand All @@ -166,29 +159,27 @@ You can use the `<InputOTPSeparator />` component to add a separator between the

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

```tsx showLineNumbers {4,17}
```tsx showLineNumbers {4,15}
import {
InputOTP,
InputOTPGroup,
InputOTPSeparator,
InputOTPSlot,
} from "@/registry/new-york/ui/input-otp"
} from "@/components/ui/input-otp"

...

<InputOTP
maxLength={6}
render={({ slots }) => (
<InputOTPGroup className="gap-2">
{slots.map((slot, index) => (
<React.Fragment key={index}>
<InputOTPSlot className="rounded-md border" {...slot} />
{index !== slots.length - 1 && <InputOTPSeparator />}
</React.Fragment>
))}{" "}
</InputOTPGroup>
)}
/>
<InputOTP maxLength={4}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
</InputOTPGroup>
</InputOTP>
```

### Controlled
Expand All @@ -200,3 +191,80 @@ You can use the `value` and `onChange` props to control the input value.
### Form

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

## Changelog

### 2024-03-19 Composition

We've made some updates and replaced the render props pattern with composition. Here's how to update your code if you prefer the composition pattern.

<Callout className="mt-6">
**Note:** You are not required to update your code if you are using the
`render` prop. It is still supported.
</Callout>

<Steps>

<Step>Update to the latest version of `input-otp`.</Step>

```bash
npm install input-otp@latest
```

<Step>Update `input-otp.tsx`</Step>

```diff showLineNumbers title="input-otp.tsx" {2,8-11}
- import { OTPInput, SlotProps } from "input-otp"
+ import { OTPInput, OTPInputContext } from "input-otp"

const InputOTPSlot = React.forwardRef<
React.ElementRef<"div">,
- SlotProps & React.ComponentPropsWithoutRef<"div">
- >(({ char, hasFakeCaret, isActive, className, ...props }, ref) => {
+ React.ComponentPropsWithoutRef<"div"> & { index: number }
+ >(({ index, className, ...props }, ref) => {
+ const inputOTPContext = React.useContext(OTPInputContext)
+ const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]
```

<Step>Then replace the `render` prop in your code.</Step>

```diff showLineNumbers {2-12}
<InputOTP maxLength={6}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
```

</Steps>

### 2024-03-19 Disabled

To add a disabled state to the input, update `<InputOTP />` as follows:

```tsx showLineNumbers title="input-otp.tsx" {4,7-11}
const InputOTP = React.forwardRef<
React.ElementRef<typeof OTPInput>,
React.ComponentPropsWithoutRef<typeof OTPInput>
>(({ className, containerClassName, ...props }, ref) => (
<OTPInput
ref={ref}
containerClassName={cn(
"flex items-center gap-2 has-[:disabled]:opacity-50",
containerClassName
)}
className={cn("disabled:cursor-not-allowed", className)}
{...props}
/>
))
InputOTP.displayName = "InputOTP"
```
2 changes: 1 addition & 1 deletion apps/www/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
"embla-carousel-autoplay": "8.0.0-rc15",
"embla-carousel-react": "8.0.0-rc15",
"geist": "^1.1.0",
"input-otp": "^1.0.1",
"input-otp": "^1.2.2",
"jotai": "^2.1.0",
"lodash.template": "^4.5.0",
"lucide-react": "0.288.0",
Expand Down
2 changes: 1 addition & 1 deletion apps/www/public/registry/styles/default/input-otp.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"files": [
{
"name": "input-otp.tsx",
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { OTPInput, SlotProps } from \"input-otp\"\nimport { Dot } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst InputOTP = React.forwardRef<\n React.ElementRef<typeof OTPInput>,\n React.ComponentPropsWithoutRef<typeof OTPInput>\n>(({ className, ...props }, ref) => (\n <OTPInput\n ref={ref}\n containerClassName={cn(\"flex items-center gap-2\", className)}\n {...props}\n />\n))\nInputOTP.displayName = \"InputOTP\"\n\nconst InputOTPGroup = React.forwardRef<\n React.ElementRef<\"div\">,\n React.ComponentPropsWithoutRef<\"div\">\n>(({ className, ...props }, ref) => (\n <div ref={ref} className={cn(\"flex items-center\", className)} {...props} />\n))\nInputOTPGroup.displayName = \"InputOTPGroup\"\n\nconst InputOTPSlot = React.forwardRef<\n React.ElementRef<\"div\">,\n SlotProps & React.ComponentPropsWithoutRef<\"div\">\n>(({ char, hasFakeCaret, isActive, className, ...props }, ref) => {\n return (\n <div\n ref={ref}\n className={cn(\n \"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md\",\n isActive && \"z-10 ring-2 ring-offset-background ring-ring\",\n className\n )}\n {...props}\n >\n {char}\n {hasFakeCaret && (\n <div className=\"pointer-events-none absolute inset-0 flex items-center justify-center\">\n <div className=\"animate-caret-blink h-4 w-px bg-foreground duration-1000\" />\n </div>\n )}\n </div>\n )\n})\nInputOTPSlot.displayName = \"InputOTPSlot\"\n\nconst InputOTPSeparator = React.forwardRef<\n React.ElementRef<\"div\">,\n React.ComponentPropsWithoutRef<\"div\">\n>(({ ...props }, ref) => (\n <div ref={ref} role=\"separator\" {...props}>\n <Dot />\n </div>\n))\nInputOTPSeparator.displayName = \"InputOTPSeparator\"\n\nexport { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }\n"
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { OTPInput, OTPInputContext } from \"input-otp\"\nimport { Dot } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst InputOTP = React.forwardRef<\n React.ElementRef<typeof OTPInput>,\n React.ComponentPropsWithoutRef<typeof OTPInput>\n>(({ className, containerClassName, ...props }, ref) => (\n <OTPInput\n ref={ref}\n containerClassName={cn(\n \"flex items-center gap-2 has-[:disabled]:opacity-50\",\n containerClassName\n )}\n className={cn(\"disabled:cursor-not-allowed\", className)}\n {...props}\n />\n))\nInputOTP.displayName = \"InputOTP\"\n\nconst InputOTPGroup = React.forwardRef<\n React.ElementRef<\"div\">,\n React.ComponentPropsWithoutRef<\"div\">\n>(({ className, ...props }, ref) => (\n <div ref={ref} className={cn(\"flex items-center\", className)} {...props} />\n))\nInputOTPGroup.displayName = \"InputOTPGroup\"\n\nconst InputOTPSlot = React.forwardRef<\n React.ElementRef<\"div\">,\n React.ComponentPropsWithoutRef<\"div\"> & { index: number }\n>(({ index, className, ...props }, ref) => {\n const inputOTPContext = React.useContext(OTPInputContext)\n const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]\n\n return (\n <div\n ref={ref}\n className={cn(\n \"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md\",\n isActive && \"z-10 ring-2 ring-ring ring-offset-background\",\n className\n )}\n {...props}\n >\n {char}\n {hasFakeCaret && (\n <div className=\"pointer-events-none absolute inset-0 flex items-center justify-center\">\n <div className=\"h-4 w-px animate-caret-blink bg-foreground duration-1000\" />\n </div>\n )}\n </div>\n )\n})\nInputOTPSlot.displayName = \"InputOTPSlot\"\n\nconst InputOTPSeparator = React.forwardRef<\n React.ElementRef<\"div\">,\n React.ComponentPropsWithoutRef<\"div\">\n>(({ ...props }, ref) => (\n <div ref={ref} role=\"separator\" {...props}>\n <Dot />\n </div>\n))\nInputOTPSeparator.displayName = \"InputOTPSeparator\"\n\nexport { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }\n"
}
],
"type": "components:ui"
Expand Down
2 changes: 1 addition & 1 deletion apps/www/public/registry/styles/new-york/input-otp.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"files": [
{
"name": "input-otp.tsx",
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { DashIcon } from \"@radix-ui/react-icons\"\nimport { OTPInput, SlotProps } from \"input-otp\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst InputOTP = React.forwardRef<\n React.ElementRef<typeof OTPInput>,\n React.ComponentPropsWithoutRef<typeof OTPInput>\n>(({ className, ...props }, ref) => (\n <OTPInput\n ref={ref}\n containerClassName={cn(\"flex items-center gap-2\", className)}\n {...props}\n />\n))\nInputOTP.displayName = \"InputOTP\"\n\nconst InputOTPGroup = React.forwardRef<\n React.ElementRef<\"div\">,\n React.ComponentPropsWithoutRef<\"div\">\n>(({ className, ...props }, ref) => (\n <div ref={ref} className={cn(\"flex items-center\", className)} {...props} />\n))\nInputOTPGroup.displayName = \"InputOTPGroup\"\n\nconst InputOTPSlot = React.forwardRef<\n React.ElementRef<\"div\">,\n SlotProps & React.ComponentPropsWithoutRef<\"div\">\n>(({ char, hasFakeCaret, isActive, className, ...props }, ref) => {\n return (\n <div\n ref={ref}\n className={cn(\n \"relative flex h-9 w-9 items-center justify-center border-y border-r border-input text-sm shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md\",\n isActive && \"z-10 ring-1 ring-ring\",\n className\n )}\n {...props}\n >\n {char}\n {hasFakeCaret && (\n <div className=\"pointer-events-none absolute inset-0 flex items-center justify-center\">\n <div className=\"animate-caret-blink h-4 w-px bg-foreground duration-1000\" />\n </div>\n )}\n </div>\n )\n})\nInputOTPSlot.displayName = \"InputOTPSlot\"\n\nconst InputOTPSeparator = React.forwardRef<\n React.ElementRef<\"div\">,\n React.ComponentPropsWithoutRef<\"div\">\n>(({ ...props }, ref) => (\n <div ref={ref} role=\"separator\" {...props}>\n <DashIcon />\n </div>\n))\nInputOTPSeparator.displayName = \"InputOTPSeparator\"\n\nexport { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }\n"
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { DashIcon } from \"@radix-ui/react-icons\"\nimport { OTPInput, OTPInputContext } from \"input-otp\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst InputOTP = React.forwardRef<\n React.ElementRef<typeof OTPInput>,\n React.ComponentPropsWithoutRef<typeof OTPInput>\n>(({ className, containerClassName, ...props }, ref) => (\n <OTPInput\n ref={ref}\n containerClassName={cn(\n \"flex items-center gap-2 has-[:disabled]:opacity-50\",\n containerClassName\n )}\n className={cn(\"disabled:cursor-not-allowed\", className)}\n {...props}\n />\n))\nInputOTP.displayName = \"InputOTP\"\n\nconst InputOTPGroup = React.forwardRef<\n React.ElementRef<\"div\">,\n React.ComponentPropsWithoutRef<\"div\">\n>(({ className, ...props }, ref) => (\n <div ref={ref} className={cn(\"flex items-center\", className)} {...props} />\n))\nInputOTPGroup.displayName = \"InputOTPGroup\"\n\nconst InputOTPSlot = React.forwardRef<\n React.ElementRef<\"div\">,\n React.ComponentPropsWithoutRef<\"div\"> & { index: number }\n>(({ index, className, ...props }, ref) => {\n const inputOTPContext = React.useContext(OTPInputContext)\n const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index]\n\n return (\n <div\n ref={ref}\n className={cn(\n \"relative flex h-9 w-9 items-center justify-center border-y border-r border-input text-sm shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md\",\n isActive && \"z-10 ring-1 ring-ring\",\n className\n )}\n {...props}\n >\n {char}\n {hasFakeCaret && (\n <div className=\"pointer-events-none absolute inset-0 flex items-center justify-center\">\n <div className=\"h-4 w-px animate-caret-blink bg-foreground duration-1000\" />\n </div>\n )}\n </div>\n )\n})\nInputOTPSlot.displayName = \"InputOTPSlot\"\n\nconst InputOTPSeparator = React.forwardRef<\n React.ElementRef<\"div\">,\n React.ComponentPropsWithoutRef<\"div\">\n>(({ ...props }, ref) => (\n <div ref={ref} role=\"separator\" {...props}>\n <DashIcon />\n </div>\n))\nInputOTPSeparator.displayName = \"InputOTPSeparator\"\n\nexport { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }\n"
}
],
"type": "components:ui"
Expand Down
18 changes: 10 additions & 8 deletions apps/www/registry/default/example/input-otp-controlled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,16 @@ export default function InputOTPControlled() {
maxLength={6}
value={value}
onChange={(value) => setValue(value)}
render={({ slots }) => (
<InputOTPGroup>
{slots.map((slot, index) => (
<InputOTPSlot key={index} {...slot} />
))}{" "}
</InputOTPGroup>
)}
/>
>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
<div className="text-center text-sm">
{value === "" ? (
<>Enter your one-time password.</>
Expand Down
31 changes: 13 additions & 18 deletions apps/www/registry/default/example/input-otp-demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,18 @@ import {

export default function InputOTPDemo() {
return (
<InputOTP
maxLength={6}
render={({ slots }) => (
<>
<InputOTPGroup>
{slots.slice(0, 3).map((slot, index) => (
<InputOTPSlot key={index} {...slot} />
))}{" "}
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
{slots.slice(3).map((slot, index) => (
<InputOTPSlot key={index + 3} {...slot} />
))}
</InputOTPGroup>
</>
)}
/>
<InputOTP maxLength={6}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
)
}
21 changes: 10 additions & 11 deletions apps/www/registry/default/example/input-otp-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,16 @@ export default function InputOTPForm() {
<FormItem>
<FormLabel>One-Time Password</FormLabel>
<FormControl>
<InputOTP
maxLength={6}
render={({ slots }) => (
<InputOTPGroup>
{slots.map((slot, index) => (
<InputOTPSlot key={index} {...slot} />
))}{" "}
</InputOTPGroup>
)}
{...field}
/>
<InputOTP maxLength={6} {...field}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
</FormControl>
<FormDescription>
Please enter the one-time password sent to your phone.
Expand Down
21 changes: 10 additions & 11 deletions apps/www/registry/default/example/input-otp-pattern.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,15 @@ import {

export default function InputOTPPattern() {
return (
<InputOTP
maxLength={6}
pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
render={({ slots }) => (
<InputOTPGroup>
{slots.map((slot, index) => (
<InputOTPSlot key={index} {...slot} />
))}{" "}
</InputOTPGroup>
)}
/>
<InputOTP maxLength={6} pattern={REGEXP_ONLY_DIGITS_AND_CHARS}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
)
}
29 changes: 16 additions & 13 deletions apps/www/registry/default/example/input-otp-separator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,21 @@ import {

export default function InputOTPWithSeparator() {
return (
<InputOTP
maxLength={6}
render={({ slots }) => (
<InputOTPGroup className="gap-2">
{slots.map((slot, index) => (
<React.Fragment key={index}>
<InputOTPSlot className="rounded-md border" {...slot} />
{index !== slots.length - 1 && <InputOTPSeparator />}
</React.Fragment>
))}{" "}
</InputOTPGroup>
)}
/>
<InputOTP maxLength={6}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
)
}
Loading

0 comments on commit f199dd3

Please sign in to comment.