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

@@ -3,7 +3,7 @@ import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { ValueAccessor } from '@ionic/angular/common';
@Directive({
selector: 'ion-input[type=number],ion-range',
selector: 'ion-input[type=number],ion-input-otp:not([type=text]),ion-range',
providers: [
{
provide: NG_VALUE_ACCESSOR,
@@ -18,12 +18,12 @@ export class NumericValueAccessorDirective extends ValueAccessor {
}
@HostListener('ionInput', ['$event.target'])
handleInputEvent(el: HTMLIonInputElement | HTMLIonRangeElement): void {
handleInputEvent(el: HTMLIonInputElement | HTMLIonInputOtpElement | HTMLIonRangeElement): void {
this.handleValueChange(el, el.value);
}
registerOnChange(fn: (_: number | null) => void): void {
if (this.el.nativeElement.tagName === 'ION-INPUT') {
if (this.el.nativeElement.tagName === 'ION-INPUT' || this.el.nativeElement.tagName === 'ION-INPUT-OTP') {
super.registerOnChange((value: string) => {
fn(value === '' ? null : parseFloat(value));
});

View File

@@ -3,7 +3,7 @@ import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { ValueAccessor } from '@ionic/angular/common';
@Directive({
selector: 'ion-input:not([type=number]),ion-textarea,ion-searchbar',
selector: 'ion-input:not([type=number]),ion-input-otp[type=text],ion-textarea,ion-searchbar',
providers: [
{
provide: NG_VALUE_ACCESSOR,
@@ -18,7 +18,9 @@ export class TextValueAccessorDirective extends ValueAccessor {
}
@HostListener('ionInput', ['$event.target'])
_handleInputEvent(el: HTMLIonInputElement | HTMLIonTextareaElement | HTMLIonSearchbarElement): void {
_handleInputEvent(
el: HTMLIonInputElement | HTMLIonInputOtpElement | HTMLIonTextareaElement | HTMLIonSearchbarElement
): void {
this.handleValueChange(el, el.value);
}
}

View File

@@ -36,6 +36,7 @@ export const DIRECTIVES = [
d.IonInfiniteScroll,
d.IonInfiniteScrollContent,
d.IonInput,
d.IonInputOtp,
d.IonInputPasswordToggle,
d.IonItem,
d.IonItemDivider,

View File

@@ -1017,6 +1017,69 @@ This event will not emit when programmatically setting the `value` property.
}
@ProxyCmp({
inputs: ['autocapitalize', 'color', 'disabled', 'fill', 'inputmode', 'length', 'pattern', 'readonly', 'separators', 'shape', 'size', 'type', 'value'],
methods: ['setFocus']
})
@Component({
selector: 'ion-input-otp',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['autocapitalize', 'color', 'disabled', 'fill', 'inputmode', 'length', 'pattern', 'readonly', 'separators', 'shape', 'size', 'type', 'value'],
})
export class IonInputOtp {
protected el: HTMLIonInputOtpElement;
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
c.detach();
this.el = r.nativeElement;
proxyOutputs(this, this.el, ['ionInput', 'ionChange', 'ionComplete', 'ionBlur', 'ionFocus']);
}
}
import type { InputOtpInputEventDetail as IIonInputOtpInputOtpInputEventDetail } from '@ionic/core';
import type { InputOtpChangeEventDetail as IIonInputOtpInputOtpChangeEventDetail } from '@ionic/core';
import type { InputOtpCompleteEventDetail as IIonInputOtpInputOtpCompleteEventDetail } from '@ionic/core';
export declare interface IonInputOtp extends Components.IonInputOtp {
/**
* The `ionInput` event is fired each time the user modifies the input's value.
Unlike the `ionChange` event, the `ionInput` event is fired for each alteration
to the input's value. This typically happens for each keystroke as the user types.
For elements that accept text input (`type=text`, `type=tel`, etc.), the interface
is [`InputEvent`](https://developer.mozilla.org/en-US/docs/Web/API/InputEvent); for others,
the interface is [`Event`](https://developer.mozilla.org/en-US/docs/Web/API/Event). If
the input is cleared on edit, the type is `null`.
*/
ionInput: EventEmitter<CustomEvent<IIonInputOtpInputOtpInputEventDetail>>;
/**
* The `ionChange` event is fired when the user modifies the input's value.
Unlike the `ionInput` event, the `ionChange` event is only fired when changes
are committed, not as the user types.
The `ionChange` event fires when the `<ion-input-otp>` component loses
focus after its value has changed.
This event will not emit when programmatically setting the `value` property.
*/
ionChange: EventEmitter<CustomEvent<IIonInputOtpInputOtpChangeEventDetail>>;
/**
* Emitted when all input boxes have been filled with valid values.
*/
ionComplete: EventEmitter<CustomEvent<IIonInputOtpInputOtpCompleteEventDetail>>;
/**
* Emitted when the input group loses focus.
*/
ionBlur: EventEmitter<CustomEvent<FocusEvent>>;
/**
* Emitted when the input group has focus.
*/
ionFocus: EventEmitter<CustomEvent<FocusEvent>>;
}
@ProxyCmp({
inputs: ['color', 'hideIcon', 'mode', 'showIcon']
})