Skip to content

Commit 4d6a067

Browse files
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>
1 parent 2dea607 commit 4d6a067

File tree

275 files changed

+4452
-79
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

275 files changed

+4452
-79
lines changed

core/.eslintrc.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ module.exports = {
2626
"@typescript-eslint/no-unused-vars": [
2727
"warn",
2828
{
29-
"varsIgnorePattern": "^h$"
29+
"varsIgnorePattern": "^(h|Fragment)$"
3030
}
3131
],
3232
"no-useless-catch": "off",

core/api.txt

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,73 @@ ion-input,css-prop,--placeholder-font-weight,md
780780
ion-input,css-prop,--placeholder-opacity,ios
781781
ion-input,css-prop,--placeholder-opacity,md
782782

783+
ion-input-otp,scoped
784+
ion-input-otp,prop,autocapitalize,string,'off',false,false
785+
ion-input-otp,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
786+
ion-input-otp,prop,disabled,boolean,false,false,true
787+
ion-input-otp,prop,fill,"outline" | "solid" | undefined,'outline',false,false
788+
ion-input-otp,prop,inputmode,"decimal" | "email" | "none" | "numeric" | "search" | "tel" | "text" | "url" | undefined,undefined,false,false
789+
ion-input-otp,prop,length,number,4,false,false
790+
ion-input-otp,prop,pattern,string | undefined,undefined,false,false
791+
ion-input-otp,prop,readonly,boolean,false,false,true
792+
ion-input-otp,prop,separators,number[] | string | undefined,undefined,false,false
793+
ion-input-otp,prop,shape,"rectangular" | "round" | "soft",'round',false,false
794+
ion-input-otp,prop,size,"large" | "medium" | "small",'medium',false,false
795+
ion-input-otp,prop,type,"number" | "text",'number',false,false
796+
ion-input-otp,prop,value,null | number | string | undefined,'',false,false
797+
ion-input-otp,method,setFocus,setFocus(index?: number) => Promise<void>
798+
ion-input-otp,event,ionBlur,FocusEvent,true
799+
ion-input-otp,event,ionChange,InputOtpChangeEventDetail,true
800+
ion-input-otp,event,ionComplete,InputOtpCompleteEventDetail,true
801+
ion-input-otp,event,ionFocus,FocusEvent,true
802+
ion-input-otp,event,ionInput,InputOtpInputEventDetail,true
803+
ion-input-otp,css-prop,--background,ios
804+
ion-input-otp,css-prop,--background,md
805+
ion-input-otp,css-prop,--border-color,ios
806+
ion-input-otp,css-prop,--border-color,md
807+
ion-input-otp,css-prop,--border-radius,ios
808+
ion-input-otp,css-prop,--border-radius,md
809+
ion-input-otp,css-prop,--border-width,ios
810+
ion-input-otp,css-prop,--border-width,md
811+
ion-input-otp,css-prop,--color,ios
812+
ion-input-otp,css-prop,--color,md
813+
ion-input-otp,css-prop,--height,ios
814+
ion-input-otp,css-prop,--height,md
815+
ion-input-otp,css-prop,--highlight-color-focused,ios
816+
ion-input-otp,css-prop,--highlight-color-focused,md
817+
ion-input-otp,css-prop,--highlight-color-invalid,ios
818+
ion-input-otp,css-prop,--highlight-color-invalid,md
819+
ion-input-otp,css-prop,--highlight-color-valid,ios
820+
ion-input-otp,css-prop,--highlight-color-valid,md
821+
ion-input-otp,css-prop,--margin-bottom,ios
822+
ion-input-otp,css-prop,--margin-bottom,md
823+
ion-input-otp,css-prop,--margin-end,ios
824+
ion-input-otp,css-prop,--margin-end,md
825+
ion-input-otp,css-prop,--margin-start,ios
826+
ion-input-otp,css-prop,--margin-start,md
827+
ion-input-otp,css-prop,--margin-top,ios
828+
ion-input-otp,css-prop,--margin-top,md
829+
ion-input-otp,css-prop,--min-width,ios
830+
ion-input-otp,css-prop,--min-width,md
831+
ion-input-otp,css-prop,--padding-bottom,ios
832+
ion-input-otp,css-prop,--padding-bottom,md
833+
ion-input-otp,css-prop,--padding-end,ios
834+
ion-input-otp,css-prop,--padding-end,md
835+
ion-input-otp,css-prop,--padding-start,ios
836+
ion-input-otp,css-prop,--padding-start,md
837+
ion-input-otp,css-prop,--padding-top,ios
838+
ion-input-otp,css-prop,--padding-top,md
839+
ion-input-otp,css-prop,--separator-border-radius,ios
840+
ion-input-otp,css-prop,--separator-border-radius,md
841+
ion-input-otp,css-prop,--separator-color,ios
842+
ion-input-otp,css-prop,--separator-color,md
843+
ion-input-otp,css-prop,--separator-height,ios
844+
ion-input-otp,css-prop,--separator-height,md
845+
ion-input-otp,css-prop,--separator-width,ios
846+
ion-input-otp,css-prop,--separator-width,md
847+
ion-input-otp,css-prop,--width,ios
848+
ion-input-otp,css-prop,--width,md
849+
783850
ion-input-password-toggle,shadow
784851
ion-input-password-toggle,prop,color,"danger" | "dark" | "light" | "medium" | "primary" | "secondary" | "success" | "tertiary" | "warning" | string & Record<never, never> | undefined,undefined,false,true
785852
ion-input-password-toggle,prop,hideIcon,string | undefined,undefined,false,false

core/src/components.d.ts

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { ScrollBaseDetail, ScrollDetail } from "./components/content/content-int
1818
import { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimePresentation, FormatOptions, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface";
1919
import { SpinnerTypes } from "./components/spinner/spinner-configs";
2020
import { InputChangeEventDetail, InputInputEventDetail } from "./components/input/input-interface";
21+
import { InputOtpChangeEventDetail, InputOtpCompleteEventDetail, InputOtpInputEventDetail } from "./components/input-otp/input-otp-interface";
2122
import { MenuChangeEventDetail, MenuCloseEventDetail, MenuType, Side } from "./components/menu/menu-interface";
2223
import { ModalBreakpointChangeEventDetail, ModalHandleBehavior } from "./components/modal/modal-interface";
2324
import { NavComponent, NavComponentWithProps, NavOptions, RouterOutletOptions, SwipeGestureHandler, TransitionDoneFn, TransitionInstruction } from "./components/nav/nav-interface";
@@ -55,6 +56,7 @@ export { ScrollBaseDetail, ScrollDetail } from "./components/content/content-int
5556
export { DatetimeChangeEventDetail, DatetimeHighlight, DatetimeHighlightCallback, DatetimeHourCycle, DatetimePresentation, FormatOptions, TitleSelectedDatesFormatter } from "./components/datetime/datetime-interface";
5657
export { SpinnerTypes } from "./components/spinner/spinner-configs";
5758
export { InputChangeEventDetail, InputInputEventDetail } from "./components/input/input-interface";
59+
export { InputOtpChangeEventDetail, InputOtpCompleteEventDetail, InputOtpInputEventDetail } from "./components/input-otp/input-otp-interface";
5860
export { MenuChangeEventDetail, MenuCloseEventDetail, MenuType, Side } from "./components/menu/menu-interface";
5961
export { ModalBreakpointChangeEventDetail, ModalHandleBehavior } from "./components/modal/modal-interface";
6062
export { NavComponent, NavComponentWithProps, NavOptions, RouterOutletOptions, SwipeGestureHandler, TransitionDoneFn, TransitionInstruction } from "./components/nav/nav-interface";
@@ -1319,6 +1321,65 @@ export namespace Components {
13191321
*/
13201322
"value"?: string | number | null;
13211323
}
1324+
interface IonInputOtp {
1325+
/**
1326+
* Indicates whether and how the text value should be automatically capitalized as it is entered/edited by the user. Available options: `"off"`, `"none"`, `"on"`, `"sentences"`, `"words"`, `"characters"`.
1327+
*/
1328+
"autocapitalize": string;
1329+
/**
1330+
* The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics).
1331+
*/
1332+
"color"?: Color;
1333+
/**
1334+
* If `true`, the user cannot interact with the input.
1335+
*/
1336+
"disabled": boolean;
1337+
/**
1338+
* The fill for the input boxes. If `"solid"` the input boxes will have a background. If `"outline"` the input boxes will be transparent with a border.
1339+
*/
1340+
"fill"?: 'outline' | 'solid';
1341+
/**
1342+
* A hint to the browser for which keyboard to display. Possible values: `"none"`, `"text"`, `"tel"`, `"url"`, `"email"`, `"numeric"`, `"decimal"`, and `"search"`. For numbers (type="number"): "numeric" For text (type="text"): "text"
1343+
*/
1344+
"inputmode"?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search';
1345+
/**
1346+
* The number of input boxes to display.
1347+
*/
1348+
"length": number;
1349+
/**
1350+
* A regex pattern string for allowed characters. Defaults based on type. For numbers (`type="number"`): `"[\p{N}]"` For text (`type="text"`): `"[\p{L}\p{N}]"`
1351+
*/
1352+
"pattern"?: string;
1353+
/**
1354+
* If `true`, the user cannot modify the value.
1355+
*/
1356+
"readonly": boolean;
1357+
/**
1358+
* Where separators should be shown between input boxes. Can be a comma-separated string or an array of numbers. For example: `"3"` will show a separator after the 3rd input box. `[1,4]` will show a separator after the 1st and 4th input boxes. `"all"` will show a separator between every input box.
1359+
*/
1360+
"separators"?: 'all' | string | number[];
1361+
/**
1362+
* Sets focus to an input box.
1363+
* @param index - The index of the input box to focus (0-based). If provided and the input box has a value, the input box at that index will be focused. Otherwise, the first empty input box or the last input if all are filled will be focused.
1364+
*/
1365+
"setFocus": (index?: number) => Promise<void>;
1366+
/**
1367+
* The shape of the input boxes. If "round" they will have an increased border radius. If "rectangular" they will have no border radius. If "soft" they will have a soft border radius.
1368+
*/
1369+
"shape": 'round' | 'rectangular' | 'soft';
1370+
/**
1371+
* The size of the input boxes.
1372+
*/
1373+
"size": 'small' | 'medium' | 'large';
1374+
/**
1375+
* The type of input allowed in the input boxes.
1376+
*/
1377+
"type": 'text' | 'number';
1378+
/**
1379+
* The value of the input group.
1380+
*/
1381+
"value"?: string | number | null;
1382+
}
13221383
interface IonInputPasswordToggle {
13231384
/**
13241385
* The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics).
@@ -3404,6 +3465,10 @@ export interface IonInputCustomEvent<T> extends CustomEvent<T> {
34043465
detail: T;
34053466
target: HTMLIonInputElement;
34063467
}
3468+
export interface IonInputOtpCustomEvent<T> extends CustomEvent<T> {
3469+
detail: T;
3470+
target: HTMLIonInputOtpElement;
3471+
}
34073472
export interface IonItemOptionsCustomEvent<T> extends CustomEvent<T> {
34083473
detail: T;
34093474
target: HTMLIonItemOptionsElement;
@@ -3933,6 +3998,27 @@ declare global {
39333998
prototype: HTMLIonInputElement;
39343999
new (): HTMLIonInputElement;
39354000
};
4001+
interface HTMLIonInputOtpElementEventMap {
4002+
"ionInput": InputOtpInputEventDetail;
4003+
"ionChange": InputOtpChangeEventDetail;
4004+
"ionComplete": InputOtpCompleteEventDetail;
4005+
"ionBlur": FocusEvent;
4006+
"ionFocus": FocusEvent;
4007+
}
4008+
interface HTMLIonInputOtpElement extends Components.IonInputOtp, HTMLStencilElement {
4009+
addEventListener<K extends keyof HTMLIonInputOtpElementEventMap>(type: K, listener: (this: HTMLIonInputOtpElement, ev: IonInputOtpCustomEvent<HTMLIonInputOtpElementEventMap[K]>) => any, options?: boolean | AddEventListenerOptions): void;
4010+
addEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
4011+
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
4012+
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
4013+
removeEventListener<K extends keyof HTMLIonInputOtpElementEventMap>(type: K, listener: (this: HTMLIonInputOtpElement, ev: IonInputOtpCustomEvent<HTMLIonInputOtpElementEventMap[K]>) => any, options?: boolean | EventListenerOptions): void;
4014+
removeEventListener<K extends keyof DocumentEventMap>(type: K, listener: (this: Document, ev: DocumentEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
4015+
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
4016+
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
4017+
}
4018+
var HTMLIonInputOtpElement: {
4019+
prototype: HTMLIonInputOtpElement;
4020+
new (): HTMLIonInputOtpElement;
4021+
};
39364022
interface HTMLIonInputPasswordToggleElement extends Components.IonInputPasswordToggle, HTMLStencilElement {
39374023
}
39384024
var HTMLIonInputPasswordToggleElement: {
@@ -4792,6 +4878,7 @@ declare global {
47924878
"ion-infinite-scroll": HTMLIonInfiniteScrollElement;
47934879
"ion-infinite-scroll-content": HTMLIonInfiniteScrollContentElement;
47944880
"ion-input": HTMLIonInputElement;
4881+
"ion-input-otp": HTMLIonInputOtpElement;
47954882
"ion-input-password-toggle": HTMLIonInputPasswordToggleElement;
47964883
"ion-item": HTMLIonItemElement;
47974884
"ion-item-divider": HTMLIonItemDividerElement;
@@ -6178,6 +6265,80 @@ declare namespace LocalJSX {
61786265
*/
61796266
"value"?: string | number | null;
61806267
}
6268+
interface IonInputOtp {
6269+
/**
6270+
* Indicates whether and how the text value should be automatically capitalized as it is entered/edited by the user. Available options: `"off"`, `"none"`, `"on"`, `"sentences"`, `"words"`, `"characters"`.
6271+
*/
6272+
"autocapitalize"?: string;
6273+
/**
6274+
* The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics).
6275+
*/
6276+
"color"?: Color;
6277+
/**
6278+
* If `true`, the user cannot interact with the input.
6279+
*/
6280+
"disabled"?: boolean;
6281+
/**
6282+
* The fill for the input boxes. If `"solid"` the input boxes will have a background. If `"outline"` the input boxes will be transparent with a border.
6283+
*/
6284+
"fill"?: 'outline' | 'solid';
6285+
/**
6286+
* A hint to the browser for which keyboard to display. Possible values: `"none"`, `"text"`, `"tel"`, `"url"`, `"email"`, `"numeric"`, `"decimal"`, and `"search"`. For numbers (type="number"): "numeric" For text (type="text"): "text"
6287+
*/
6288+
"inputmode"?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search';
6289+
/**
6290+
* The number of input boxes to display.
6291+
*/
6292+
"length"?: number;
6293+
/**
6294+
* Emitted when the input group loses focus.
6295+
*/
6296+
"onIonBlur"?: (event: IonInputOtpCustomEvent<FocusEvent>) => void;
6297+
/**
6298+
* 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.
6299+
*/
6300+
"onIonChange"?: (event: IonInputOtpCustomEvent<InputOtpChangeEventDetail>) => void;
6301+
/**
6302+
* Emitted when all input boxes have been filled with valid values.
6303+
*/
6304+
"onIonComplete"?: (event: IonInputOtpCustomEvent<InputOtpCompleteEventDetail>) => void;
6305+
/**
6306+
* Emitted when the input group has focus.
6307+
*/
6308+
"onIonFocus"?: (event: IonInputOtpCustomEvent<FocusEvent>) => void;
6309+
/**
6310+
* 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`.
6311+
*/
6312+
"onIonInput"?: (event: IonInputOtpCustomEvent<InputOtpInputEventDetail>) => void;
6313+
/**
6314+
* A regex pattern string for allowed characters. Defaults based on type. For numbers (`type="number"`): `"[\p{N}]"` For text (`type="text"`): `"[\p{L}\p{N}]"`
6315+
*/
6316+
"pattern"?: string;
6317+
/**
6318+
* If `true`, the user cannot modify the value.
6319+
*/
6320+
"readonly"?: boolean;
6321+
/**
6322+
* Where separators should be shown between input boxes. Can be a comma-separated string or an array of numbers. For example: `"3"` will show a separator after the 3rd input box. `[1,4]` will show a separator after the 1st and 4th input boxes. `"all"` will show a separator between every input box.
6323+
*/
6324+
"separators"?: 'all' | string | number[];
6325+
/**
6326+
* The shape of the input boxes. If "round" they will have an increased border radius. If "rectangular" they will have no border radius. If "soft" they will have a soft border radius.
6327+
*/
6328+
"shape"?: 'round' | 'rectangular' | 'soft';
6329+
/**
6330+
* The size of the input boxes.
6331+
*/
6332+
"size"?: 'small' | 'medium' | 'large';
6333+
/**
6334+
* The type of input allowed in the input boxes.
6335+
*/
6336+
"type"?: 'text' | 'number';
6337+
/**
6338+
* The value of the input group.
6339+
*/
6340+
"value"?: string | number | null;
6341+
}
61816342
interface IonInputPasswordToggle {
61826343
/**
61836344
* The color to use from your application's color palette. Default options are: `"primary"`, `"secondary"`, `"tertiary"`, `"success"`, `"warning"`, `"danger"`, `"light"`, `"medium"`, and `"dark"`. For more information on colors, see [theming](/docs/theming/basics).
@@ -8309,6 +8470,7 @@ declare namespace LocalJSX {
83098470
"ion-infinite-scroll": IonInfiniteScroll;
83108471
"ion-infinite-scroll-content": IonInfiniteScrollContent;
83118472
"ion-input": IonInput;
8473+
"ion-input-otp": IonInputOtp;
83128474
"ion-input-password-toggle": IonInputPasswordToggle;
83138475
"ion-item": IonItem;
83148476
"ion-item-divider": IonItemDivider;
@@ -8411,6 +8573,7 @@ declare module "@stencil/core" {
84118573
"ion-infinite-scroll": LocalJSX.IonInfiniteScroll & JSXBase.HTMLAttributes<HTMLIonInfiniteScrollElement>;
84128574
"ion-infinite-scroll-content": LocalJSX.IonInfiniteScrollContent & JSXBase.HTMLAttributes<HTMLIonInfiniteScrollContentElement>;
84138575
"ion-input": LocalJSX.IonInput & JSXBase.HTMLAttributes<HTMLIonInputElement>;
8576+
"ion-input-otp": LocalJSX.IonInputOtp & JSXBase.HTMLAttributes<HTMLIonInputOtpElement>;
84148577
"ion-input-password-toggle": LocalJSX.IonInputPasswordToggle & JSXBase.HTMLAttributes<HTMLIonInputPasswordToggleElement>;
84158578
"ion-item": LocalJSX.IonItem & JSXBase.HTMLAttributes<HTMLIonItemElement>;
84168579
"ion-item-divider": LocalJSX.IonItemDivider & JSXBase.HTMLAttributes<HTMLIonItemDividerElement>;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* Values are converted to strings when emitted which is
3+
* why we do not have a `number` type here even though the
4+
* `value` prop accepts a `number` type.
5+
*/
6+
export interface InputOtpInputEventDetail {
7+
value?: string | null;
8+
event?: Event;
9+
}
10+
export interface InputOtpChangeEventDetail {
11+
value?: string | null;
12+
event?: Event;
13+
}
14+
15+
export interface InputOtpCompleteEventDetail {
16+
value?: string | null;
17+
event?: Event;
18+
}
19+
20+
export interface InputOtpCustomEvent<T = InputOtpChangeEventDetail> extends CustomEvent {
21+
detail: T;
22+
target: HTMLIonInputOtpElement;
23+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
@import "./input-otp";
2+
@import "../../themes/ionic.globals.ios";
3+
4+
// iOS Input OTP
5+
// --------------------------------------------------
6+
7+
:host {
8+
--border-width: #{$hairlines-width};
9+
}
10+
11+
:host(.has-focus) .native-input:focus {
12+
--border-width: 1px;
13+
}
14+
15+
// Fills
16+
// --------------------------------------------------
17+
18+
:host(.input-otp-fill-outline) {
19+
--border-color: #{$item-ios-border-color};
20+
}

0 commit comments

Comments
 (0)