@ionic/core
Version:
Base components for Ionic
632 lines (626 loc) • 40.2 kB
JavaScript
/*!
* (C) Ionic http://ionicframework.com - MIT License
*/
import { proxyCustomElement, HTMLElement, createEvent, h, Host, Fragment } from '@stencil/core/internal/client';
import { i as inheritAriaAttributes } from './helpers.js';
import { p as printIonWarning } from './index4.js';
import { i as isRTL } from './dir.js';
import { c as createColorClasses } from './theme.js';
import { b as getIonMode } from './ionic-global.js';
const inputOtpIosCss = ".sc-ion-input-otp-ios-h{--margin-top:0;--margin-end:0;--margin-bottom:0;--margin-start:0;--padding-top:16px;--padding-end:0;--padding-bottom:16px;--padding-start:0;--color:initial;--min-width:40px;--separator-width:8px;--separator-height:var(--separator-width);--separator-border-radius:999px;--separator-color:var(--ion-color-step-150, var(--ion-background-color-step-150, #d9d9d9));--highlight-color-focused:var(--ion-color-primary, #0054e9);--highlight-color-valid:var(--ion-color-success, #2dd55b);--highlight-color-invalid:var(--ion-color-danger, #c5000f);--highlight-color:var(--highlight-color-focused);display:block;position:relative;font-size:0.875rem}.input-otp-group.sc-ion-input-otp-ios{-webkit-margin-start:var(--margin-start);margin-inline-start:var(--margin-start);-webkit-margin-end:var(--margin-end);margin-inline-end:var(--margin-end);margin-top:var(--margin-top);margin-bottom:var(--margin-bottom);-webkit-padding-start:var(--padding-start);padding-inline-start:var(--padding-start);-webkit-padding-end:var(--padding-end);padding-inline-end:var(--padding-end);padding-top:var(--padding-top);padding-bottom:var(--padding-bottom);display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.native-wrapper.sc-ion-input-otp-ios{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;min-width:var(--min-width)}.native-input.sc-ion-input-otp-ios{border-radius:var(--border-radius);width:var(--width);min-width:inherit;height:var(--height);border-width:var(--border-width);border-style:solid;border-color:var(--border-color);background:var(--background);color:var(--color);font-size:inherit;text-align:center;-webkit-appearance:none;-moz-appearance:none;appearance:none}.has-focus.sc-ion-input-otp-ios-h .native-input.sc-ion-input-otp-ios{caret-color:var(--highlight-color)}.input-otp-description.sc-ion-input-otp-ios{color:var(--ion-color-step-700, var(--ion-text-color-step-300, #4d4d4d));font-size:0.75rem;line-height:1.25rem;text-align:center}.input-otp-description-hidden.sc-ion-input-otp-ios{display:none}.input-otp-separator.sc-ion-input-otp-ios{border-radius:var(--separator-border-radius);-ms-flex-negative:0;flex-shrink:0;width:var(--separator-width);height:var(--separator-height);background:var(--separator-color)}.input-otp-size-small.sc-ion-input-otp-ios-h{--width:40px;--height:40px}.input-otp-size-small.sc-ion-input-otp-ios-h .input-otp-group.sc-ion-input-otp-ios{gap:8px}.input-otp-size-medium.sc-ion-input-otp-ios-h{--width:48px;--height:48px}.input-otp-size-large.sc-ion-input-otp-ios-h{--width:56px;--height:56px}.input-otp-size-medium.sc-ion-input-otp-ios-h .input-otp-group.sc-ion-input-otp-ios,.input-otp-size-large.sc-ion-input-otp-ios-h .input-otp-group.sc-ion-input-otp-ios{gap:12px}.input-otp-shape-round.sc-ion-input-otp-ios-h{--border-radius:16px}.input-otp-shape-soft.sc-ion-input-otp-ios-h{--border-radius:8px}.input-otp-shape-rectangular.sc-ion-input-otp-ios-h{--border-radius:0}.input-otp-fill-outline.sc-ion-input-otp-ios-h{--background:none}.input-otp-fill-solid.sc-ion-input-otp-ios-h{--border-color:var(--ion-color-step-50, var(--ion-background-color-step-50, #f2f2f2));--background:var(--ion-color-step-50, var(--ion-background-color-step-50, #f2f2f2))}.input-otp-disabled.sc-ion-input-otp-ios-h{--color:var(--ion-color-step-350, var(--ion-text-color-step-650, #a6a6a6))}.input-otp-fill-outline.input-otp-disabled.sc-ion-input-otp-ios-h{--background:var(--ion-color-step-50, var(--ion-background-color-step-50, #f2f2f2));--border-color:var(--ion-color-step-100, var(--ion-background-color-step-100, #e6e6e6))}.input-otp-disabled.sc-ion-input-otp-ios-h,.input-otp-disabled.sc-ion-input-otp-ios-h .native-input.sc-ion-input-otp-ios:disabled{cursor:not-allowed}.has-focus.sc-ion-input-otp-ios-h .native-input.sc-ion-input-otp-ios:focus{--border-color:var(--highlight-color);outline:none}.input-otp-fill-outline.input-otp-readonly.sc-ion-input-otp-ios-h{--background:var(--ion-color-step-50, var(--ion-background-color-step-50, #f2f2f2))}.input-otp-fill-solid.input-otp-disabled.sc-ion-input-otp-ios-h,.input-otp-fill-solid.input-otp-readonly.sc-ion-input-otp-ios-h{--border-color:var(--ion-color-step-100, var(--ion-background-color-step-100, #e6e6e6));--background:var(--ion-color-step-100, var(--ion-background-color-step-100, #e6e6e6))}.ion-touched.ion-invalid.sc-ion-input-otp-ios-h{--highlight-color:var(--highlight-color-invalid)}.ion-valid.sc-ion-input-otp-ios-h{--highlight-color:var(--highlight-color-valid)}.has-focus.ion-valid.sc-ion-input-otp-ios-h,.ion-touched.ion-invalid.sc-ion-input-otp-ios-h{--border-color:var(--highlight-color)}.ion-color.sc-ion-input-otp-ios-h{--highlight-color-focused:var(--ion-color-base)}.input-otp-fill-outline.ion-color.sc-ion-input-otp-ios-h .native-input.sc-ion-input-otp-ios,.input-otp-fill-solid.ion-color.sc-ion-input-otp-ios-h .native-input.sc-ion-input-otp-ios:focus{border-color:rgba(var(--ion-color-base-rgb), 0.6)}.input-otp-fill-outline.ion-color.ion-invalid.sc-ion-input-otp-ios-h .native-input.sc-ion-input-otp-ios,.input-otp-fill-solid.ion-color.ion-invalid.sc-ion-input-otp-ios-h .native-input.sc-ion-input-otp-ios,.input-otp-fill-outline.ion-color.has-focus.ion-invalid.sc-ion-input-otp-ios-h .native-input.sc-ion-input-otp-ios,.input-otp-fill-solid.ion-color.has-focus.ion-invalid.sc-ion-input-otp-ios-h .native-input.sc-ion-input-otp-ios{border-color:var(--ion-color-danger, #c5000f)}.input-otp-fill-outline.ion-color.ion-valid.sc-ion-input-otp-ios-h .native-input.sc-ion-input-otp-ios,.input-otp-fill-solid.ion-color.ion-valid.sc-ion-input-otp-ios-h .native-input.sc-ion-input-otp-ios,.input-otp-fill-outline.ion-color.has-focus.ion-valid.sc-ion-input-otp-ios-h .native-input.sc-ion-input-otp-ios,.input-otp-fill-solid.ion-color.has-focus.ion-valid.sc-ion-input-otp-ios-h .native-input.sc-ion-input-otp-ios{border-color:var(--ion-color-success, #2dd55b)}.input-otp-fill-outline.input-otp-disabled.ion-color.sc-ion-input-otp-ios-h .native-input.sc-ion-input-otp-ios{border-color:rgba(var(--ion-color-base-rgb), 0.3)}.sc-ion-input-otp-ios-h{--border-width:0.55px}.has-focus.sc-ion-input-otp-ios-h .native-input.sc-ion-input-otp-ios:focus{--border-width:1px}.input-otp-fill-outline.sc-ion-input-otp-ios-h{--border-color:var(--ion-item-border-color, var(--ion-border-color, var(--ion-color-step-250, var(--ion-background-color-step-250, #c8c7cc))))}";
const inputOtpMdCss = ".sc-ion-input-otp-md-h{--margin-top:0;--margin-end:0;--margin-bottom:0;--margin-start:0;--padding-top:16px;--padding-end:0;--padding-bottom:16px;--padding-start:0;--color:initial;--min-width:40px;--separator-width:8px;--separator-height:var(--separator-width);--separator-border-radius:999px;--separator-color:var(--ion-color-step-150, var(--ion-background-color-step-150, #d9d9d9));--highlight-color-focused:var(--ion-color-primary, #0054e9);--highlight-color-valid:var(--ion-color-success, #2dd55b);--highlight-color-invalid:var(--ion-color-danger, #c5000f);--highlight-color:var(--highlight-color-focused);display:block;position:relative;font-size:0.875rem}.input-otp-group.sc-ion-input-otp-md{-webkit-margin-start:var(--margin-start);margin-inline-start:var(--margin-start);-webkit-margin-end:var(--margin-end);margin-inline-end:var(--margin-end);margin-top:var(--margin-top);margin-bottom:var(--margin-bottom);-webkit-padding-start:var(--padding-start);padding-inline-start:var(--padding-start);-webkit-padding-end:var(--padding-end);padding-inline-end:var(--padding-end);padding-top:var(--padding-top);padding-bottom:var(--padding-bottom);display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center}.native-wrapper.sc-ion-input-otp-md{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;min-width:var(--min-width)}.native-input.sc-ion-input-otp-md{border-radius:var(--border-radius);width:var(--width);min-width:inherit;height:var(--height);border-width:var(--border-width);border-style:solid;border-color:var(--border-color);background:var(--background);color:var(--color);font-size:inherit;text-align:center;-webkit-appearance:none;-moz-appearance:none;appearance:none}.has-focus.sc-ion-input-otp-md-h .native-input.sc-ion-input-otp-md{caret-color:var(--highlight-color)}.input-otp-description.sc-ion-input-otp-md{color:var(--ion-color-step-700, var(--ion-text-color-step-300, #4d4d4d));font-size:0.75rem;line-height:1.25rem;text-align:center}.input-otp-description-hidden.sc-ion-input-otp-md{display:none}.input-otp-separator.sc-ion-input-otp-md{border-radius:var(--separator-border-radius);-ms-flex-negative:0;flex-shrink:0;width:var(--separator-width);height:var(--separator-height);background:var(--separator-color)}.input-otp-size-small.sc-ion-input-otp-md-h{--width:40px;--height:40px}.input-otp-size-small.sc-ion-input-otp-md-h .input-otp-group.sc-ion-input-otp-md{gap:8px}.input-otp-size-medium.sc-ion-input-otp-md-h{--width:48px;--height:48px}.input-otp-size-large.sc-ion-input-otp-md-h{--width:56px;--height:56px}.input-otp-size-medium.sc-ion-input-otp-md-h .input-otp-group.sc-ion-input-otp-md,.input-otp-size-large.sc-ion-input-otp-md-h .input-otp-group.sc-ion-input-otp-md{gap:12px}.input-otp-shape-round.sc-ion-input-otp-md-h{--border-radius:16px}.input-otp-shape-soft.sc-ion-input-otp-md-h{--border-radius:8px}.input-otp-shape-rectangular.sc-ion-input-otp-md-h{--border-radius:0}.input-otp-fill-outline.sc-ion-input-otp-md-h{--background:none}.input-otp-fill-solid.sc-ion-input-otp-md-h{--border-color:var(--ion-color-step-50, var(--ion-background-color-step-50, #f2f2f2));--background:var(--ion-color-step-50, var(--ion-background-color-step-50, #f2f2f2))}.input-otp-disabled.sc-ion-input-otp-md-h{--color:var(--ion-color-step-350, var(--ion-text-color-step-650, #a6a6a6))}.input-otp-fill-outline.input-otp-disabled.sc-ion-input-otp-md-h{--background:var(--ion-color-step-50, var(--ion-background-color-step-50, #f2f2f2));--border-color:var(--ion-color-step-100, var(--ion-background-color-step-100, #e6e6e6))}.input-otp-disabled.sc-ion-input-otp-md-h,.input-otp-disabled.sc-ion-input-otp-md-h .native-input.sc-ion-input-otp-md:disabled{cursor:not-allowed}.has-focus.sc-ion-input-otp-md-h .native-input.sc-ion-input-otp-md:focus{--border-color:var(--highlight-color);outline:none}.input-otp-fill-outline.input-otp-readonly.sc-ion-input-otp-md-h{--background:var(--ion-color-step-50, var(--ion-background-color-step-50, #f2f2f2))}.input-otp-fill-solid.input-otp-disabled.sc-ion-input-otp-md-h,.input-otp-fill-solid.input-otp-readonly.sc-ion-input-otp-md-h{--border-color:var(--ion-color-step-100, var(--ion-background-color-step-100, #e6e6e6));--background:var(--ion-color-step-100, var(--ion-background-color-step-100, #e6e6e6))}.ion-touched.ion-invalid.sc-ion-input-otp-md-h{--highlight-color:var(--highlight-color-invalid)}.ion-valid.sc-ion-input-otp-md-h{--highlight-color:var(--highlight-color-valid)}.has-focus.ion-valid.sc-ion-input-otp-md-h,.ion-touched.ion-invalid.sc-ion-input-otp-md-h{--border-color:var(--highlight-color)}.ion-color.sc-ion-input-otp-md-h{--highlight-color-focused:var(--ion-color-base)}.input-otp-fill-outline.ion-color.sc-ion-input-otp-md-h .native-input.sc-ion-input-otp-md,.input-otp-fill-solid.ion-color.sc-ion-input-otp-md-h .native-input.sc-ion-input-otp-md:focus{border-color:rgba(var(--ion-color-base-rgb), 0.6)}.input-otp-fill-outline.ion-color.ion-invalid.sc-ion-input-otp-md-h .native-input.sc-ion-input-otp-md,.input-otp-fill-solid.ion-color.ion-invalid.sc-ion-input-otp-md-h .native-input.sc-ion-input-otp-md,.input-otp-fill-outline.ion-color.has-focus.ion-invalid.sc-ion-input-otp-md-h .native-input.sc-ion-input-otp-md,.input-otp-fill-solid.ion-color.has-focus.ion-invalid.sc-ion-input-otp-md-h .native-input.sc-ion-input-otp-md{border-color:var(--ion-color-danger, #c5000f)}.input-otp-fill-outline.ion-color.ion-valid.sc-ion-input-otp-md-h .native-input.sc-ion-input-otp-md,.input-otp-fill-solid.ion-color.ion-valid.sc-ion-input-otp-md-h .native-input.sc-ion-input-otp-md,.input-otp-fill-outline.ion-color.has-focus.ion-valid.sc-ion-input-otp-md-h .native-input.sc-ion-input-otp-md,.input-otp-fill-solid.ion-color.has-focus.ion-valid.sc-ion-input-otp-md-h .native-input.sc-ion-input-otp-md{border-color:var(--ion-color-success, #2dd55b)}.input-otp-fill-outline.input-otp-disabled.ion-color.sc-ion-input-otp-md-h .native-input.sc-ion-input-otp-md{border-color:rgba(var(--ion-color-base-rgb), 0.3)}.sc-ion-input-otp-md-h{--border-width:1px}.has-focus.sc-ion-input-otp-md-h .native-input.sc-ion-input-otp-md:focus{--border-width:2px}.input-otp-fill-outline.sc-ion-input-otp-md-h{--border-color:var(--ion-color-step-300, var(--ion-background-color-step-300, #b3b3b3))}";
const InputOTP = /*@__PURE__*/ proxyCustomElement(class InputOTP extends HTMLElement {
constructor() {
super();
this.__registerHost();
this.ionInput = createEvent(this, "ionInput", 7);
this.ionChange = createEvent(this, "ionChange", 7);
this.ionComplete = createEvent(this, "ionComplete", 7);
this.ionBlur = createEvent(this, "ionBlur", 7);
this.ionFocus = createEvent(this, "ionFocus", 7);
this.inheritedAttributes = {};
this.inputRefs = [];
this.inputId = `ion-input-otp-${inputIds++}`;
this.parsedSeparators = [];
/**
* Tracks whether the user is navigating through input boxes using keyboard navigation
* (arrow keys, tab) versus mouse clicks. This is used to determine the appropriate
* focus behavior when an input box is focused.
*/
this.isKeyboardNavigation = false;
this.inputValues = [];
this.hasFocus = false;
/**
* 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"`.
*/
this.autocapitalize = 'off';
/**
* If `true`, the user cannot interact with the input.
*/
this.disabled = false;
/**
* 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.
*/
this.fill = 'outline';
/**
* The number of input boxes to display.
*/
this.length = 4;
/**
* If `true`, the user cannot modify the value.
*/
this.readonly = false;
/**
* 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.
*/
this.shape = 'round';
/**
* The size of the input boxes.
*/
this.size = 'medium';
/**
* The type of input allowed in the input boxes.
*/
this.type = 'number';
/**
* The value of the input group.
*/
this.value = '';
/**
* Handles the focus behavior for the input OTP component.
*
* Focus behavior:
* 1. Keyboard navigation: Allow normal focus movement
* 2. Mouse click:
* - If clicked box has value: Focus that box
* - If clicked box is empty: Focus first empty box
*
* Emits the `ionFocus` event when the input group gains focus.
*/
this.onFocus = (index) => (event) => {
var _a;
const { inputRefs } = this;
// Only emit ionFocus and set the focusedValue when the
// component first gains focus
if (!this.hasFocus) {
this.ionFocus.emit(event);
this.focusedValue = this.value;
}
this.hasFocus = true;
let finalIndex = index;
if (!this.isKeyboardNavigation) {
// If the clicked box has a value, focus it
// Otherwise focus the first empty box
const targetIndex = this.inputValues[index] ? index : this.getFirstEmptyIndex();
finalIndex = targetIndex === -1 ? this.length - 1 : targetIndex;
// Focus the target box
(_a = this.inputRefs[finalIndex]) === null || _a === void 0 ? void 0 : _a.focus();
}
// Update tabIndexes to match the focused box
inputRefs.forEach((input, i) => {
input.tabIndex = i === finalIndex ? 0 : -1;
});
// Reset the keyboard navigation flag
this.isKeyboardNavigation = false;
};
/**
* Handles the blur behavior for the input OTP component.
* Emits the `ionBlur` event when the input group loses focus.
*/
this.onBlur = (event) => {
const { inputRefs } = this;
const relatedTarget = event.relatedTarget;
// Do not emit blur if we're moving to another input box in the same component
const isInternalFocus = relatedTarget != null && inputRefs.includes(relatedTarget);
if (!isInternalFocus) {
this.hasFocus = false;
// Reset tabIndexes when focus leaves the component
this.updateTabIndexes();
// Always emit ionBlur when focus leaves the component
this.ionBlur.emit(event);
// Only emit ionChange if the value has actually changed
if (this.focusedValue !== this.value) {
this.emitIonChange(event);
}
}
};
/**
* Handles keyboard navigation and input for the OTP component.
*
* Navigation:
* - Backspace: Clears current input and moves to previous box if empty
* - Arrow Left/Right: Moves focus between input boxes
* - Tab: Allows normal tab navigation between components
*
* Input Behavior:
* - Validates input against the allowed pattern
* - When entering a key in a filled box:
* - Shifts existing values right if there is room
* - Updates the value of the input group
* - Prevents default behavior to avoid automatic focus shift
*/
this.onKeyDown = (index) => (event) => {
const { length } = this;
const rtl = isRTL(this.el);
const input = event.target;
// Meta shortcuts are used to copy, paste, and select text
// We don't want to handle these keys here
const metaShortcuts = ['a', 'c', 'v', 'x', 'r', 'z', 'y'];
const isTextSelection = input.selectionStart !== input.selectionEnd;
// Return if the key is a meta shortcut or the input value
// text is selected and let the onPaste / onInput handler manage it
if (isTextSelection || ((event.metaKey || event.ctrlKey) && metaShortcuts.includes(event.key.toLowerCase()))) {
return;
}
if (event.key === 'Backspace') {
if (this.inputValues[index]) {
// Shift all values to the right of the current index left by one
for (let i = index; i < length - 1; i++) {
this.inputValues[i] = this.inputValues[i + 1];
}
// Clear the last box
this.inputValues[length - 1] = '';
// Update all inputRefs to match inputValues
for (let i = 0; i < length; i++) {
this.inputRefs[i].value = this.inputValues[i] || '';
}
this.updateValue(event);
event.preventDefault();
}
else if (!this.inputValues[index] && index > 0) {
// If current input is empty, move to previous input
this.focusPrevious(index);
}
}
else if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
this.isKeyboardNavigation = true;
event.preventDefault();
const isLeft = event.key === 'ArrowLeft';
const shouldMoveNext = (isLeft && rtl) || (!isLeft && !rtl);
// Only allow moving to the next input if the current has a value
if (shouldMoveNext) {
if (this.inputValues[index] && index < length - 1) {
this.focusNext(index);
}
}
else {
this.focusPrevious(index);
}
}
else if (event.key === 'Tab') {
this.isKeyboardNavigation = true;
// Let all tab events proceed normally
return;
}
// If the input box contains a value and the key being
// entered is a valid key for the input box update the value
// and shift the values to the right if there is room.
if (this.inputValues[index] && this.validKeyPattern.test(event.key)) {
if (!this.inputValues[length - 1]) {
for (let i = length - 1; i > index; i--) {
this.inputValues[i] = this.inputValues[i - 1];
this.inputRefs[i].value = this.inputValues[i] || '';
}
}
this.inputValues[index] = event.key;
this.inputRefs[index].value = event.key;
this.updateValue(event);
// Prevent default to avoid the browser from
// automatically moving the focus to the next input
event.preventDefault();
}
};
this.onInput = (index) => (event) => {
const { length, validKeyPattern } = this;
const value = event.target.value;
// If the value is longer than 1 character (autofill), split it into
// characters and filter out invalid ones
if (value.length > 1) {
const validChars = value
.split('')
.filter((char) => validKeyPattern.test(char))
.slice(0, length);
// If there are no valid characters coming from the
// autofill, all input refs have to be cleared after the
// browser has finished the autofill behavior
if (validChars.length === 0) {
requestAnimationFrame(() => {
this.inputRefs.forEach((input) => {
input.value = '';
});
});
}
// Update the value of the input group and emit the input change event
this.value = validChars.join('');
this.updateValue(event);
// Focus the first empty input box or the last input box if all boxes
// are filled after a small delay to ensure the input boxes have been
// updated before moving the focus
setTimeout(() => {
var _a;
const nextIndex = validChars.length < length ? validChars.length : length - 1;
(_a = this.inputRefs[nextIndex]) === null || _a === void 0 ? void 0 : _a.focus();
}, 20);
return;
}
// Only allow input if it matches the pattern
if (value.length > 0 && !validKeyPattern.test(value)) {
this.inputRefs[index].value = '';
this.inputValues[index] = '';
return;
}
// For single character input, fill the current box
this.inputValues[index] = value;
this.updateValue(event);
if (value.length > 0) {
this.focusNext(index);
}
};
/**
* Handles pasting text into the input OTP component.
* This function prevents the default paste behavior and
* validates the pasted text against the allowed pattern.
* It then updates the value of the input group and focuses
* the next empty input after pasting.
*/
this.onPaste = (event) => {
var _a, _b, _c;
const { inputRefs, length, validKeyPattern } = this;
event.preventDefault();
const pastedText = (_a = event.clipboardData) === null || _a === void 0 ? void 0 : _a.getData('text');
// If there is no pasted text, still emit the input change event
// because this is how the native input element behaves
// but return early because there is nothing to paste.
if (!pastedText) {
this.emitIonInput(event);
return;
}
const validChars = pastedText
.split('')
.filter((char) => validKeyPattern.test(char))
.slice(0, length);
// Always paste starting at the first box
validChars.forEach((char, index) => {
if (index < length) {
this.inputRefs[index].value = char;
this.inputValues[index] = char;
}
});
// Update the value so that all input boxes are updated
this.value = validChars.join('');
this.updateValue(event);
// Focus the next empty input after pasting
// If all boxes are filled, focus the last input
const nextEmptyIndex = validChars.length;
if (nextEmptyIndex < length) {
(_b = inputRefs[nextEmptyIndex]) === null || _b === void 0 ? void 0 : _b.focus();
}
else {
(_c = inputRefs[length - 1]) === null || _c === void 0 ? void 0 : _c.focus();
}
};
}
/**
* Sets focus to an input box.
* @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.
*/
async setFocus(index) {
var _a, _b;
if (typeof index === 'number') {
const validIndex = Math.max(0, Math.min(index, this.length - 1));
(_a = this.inputRefs[validIndex]) === null || _a === void 0 ? void 0 : _a.focus();
}
else {
const tabbableIndex = this.getTabbableIndex();
(_b = this.inputRefs[tabbableIndex]) === null || _b === void 0 ? void 0 : _b.focus();
}
}
valueChanged() {
this.initializeValues();
this.updateTabIndexes();
}
/**
* Processes the separators prop into an array of numbers.
*
* If the separators prop is not provided, returns an empty array.
* If the separators prop is 'all', returns an array of all valid positions (1 to length-1).
* If the separators prop is an array, returns it as is.
* If the separators prop is a string, splits it by commas and parses each part as a number.
*
* If the separators are greater than the input length, it will warn and ignore the separators.
*/
processSeparators() {
const { separators, length } = this;
if (separators === undefined) {
this.parsedSeparators = [];
return;
}
if (typeof separators === 'string' && separators !== 'all') {
const isValidFormat = /^(\d+)(,\d+)*$/.test(separators);
if (!isValidFormat) {
printIonWarning(`[ion-input-otp] - Invalid separators format. Expected a comma-separated list of numbers, an array of numbers, or "all". Received: ${separators}`, this.el);
this.parsedSeparators = [];
return;
}
}
let separatorValues;
if (separators === 'all') {
separatorValues = Array.from({ length: length - 1 }, (_, i) => i + 1);
}
else if (Array.isArray(separators)) {
separatorValues = separators;
}
else {
separatorValues = separators
.split(',')
.map((pos) => parseInt(pos, 10))
.filter((pos) => !isNaN(pos));
}
// Check for duplicate separator positions
const duplicates = separatorValues.filter((pos, index) => separatorValues.indexOf(pos) !== index);
if (duplicates.length > 0) {
printIonWarning(`[ion-input-otp] - Duplicate separator positions are not allowed. Received: ${separators}`, this.el);
}
const invalidSeparators = separatorValues.filter((pos) => pos > length);
if (invalidSeparators.length > 0) {
printIonWarning(`[ion-input-otp] - The following separator positions are greater than the input length (${length}): ${invalidSeparators.join(', ')}. These separators will be ignored.`, this.el);
}
this.parsedSeparators = separatorValues.filter((pos) => pos <= length);
}
componentWillLoad() {
this.inheritedAttributes = inheritAriaAttributes(this.el);
this.processSeparators();
this.initializeValues();
}
componentDidLoad() {
this.updateTabIndexes();
}
/**
* Get the regex pattern for allowed characters.
* If a pattern is provided, use it to create a regex pattern
* Otherwise, use the default regex pattern based on type
*/
get validKeyPattern() {
return new RegExp(`^${this.getPattern()}$`, 'u');
}
/**
* Gets the string pattern to pass to the input element
* and use in the regex for allowed characters.
*/
getPattern() {
const { pattern, type } = this;
if (pattern) {
return pattern;
}
return type === 'number' ? '[\\p{N}]' : '[\\p{L}\\p{N}]';
}
/**
* Get the default value for inputmode.
* If inputmode is provided, use it.
* Otherwise, use the default inputmode based on type
*/
getInputmode() {
const { inputmode } = this;
if (inputmode) {
return inputmode;
}
if (this.type == 'number') {
return 'numeric';
}
else {
return 'text';
}
}
/**
* Initializes the input values array based on the current value prop.
* This splits the value into individual characters and validates them against
* the allowed pattern. The values are then used as the values in the native
* input boxes and the value of the input group is updated.
*/
initializeValues() {
// Clear all input values
this.inputValues = Array(this.length).fill('');
// If the value is null, undefined, or an empty string, return
if (this.value == null || String(this.value).length === 0) {
return;
}
// Split the value into individual characters and validate
// them against the allowed pattern
const chars = String(this.value).split('').slice(0, this.length);
chars.forEach((char, index) => {
if (this.validKeyPattern.test(char)) {
this.inputValues[index] = char;
}
});
// Update the value without emitting events
this.value = this.inputValues.join('');
}
/**
* Updates the value of the input group.
* This updates the value of the input group and emits an `ionChange` event.
* If all of the input boxes are filled, it emits an `ionComplete` event.
*/
updateValue(event) {
const { inputValues, length } = this;
const newValue = inputValues.join('');
this.value = newValue;
this.emitIonInput(event);
if (newValue.length === length) {
this.ionComplete.emit({ value: newValue });
}
}
/**
* Emits an `ionChange` event.
* This API should be called for user committed changes.
* This API should not be used for external value changes.
*/
emitIonChange(event) {
const { value } = this;
// Checks for both null and undefined values
const newValue = value == null ? value : value.toString();
this.ionChange.emit({ value: newValue, event });
}
/**
* Emits an `ionInput` event.
* This is used to emit the input value when the user types,
* backspaces, or pastes.
*/
emitIonInput(event) {
const { value } = this;
// Checks for both null and undefined values
const newValue = value == null ? value : value.toString();
this.ionInput.emit({ value: newValue, event });
}
/**
* Focuses the next input box.
*/
focusNext(currentIndex) {
var _a;
const { inputRefs, length } = this;
if (currentIndex < length - 1) {
(_a = inputRefs[currentIndex + 1]) === null || _a === void 0 ? void 0 : _a.focus();
}
}
/**
* Focuses the previous input box.
*/
focusPrevious(currentIndex) {
var _a;
const { inputRefs } = this;
if (currentIndex > 0) {
(_a = inputRefs[currentIndex - 1]) === null || _a === void 0 ? void 0 : _a.focus();
}
}
/**
* Searches through the input values and returns the index
* of the first empty input.
* Returns -1 if all inputs are filled.
*/
getFirstEmptyIndex() {
var _a;
const { inputValues, length } = this;
// Create an array of the same length as the input OTP
// and fill it with the input values
const values = Array.from({ length }, (_, i) => inputValues[i] || '');
return (_a = values.findIndex((value) => !value || value === '')) !== null && _a !== void 0 ? _a : -1;
}
/**
* Returns the index of the input that should be tabbed to.
* If all inputs are filled, returns the last input's index.
* Otherwise, returns the index of the first empty input.
*/
getTabbableIndex() {
const { length } = this;
const firstEmptyIndex = this.getFirstEmptyIndex();
return firstEmptyIndex === -1 ? length - 1 : firstEmptyIndex;
}
/**
* Updates the tabIndexes for the input boxes.
* This is used to ensure that the correct input is
* focused when the user navigates using the tab key.
*/
updateTabIndexes() {
const { inputRefs, inputValues, length } = this;
// Find first empty index after any filled boxes
let firstEmptyIndex = -1;
for (let i = 0; i < length; i++) {
if (!inputValues[i] || inputValues[i] === '') {
firstEmptyIndex = i;
break;
}
}
// Update tabIndex and aria-hidden for all inputs
inputRefs.forEach((input, index) => {
const shouldBeTabbable = firstEmptyIndex === -1 ? index === length - 1 : firstEmptyIndex === index;
input.tabIndex = shouldBeTabbable ? 0 : -1;
// If the input is empty and not the first empty input,
// it should be hidden from screen readers.
const isEmpty = !inputValues[index] || inputValues[index] === '';
input.setAttribute('aria-hidden', isEmpty && !shouldBeTabbable ? 'true' : 'false');
});
}
/**
* Determines if a separator should be shown for a given index by
* checking if the index is included in the parsed separators array.
*/
showSeparator(index) {
const { length } = this;
return this.parsedSeparators.includes(index + 1) && index < length - 1;
}
render() {
var _a, _b;
const { autocapitalize, color, disabled, el, fill, hasFocus, inheritedAttributes, inputId, inputRefs, inputValues, length, readonly, shape, size, } = this;
const mode = getIonMode(this);
const inputmode = this.getInputmode();
const tabbableIndex = this.getTabbableIndex();
const pattern = this.getPattern();
const hasDescription = ((_b = (_a = el.querySelector('.input-otp-description')) === null || _a === void 0 ? void 0 : _a.textContent) === null || _b === void 0 ? void 0 : _b.trim()) !== '';
return (h(Host, { key: 'df8fca036cedea0812185a02e3b655d7d76285e0', class: createColorClasses(color, {
[mode]: true,
'has-focus': hasFocus,
[`input-otp-size-${size}`]: true,
[`input-otp-shape-${shape}`]: true,
[`input-otp-fill-${fill}`]: true,
'input-otp-disabled': disabled,
'input-otp-readonly': readonly,
}) }, h("div", Object.assign({ key: '831be3f939cf037f0eb8d7e37e0afd4ef9a3c2c5', role: "group", "aria-label": "One-time password input", class: "input-otp-group" }, inheritedAttributes), Array.from({ length }).map((_, index) => (h(Fragment, null, h("div", { class: "native-wrapper" }, h("input", { class: "native-input", id: `${inputId}-${index}`, "aria-label": `Input ${index + 1} of ${length}`, type: "text", autoCapitalize: autocapitalize, inputmode: inputmode, pattern: pattern, disabled: disabled, readOnly: readonly, tabIndex: index === tabbableIndex ? 0 : -1, value: inputValues[index] || '', autocomplete: "one-time-code", ref: (el) => (inputRefs[index] = el), onInput: this.onInput(index), onBlur: this.onBlur, onFocus: this.onFocus(index), onKeyDown: this.onKeyDown(index), onPaste: this.onPaste })), this.showSeparator(index) && h("div", { class: "input-otp-separator" }))))), h("div", { key: '5311fedc34f7af3efd5f69e5a3d768055119c4f1', class: {
'input-otp-description': true,
'input-otp-description-hidden': !hasDescription,
} }, h("slot", { key: '9e8afa2f7fa76c3092582dc27770fdf565a1b9ba' }))));
}
get el() { return this; }
static get watchers() { return {
"value": ["valueChanged"],
"separators": ["processSeparators"],
"length": ["processSeparators"]
}; }
static get style() { return {
ios: inputOtpIosCss,
md: inputOtpMdCss
}; }
}, [38, "ion-input-otp", {
"autocapitalize": [1],
"color": [513],
"disabled": [516],
"fill": [1],
"inputmode": [1],
"length": [2],
"pattern": [1],
"readonly": [516],
"separators": [1],
"shape": [1],
"size": [1],
"type": [1],
"value": [1032],
"inputValues": [32],
"hasFocus": [32],
"setFocus": [64]
}, undefined, {
"value": ["valueChanged"],
"separators": ["processSeparators"],
"length": ["processSeparators"]
}]);
let inputIds = 0;
function defineCustomElement$1() {
if (typeof customElements === "undefined") {
return;
}
const components = ["ion-input-otp"];
components.forEach(tagName => { switch (tagName) {
case "ion-input-otp":
if (!customElements.get(tagName)) {
customElements.define(tagName, InputOTP);
}
break;
} });
}
const IonInputOtp = InputOTP;
const defineCustomElement = defineCustomElement$1;
export { IonInputOtp, defineCustomElement };