410 lines
11 KiB
Markdown
410 lines
11 KiB
Markdown
---
|
|
title: OtpInput
|
|
description: One-time password input component with multiple fields, auto-focus, and paste handling.
|
|
---
|
|
|
|
# OtpInput
|
|
|
|
A specialized input component for one-time passwords (OTP) that displays multiple input fields in a grid layout. Perfect for SMS verification codes, authenticator app codes, and other numeric verification scenarios.
|
|
|
|
## Import
|
|
|
|
```rust
|
|
use gpui_component::input::{OtpInput, OtpState};
|
|
```
|
|
|
|
## Usage
|
|
|
|
### Basic OTP Input
|
|
|
|
```rust
|
|
let otp_state = cx.new(|cx| OtpState::new(6, window, cx));
|
|
|
|
OtpInput::new(&otp_state)
|
|
```
|
|
|
|
### With Default Value
|
|
|
|
```rust
|
|
let otp_state = cx.new(|cx|
|
|
OtpState::new(6, window, cx)
|
|
.default_value("123456")
|
|
);
|
|
|
|
OtpInput::new(&otp_state)
|
|
```
|
|
|
|
### Masked OTP Input
|
|
|
|
```rust
|
|
let otp_state = cx.new(|cx|
|
|
OtpState::new(6, window, cx)
|
|
.masked(true)
|
|
.default_value("123456")
|
|
);
|
|
|
|
OtpInput::new(&otp_state)
|
|
```
|
|
|
|
### Different Sizes
|
|
|
|
```rust
|
|
// Small size
|
|
OtpInput::new(&otp_state).small()
|
|
|
|
// Medium size (default)
|
|
OtpInput::new(&otp_state)
|
|
|
|
// Large size
|
|
OtpInput::new(&otp_state).large()
|
|
|
|
// Custom size
|
|
OtpInput::new(&otp_state).with_size(px(55.))
|
|
```
|
|
|
|
### Grouped Layout
|
|
|
|
```rust
|
|
// Single group (all fields together)
|
|
OtpInput::new(&otp_state).groups(1)
|
|
|
|
// Two groups (default) - splits fields in half
|
|
OtpInput::new(&otp_state).groups(2)
|
|
|
|
// Three groups - splits fields into thirds
|
|
OtpInput::new(&otp_state).groups(3)
|
|
```
|
|
|
|
### Disabled State
|
|
|
|
```rust
|
|
OtpInput::new(&otp_state).disabled(true)
|
|
```
|
|
|
|
### Different Length Codes
|
|
|
|
```rust
|
|
// 4-digit PIN
|
|
let pin_state = cx.new(|cx| OtpState::new(4, window, cx));
|
|
OtpInput::new(&pin_state).groups(1)
|
|
|
|
// 6-digit SMS code (most common)
|
|
let sms_state = cx.new(|cx| OtpState::new(6, window, cx));
|
|
OtpInput::new(&sms_state)
|
|
|
|
// 8-digit authenticator code
|
|
let auth_state = cx.new(|cx| OtpState::new(8, window, cx));
|
|
OtpInput::new(&auth_state).groups(2)
|
|
```
|
|
|
|
### Handle OTP Events
|
|
|
|
```rust
|
|
let otp_state = cx.new(|cx| OtpState::new(6, window, cx));
|
|
|
|
cx.subscribe(&otp_state, |this, state, event: &InputEvent, cx| {
|
|
match event {
|
|
InputEvent::Change => {
|
|
let code = state.read(cx).value();
|
|
if code.len() == 6 {
|
|
println!("Complete OTP: {}", code);
|
|
// Automatically submit when complete
|
|
this.verify_otp(&code, cx);
|
|
}
|
|
}
|
|
InputEvent::Focus => println!("OTP input focused"),
|
|
InputEvent::Blur => println!("OTP input lost focus"),
|
|
_ => {}
|
|
}
|
|
});
|
|
```
|
|
|
|
### Programmatic Control
|
|
|
|
```rust
|
|
// Set value programmatically
|
|
otp_state.update(cx, |state, cx| {
|
|
state.set_value("123456", window, cx);
|
|
});
|
|
|
|
// Toggle masking
|
|
otp_state.update(cx, |state, cx| {
|
|
state.set_masked(true, window, cx);
|
|
});
|
|
|
|
// Focus the input
|
|
otp_state.update(cx, |state, cx| {
|
|
state.focus(window, cx);
|
|
});
|
|
|
|
// Get current value
|
|
let current_value = otp_state.read(cx).value();
|
|
```
|
|
|
|
## API Reference
|
|
|
|
### OtpState
|
|
|
|
| Method | Description |
|
|
| ------------------------------ | -------------------------------------------- |
|
|
| `new(length, window, cx)` | Create a new OTP state with specified length |
|
|
| `default_value(str)` | Set initial value |
|
|
| `masked(bool)` | Enable masked display (shows asterisks) |
|
|
| `set_value(str, window, cx)` | Set OTP value programmatically |
|
|
| `value()` | Get current OTP value |
|
|
| `set_masked(bool, window, cx)` | Toggle masked display |
|
|
| `focus(window, cx)` | Focus the OTP input |
|
|
| `focus_handle(cx)` | Get focus handle |
|
|
|
|
### OtpInput
|
|
|
|
| Method | Description |
|
|
| ---------------- | ---------------------------------------- |
|
|
| `new(state)` | Create OTP input with state entity |
|
|
| `groups(n)` | Set number of visual groups (default: 2) |
|
|
| `disabled(bool)` | Set disabled state |
|
|
| `small()` | Small size (6x6 px fields) |
|
|
| `large()` | Large size (11x11 px fields) |
|
|
| `with_size(px)` | Custom field size |
|
|
|
|
### InputEvent
|
|
|
|
| Event | Description |
|
|
| -------- | ------------------------------------------------- |
|
|
| `Change` | Emitted when OTP is complete (all digits entered) |
|
|
| `Focus` | Input received focus |
|
|
| `Blur` | Input lost focus |
|
|
|
|
## Examples
|
|
|
|
### SMS Verification
|
|
|
|
```rust
|
|
struct SmsVerification {
|
|
otp_state: Entity<OtpState>,
|
|
phone_number: String,
|
|
is_verifying: bool,
|
|
}
|
|
|
|
impl SmsVerification {
|
|
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
|
let otp_state = cx.new(|cx| OtpState::new(6, window, cx));
|
|
|
|
cx.subscribe(&otp_state, |this, state, event: &InputEvent, cx| {
|
|
if let InputEvent::Change = event {
|
|
let code = state.read(cx).value();
|
|
this.verify_sms_code(&code, cx);
|
|
}
|
|
});
|
|
|
|
Self {
|
|
otp_state,
|
|
phone_number: "+1234567890".to_string(),
|
|
is_verifying: false,
|
|
}
|
|
}
|
|
|
|
fn verify_sms_code(&mut self, code: &str, cx: &mut Context<Self>) {
|
|
self.is_verifying = true;
|
|
// API call to verify SMS code
|
|
println!("Verifying SMS code: {}", code);
|
|
cx.notify();
|
|
}
|
|
}
|
|
|
|
impl Render for SmsVerification {
|
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
v_flex()
|
|
.gap_4()
|
|
.child(format!("Enter the 6-digit code sent to {}", self.phone_number))
|
|
.child(OtpInput::new(&self.otp_state))
|
|
.when(self.is_verifying, |this| {
|
|
this.child("Verifying...")
|
|
})
|
|
}
|
|
}
|
|
```
|
|
|
|
### Two-Factor Authentication
|
|
|
|
```rust
|
|
struct TwoFactorAuth {
|
|
otp_state: Entity<OtpState>,
|
|
is_masked: bool,
|
|
}
|
|
|
|
impl TwoFactorAuth {
|
|
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
|
let otp_state = cx.new(|cx|
|
|
OtpState::new(6, window, cx)
|
|
.masked(true)
|
|
);
|
|
|
|
Self {
|
|
otp_state,
|
|
is_masked: true,
|
|
}
|
|
}
|
|
|
|
fn toggle_visibility(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
self.is_masked = !self.is_masked;
|
|
self.otp_state.update(cx, |state, cx| {
|
|
state.set_masked(self.is_masked, window, cx);
|
|
});
|
|
}
|
|
}
|
|
|
|
impl Render for TwoFactorAuth {
|
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
v_flex()
|
|
.gap_4()
|
|
.child("Enter your authenticator code")
|
|
.child(OtpInput::new(&self.otp_state))
|
|
.child(
|
|
Button::new("toggle-visibility")
|
|
.label(if self.is_masked { "Show" } else { "Hide" })
|
|
.on_click(cx.listener(Self::toggle_visibility))
|
|
)
|
|
}
|
|
}
|
|
```
|
|
|
|
### PIN Entry
|
|
|
|
```rust
|
|
struct PinEntry {
|
|
pin_state: Entity<OtpState>,
|
|
attempts: usize,
|
|
max_attempts: usize,
|
|
}
|
|
|
|
impl PinEntry {
|
|
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
|
let pin_state = cx.new(|cx|
|
|
OtpState::new(4, window, cx)
|
|
.masked(true)
|
|
);
|
|
|
|
cx.subscribe(&pin_state, |this, state, event: &InputEvent, cx| {
|
|
if let InputEvent::Change = event {
|
|
let pin = state.read(cx).value();
|
|
this.verify_pin(&pin, cx);
|
|
}
|
|
});
|
|
|
|
Self {
|
|
pin_state,
|
|
attempts: 0,
|
|
max_attempts: 3,
|
|
}
|
|
}
|
|
|
|
fn verify_pin(&mut self, pin: &str, cx: &mut Context<Self>) {
|
|
self.attempts += 1;
|
|
|
|
// Simulate PIN verification
|
|
if pin == "1234" {
|
|
println!("PIN verified successfully!");
|
|
} else {
|
|
println!("Incorrect PIN. Attempts: {}/{}", self.attempts, self.max_attempts);
|
|
|
|
// Clear PIN on incorrect attempt
|
|
self.pin_state.update(cx, |state, cx| {
|
|
state.set_value("", window, cx);
|
|
});
|
|
}
|
|
|
|
cx.notify();
|
|
}
|
|
}
|
|
|
|
impl Render for PinEntry {
|
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let is_locked = self.attempts >= self.max_attempts;
|
|
|
|
v_flex()
|
|
.gap_4()
|
|
.child("Enter your 4-digit PIN")
|
|
.child(
|
|
OtpInput::new(&self.pin_state)
|
|
.groups(1)
|
|
.disabled(is_locked)
|
|
)
|
|
.when(is_locked, |this| {
|
|
this.child("Too many attempts. Please try again later.")
|
|
})
|
|
.when(self.attempts > 0 && !is_locked, |this| {
|
|
this.child(format!(
|
|
"Incorrect PIN. {} attempts remaining.",
|
|
self.max_attempts - self.attempts
|
|
))
|
|
})
|
|
}
|
|
}
|
|
```
|
|
|
|
## Behavior
|
|
|
|
### Input Handling
|
|
|
|
- **Numeric Only**: Accepts only digits (0-9)
|
|
- **Auto-Focus**: Automatically moves to next field when digit is entered
|
|
- **Backspace**: Removes current digit and moves to previous field
|
|
- **Length Limit**: Prevents input beyond specified length
|
|
- **Auto-Complete**: Emits `Change` event when all fields are filled
|
|
|
|
### Visual Feedback
|
|
|
|
- **Focus Indicator**: Blue border and blinking cursor on active field
|
|
- **Masking**: Shows asterisk icons instead of numbers when enabled
|
|
- **Grouping**: Visual separation of fields into groups for better readability
|
|
- **Disabled State**: Grayed out appearance when disabled
|
|
|
|
### Keyboard Navigation
|
|
|
|
- **Arrow Keys**: Navigate between fields
|
|
- **Tab**: Move to next focusable element
|
|
- **Shift+Tab**: Move to previous focusable element
|
|
- **Backspace**: Delete current digit and move backward
|
|
- **Delete**: Clear current field
|
|
|
|
## Common Patterns
|
|
|
|
### Auto-Submit on Complete
|
|
|
|
```rust
|
|
cx.subscribe(&otp_state, |this, state, event: &InputEvent, cx| {
|
|
if let InputEvent::Change = event {
|
|
let code = state.read(cx).value();
|
|
if code.len() == 6 {
|
|
// Auto-submit when complete
|
|
this.submit_verification_code(&code, cx);
|
|
}
|
|
}
|
|
});
|
|
```
|
|
|
|
### Clear on Focus
|
|
|
|
```rust
|
|
cx.subscribe(&otp_state, |this, state, event: &InputEvent, cx| {
|
|
if let InputEvent::Focus = event {
|
|
// Clear previous value when user starts entering new code
|
|
state.update(cx, |state, cx| {
|
|
state.set_value("", window, cx);
|
|
});
|
|
}
|
|
});
|
|
```
|
|
|
|
### Resend Code Timer
|
|
|
|
```rust
|
|
struct OtpWithResend {
|
|
otp_state: Entity<OtpState>,
|
|
resend_timer: Option<Timer>,
|
|
can_resend: bool,
|
|
}
|
|
|
|
// Implementation would include timer logic for resend functionality
|
|
```
|