Skip to content

Commit

Permalink
Merge branch 'master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
guilhermerodz committed Mar 13, 2024
2 parents 7abeead + 4917dce commit 20b33f2
Show file tree
Hide file tree
Showing 15 changed files with 511 additions and 153 deletions.
179 changes: 174 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# OTP Input for React
# The only accessible & unstyled & full featured Input OTP component in the Web.

### OTP Input for React 🔐 by [@guilhermerodz](https://twitter.com/guilherme_rodz)

https://github.com/guilhermerodz/input-otp/assets/10366880/753751f5-eda8-4145-a4b9-7ef51ca5e453

Expand Down Expand Up @@ -123,6 +125,8 @@ This library works by rendering an invisible input as a sibling of the slots, co

## Features

This is the most complete OTP input on the web. It's fully featured

<details>
<summary>Supports iOS + Android copy-paste-cut</summary>

Expand Down Expand Up @@ -165,6 +169,27 @@ https://github.com/guilhermerodz/input-otp/assets/10366880/185985c0-af64-48eb-92

</details>

<details>
<summary>Automatically optimizes for password managers</summary>


For password managers such as LastPass, 1Password, Dashlane or Bitwarden, `input-otp` will automatically detect them in the page and increase input width by ~40px to trick the password manager's browser extension and prevent the badge from rendering to the last/right slot of the input.

<img width="670" alt="image" src="https://github.com/guilhermerodz/input-otp/assets/10366880/9bb306ca-deff-4803-aa3d-148c594a540c">

- **This feature is optional and it's enabled by default. You can disable this optimization by adding `pushPasswordManagerStrategy="none"`.**
- **This feature does not cause visible layout shift.**

### Auto tracks if the input has space in the right side for the badge

https://github.com/guilhermerodz/input-otp/assets/10366880/bf01af88-1f82-463e-adf4-54a737a92f59

### Experimental flag

Try `pushPasswordManagerStrategy={experimental-no-flickering}` to initially render the input with extra width so that password manager users won't see their badges flickering.
After ~6 seconds _onfocus_, the input will return to it's original width if no password manager.
</details>

## API Reference

