feat(input-otp): add new input-otp component (#30386)

Adds a new component `ion-input-otp` which provides the OTP input functionality

- Displays as an input group with multiple boxes accepting a single character
- Accepts `type` which determines whether the boxes accept numbers or text/numbers and determines the keyboard to display
- Supports changing the displayed keyboard using the `inputmode` property
- Accepts a `length` property to control the number of input boxes
- Accepts the following properties to change the design: `fill`, `shape`, `size`, `color`
- Accepts a `separators` property to show a separator between 1 or more input boxes
- Supports the `disabled`, `readonly` and invalid states
- Supports limiting the accepted input via the `pattern` property
- Emits the following events: `ionInput`, `ionChange`, `ionComplete`, `ionBlur`, `ionFocus`
- Exposes the following method: `setFocus`

---------

Co-authored-by: Brandy Smith <6577830+brandyscarney@users.noreply.github.com>
Co-authored-by: Shane <shane@shanessite.net>
This commit is contained in:
Brandy Smith
2025-05-29 15:10:37 -04:00
committed by GitHub
parent 2dea6071db
commit 4d6a067677
275 changed files with 4452 additions and 79 deletions

View File

@ -44,6 +44,20 @@
<ion-input label="Input" formControlName="input2"></ion-input>
</ion-item>
<ion-item>
<ion-input-otp id="touched-input-otp-number-test" formControlName="inputOtp" class="required">Input OTP (required)</ion-input-otp>
</ion-item>
<ion-button id="input-otp-touched" (click)="setOtpTouched()">Set Input OTP Touched</ion-button>
<ion-item>
<ion-input-otp id="touched-input-otp-text-test" type="text" formControlName="inputOtpText" class="required">Input OTP Text (required)</ion-input-otp>
</ion-item>
<ion-item>
<ion-input-otp formControlName="inputOtp2">Input OTP</ion-input-otp>
</ion-item>
<ion-item>
<ion-checkbox formControlName="checkbox"> Checkbox </ion-checkbox>
</ion-item>

View File

@ -1,6 +1,15 @@
import { Component } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, UntypedFormControl } from '@angular/forms';
import { UntypedFormGroup, UntypedFormBuilder, Validators, UntypedFormControl, AbstractControl, ValidationErrors } from '@angular/forms';
function otpRequiredLength(length: number) {
return (control: AbstractControl): ValidationErrors | null => {
const value = control.value;
if (!value || value.toString().length !== length) {
return { otpLength: true };
}
return null;
};
}
@Component({
selector: 'app-form',
templateUrl: './form.component.html',
@ -19,6 +28,9 @@ export class FormComponent {
toggle: [false],
input: ['', Validators.required],
input2: ['Default Value'],
inputOtp: [null, [Validators.required, otpRequiredLength(4)]],
inputOtpText: ['', [Validators.required, otpRequiredLength(4)]],
inputOtp2: [1234],
inputMin: [1, Validators.min(1)],
inputMax: [1, Validators.max(1)],
checkbox: [false],
@ -35,6 +47,13 @@ export class FormComponent {
}
}
setOtpTouched() {
const formControl = this.profileForm.get('inputOtp');
if (formControl) {
formControl.markAsTouched();
}
}
onSubmit() {
this.submitted = 'true';
}
@ -46,6 +65,9 @@ export class FormComponent {
toggle: true,
input: 'Some value',
input2: 'Another values',
inputOtp: 5678,
inputOtpText: 'ABCD',
inputOtp2: 1234,
checkbox: true,
radio: 'nes'
});

View File

@ -71,6 +71,16 @@
<ion-note slot="end">{{input}}</ion-note>
</ion-item>
<ion-item>
<ion-input-otp [(ngModel)]="inputOtp">Input OTP</ion-input-otp>
<ion-note slot="end" id="input-otp-note">{{inputOtp}}</ion-note>
</ion-item>
<ion-item color="dark">
<ion-input-otp [(ngModel)]="inputOtp">Input OTP Mirror</ion-input-otp>
<ion-note slot="end">{{inputOtp}}</ion-note>
</ion-item>
<ion-item>
<ion-checkbox [(ngModel)]="checkbox" slot="start" id="first-checkbox">
Checkbox

View File

@ -9,6 +9,7 @@ export class InputsComponent {
datetime? = '1994-03-15';
input? = 'some text';
inputOtp? = '1234';
checkbox = true;
radio? = 'nes';
toggle = true;
@ -20,6 +21,7 @@ export class InputsComponent {
console.log('set values');
this.datetime = '1994-03-15';
this.input = 'some text';
this.inputOtp = '1234';
this.checkbox = true;
this.radio = 'nes';
this.toggle = true;
@ -31,6 +33,7 @@ export class InputsComponent {
console.log('reset values');
this.datetime = undefined;
this.input = undefined;
this.inputOtp = undefined;
this.checkbox = false;
this.radio = undefined;
this.toggle = false;

View File

@ -44,6 +44,7 @@ export const routes: Routes = [
{ path: 'checkbox', loadComponent: () => import('../value-accessors/checkbox/checkbox.component').then(c => c.CheckboxComponent) },
{ path: 'datetime', loadComponent: () => import('../value-accessors/datetime/datetime.component').then(c => c.DatetimeComponent) },
{ path: 'input', loadComponent: () => import('../value-accessors/input/input.component').then(c => c.InputComponent) },
{ path: 'input-otp', loadComponent: () => import('../value-accessors/input-otp/input-otp.component').then(c => c.InputOtpComponent) },
{ path: 'radio-group', loadComponent: () => import('../value-accessors/radio-group/radio-group.component').then(c => c.RadioGroupComponent) },
{ path: 'range', loadComponent: () => import('../value-accessors/range/range.component').then(c => c.RangeComponent) },
{ path: 'searchbar', loadComponent: () => import('../value-accessors/searchbar/searchbar.component').then(c => c.SearchbarComponent) },

View File

@ -116,6 +116,11 @@
Input Test
</ion-label>
</ion-item>
<ion-item routerLink="/standalone/value-accessors/input-otp">
<ion-label>
Input OTP Test
</ion-label>
</ion-item>
<ion-item routerLink="/standalone/value-accessors/radio-group">
<ion-label>
Radio Group Test

View File

@ -0,0 +1,11 @@
<div>
<h1>IonInputOtp Value Accessors</h1>
<p>
This test checks the form integrations with ion-input-otp to make sure values are correctly assigned to the form group.
</p>
<app-value-accessor-test [formGroup]="form">
<ion-input-otp type="text" formControlName="inputOtpString">String</ion-input-otp>
<ion-input-otp type="number" formControlName="inputOtpNumber">Number</ion-input-otp>
</app-value-accessor-test>
</div>

View File

@ -0,0 +1,34 @@
import { Component } from "@angular/core";
import { FormBuilder, FormsModule, ReactiveFormsModule, Validators, AbstractControl, ValidationErrors } from "@angular/forms";
import { IonInputOtp } from "@ionic/angular/standalone";
import { ValueAccessorTestComponent } from "../value-accessor-test/value-accessor-test.component";
function otpRequiredLength(length: number) {
return (control: AbstractControl): ValidationErrors | null => {
const value = control.value;
if (!value || value.toString().length !== length) {
return { otpLength: true };
}
return null;
};
}
@Component({
selector: 'app-input-otp',
templateUrl: 'input-otp.component.html',
standalone: true,
imports: [
IonInputOtp,
ReactiveFormsModule,
FormsModule,
ValueAccessorTestComponent
]
})
export class InputOtpComponent {
form = this.fb.group({
inputOtpString: ['', [Validators.required, otpRequiredLength(4)]],
inputOtpNumber: ['', [Validators.required, otpRequiredLength(4)]],
});
constructor(private fb: FormBuilder) { }
}