### OTPInput
Expand All @@ -180,6 +205,9 @@ type OTPInputProps = {

// Render function creating the slots
render: (props: RenderProps) => React.ReactElement
// PS: Render prop is mandatory, except in cases
// you'd like to consume the original Context API.
// (search for Context in this docs)

// The class name for the root container
containerClassName?: string
Expand All @@ -200,6 +228,47 @@ type OTPInputProps = {
// Virtual keyboard appearance on mobile
// Default: 'numeric'
inputMode?: 'numeric' | 'text'

// Enabled by default, it's an optional
// strategy for detecting Password Managers
// in the page and then shifting their
// badges to the right side, outside the input.
// There is also `experimental-no-flickering`
// (experimental flag use it on your risk)
// which won't cause badge flickering at all.
// Default: 'increase-width'
pushPasswordManagerStrategy?:
| 'increase-width'
| 'none'
| 'experimental-no-flickering'

// Enabled by default, it's an optional
// fallback for pages without JS.
// This is a CSS string. Write your own
// rules that will be applied as soon as
// <noscript> is parsed for no-js pages.
// Use `null` to disable any no-js fallback (not recommended).
// Default: `
// [data-input-otp] {
// --nojs-bg: white !important;
// --nojs-fg: black !important;
//
// background-color: var(--nojs-bg) !important;
// color: var(--nojs-fg) !important;
// caret-color: var(--nojs-fg) !important;
// letter-spacing: .25em !important;
// text-align: center !important;
// border: 1px solid var(--nojs-fg) !important;
// border-radius: 4px !important;
// width: 100% !important;
// }
// @media (prefers-color-scheme: dark) {
// [data-input-otp] {
// --nojs-bg: black !important;
// --nojs-fg: white !important;
// }
// }`
noScriptCSSFallback?: string | null
}
```
Expand Down Expand Up @@ -249,7 +318,69 @@ export default function Page() {
## Caveats

<details>
<summary>If you're using experiencing an unwanted border on input focus:</summary>
<summary>[Workaround] If you want to block specific password manager/badges:</summary>

By default, `input-otp` handles password managers for you.
The password manager badges should be automatically shifted to the right side.

However, if you still want to block password managers, please disable the `pushPasswordManagerStrategy` and then manually block each PWM.

```diff
<OTPInput
// First, disable library's built-in strategy
// for shifting badges automatically
- pushPasswordManagerStrategy="increase-width"
- pushPasswordManagerStrategy="experimental-no-flickering"
+ pushPasswordManagerStrategy="none"
// Then, manually add specifics attributes
// your password manager docs
// Example: block LastPass
+ data-lpignore="true"
// Example: block 1Password
+ data-1p-ignore="true"
/>
```
</details>

<details>
<summary>[Setting] If you want to customize the `noscript` CSS fallback</summary>

By default, `input-otp` handles cases where JS is not in the page by applying custom CSS styles.
If you do not like the fallback design and want to apply it to your own, just pass a prop:

```diff
// This is the default CSS fallback.
// Feel free to change it entirely and apply to your design system.
const NOSCRIPT_CSS_FALLBACK = `
[data-input-otp] {
--nojs-bg: white !important;
--nojs-fg: black !important;

background-color: var(--nojs-bg) !important;
color: var(--nojs-fg) !important;
caret-color: var(--nojs-fg) !important;
letter-spacing: .25em !important;
text-align: center !important;
border: 1px solid var(--nojs-fg) !important;
border-radius: 4px !important;
width: 100% !important;
}
@media (prefers-color-scheme: dark) {
[data-input-otp] {
--nojs-bg: black !important;
--nojs-fg: white !important;
}
}`

<OTPInput
// Pass your own custom styles for when JS is disabled
+ noScriptCSSFallback={NOSCRIPT_CSS_FALLBACK}
/>
```
</details>

<details>
<summary>[Workaround] If you're experiencing an unwanted border on input focus:</summary>

```diff
<OTPInput
Expand All @@ -262,7 +393,7 @@ export default function Page() {
</details>

<details>
<summary>If you want to centralize input text/selection, use the `textAlign` prop:</summary>
<summary>[Not Recommended] If you want to centralize input text/selection, use the `textAlign` prop:</summary>

```diff
<OTPInput
Expand All @@ -271,7 +402,45 @@ export default function Page() {
/>
```

NOTE: this also affects the selected caret position after a touch/click
NOTE: this also affects the selected caret position after a touch/click.

`textAlign="left"`
<img src="https://github.com/guilhermerodz/input-otp/assets/10366880/685a03df-2b69-4a36-b21c-e453f6098f79" width="300" />
<br>

`textAlign="center"`
<img src="https://github.com/guilhermerodz/input-otp/assets/10366880/e0f15b97-ceb8-40c8-96b7-fa3a8896379f" width="300" />
<br>

`textAlign="right"`
<img src="https://github.com/guilhermerodz/input-otp/assets/10366880/26697579-0e8b-4dad-8b85-3a036102e951" width="300" />
<br>

</details>

<details>
<summary>If you want to use Context props:</summary>

```diff
+import { OTPInputContext } from 'input-otp'

function MyForm() {
+ const inputContext = React.useContext(OTPInputContext)
return (
<OTPInput
- // First remove the `render` prop
- render={...}
>
+ {/* Then consume context */}
+ {inputContext.slots.map((slot, idx) => (
+ <Slot key={idx} {...slot} />
+ ))}
+ </OTPInput>
)
}
```

NOTE: this also affects the selected caret position after a touch/click.

`textAlign="left"`
<img src="https://github.com/guilhermerodz/input-otp/assets/10366880/685a03df-2b69-4a36-b21c-e453f6098f79" width="300" />
Expand All @@ -288,7 +457,7 @@ NOTE: this also affects the selected caret position after a touch/click
</details>

<details>
<summary>Add Tailwind autocomplete for `containerClassname` attribute in VS Code.</summary>
<summary>[DX] Add Tailwind autocomplete for `containerClassname` attribute in VS Code.</summary>

Add the following setting to your `.vscode/settings.json`:
```diff
Expand Down
3 changes: 1 addition & 2 deletions apps/test/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ export default defineConfig({
fullyParallel: process.env.WINDOWED_TESTS ? false : true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 1,
retries: 2,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { OTPInput, SlotProps } from 'input-otp'
import { cn } from '@/lib/utils'

export function ExampleComponent(
props: Partial<React.ComponentProps<typeof OTPInput>>,
props: Partial<Omit<React.ComponentProps<typeof OTPInput>, 'children'>>,
) {
return (
<OTPInput
Expand Down
22 changes: 19 additions & 3 deletions apps/website/src/app/(local-pages)/shadcn/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,23 @@ export default function ShadcnPage() {

return (
<form className="container relative flex-1 flex flex-col justify-center items-center">
<InputOTP
{/* With Context API */}
<InputOTP value={value} onChange={setValue} 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>

{/* Previously with render={() => {}} prop (still supported through official lib) */}
{/* <InputOTP
value={value}
onChange={setValue}
maxLength={6}
Expand All @@ -22,7 +38,7 @@ export default function ShadcnPage() {
<InputOTPGroup>
{slots.slice(0, 3).map((slot, index) => (
<InputOTPSlot key={index} {...slot} />
))}{' '}
))}
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
Expand All @@ -32,7 +48,7 @@ export default function ShadcnPage() {
</InputOTPGroup>
</>
)}
/>
/> */}
</form>
)
}
11 changes: 6 additions & 5 deletions apps/website/src/app/(local-pages)/shadcn/pwmb/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import {
InputOTP,
InputOTPGroup,
InputOTPRenderSlot,
InputOTPSeparator,
InputOTPSlot,
} from '@/components/ui/input-otp'
Expand All @@ -14,12 +15,12 @@ export default function ShadcnPage() {

return (
<form className="container relative flex-1 flex flex-col justify-center items-center">
<input
{/* <input
// test pwmb
type="text"
autoComplete="username webauthn"
pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
/>
/> */}
<InputOTP
autoFocus
// test pwmb
Expand All @@ -35,13 +36,13 @@ export default function ShadcnPage() {
<>
<InputOTPGroup>
{slots.slice(0, 3).map((slot, index) => (
<InputOTPSlot key={index} {...slot} />
))}{' '}
<InputOTPRenderSlot key={index} {...slot} />
))}
</InputOTPGroup>
<InputOTPSeparator />
<InputOTPGroup>
{slots.slice(3).map((slot, index) => (
<InputOTPSlot key={index} {...slot} />
<InputOTPRenderSlot key={index} {...slot} />
))}
</InputOTPGroup>
</>
Expand Down
9 changes: 9 additions & 0 deletions apps/website/src/app/(local-pages)/shadcn/static/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use server'

import { default as ShadcnPage } from '../page'

export default async function StaticPage(pageProps: any) {
return (
<ShadcnPage {...pageProps} />
)
}

0 comments on commit 20b33f2

Please sign in to comment.