diff --git a/core/src/components/input-otp/input-otp.tsx b/core/src/components/input-otp/input-otp.tsx
index 55c52d6ee91..3e6cc3855b2 100644
--- a/core/src/components/input-otp/input-otp.tsx
+++ b/core/src/components/input-otp/input-otp.tsx
@@ -544,12 +544,14 @@ export class InputOTP implements ComponentInterface {
const rtl = isRTL(this.el);
const input = event.target as HTMLInputElement;
- const isPasteShortcut = (event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'v';
+ // 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 the paste shortcut or the input value
+ // Return if the key is a meta shortcut or the input value
// text is selected and let the onPaste / onInput handler manage it
- if (isPasteShortcut || isTextSelection) {
+ if (isTextSelection || ((event.metaKey || event.ctrlKey) && metaShortcuts.includes(event.key.toLowerCase()))) {
return;
}
@@ -615,39 +617,57 @@ export class InputOTP implements ComponentInterface {
};
private onInput = (index: number) => (event: InputEvent) => {
- const { validKeyPattern } = this;
-
+ const { length, validKeyPattern } = this;
const value = (event.target as HTMLInputElement).value;
- // Only allow input if it's a single character and matches the pattern
- if (value.length > 1 || (value.length > 0 && !validKeyPattern.test(value))) {
- // Reset the input value if not valid
- this.inputRefs[index].value = '';
- this.inputValues[index] = '';
- return;
- }
-
- // Find the first empty box before or at the current index
- let targetIndex = index;
- for (let i = 0; i < index; i++) {
- if (!this.inputValues[i] || this.inputValues[i] === '') {
- targetIndex = i;
- break;
+ // 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 = '';
+ });
+ });
}
- }
- // Set the value at the target index
- this.inputValues[targetIndex] = 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(() => {
+ const nextIndex = validChars.length < length ? validChars.length : length - 1;
+ this.inputRefs[nextIndex]?.focus();
+ }, 20);
- // If the value was entered in a later box, clear the current box
- if (targetIndex !== index) {
+ 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(targetIndex);
+ this.focusNext(index);
}
- this.updateValue(event);
};
/**
@@ -754,13 +774,12 @@ export class InputOTP implements ComponentInterface {
type="text"
autoCapitalize={autocapitalize}
inputmode={inputmode}
- maxLength={1}
pattern={pattern}
disabled={disabled}
readOnly={readonly}
tabIndex={index === tabbableIndex ? 0 : -1}
value={inputValues[index] || ''}
- autocomplete={index === 0 ? 'one-time-code' : 'off'}
+ autocomplete="one-time-code"
ref={(el) => (inputRefs[index] = el as HTMLInputElement)}
onInput={this.onInput(index)}
onBlur={this.onBlur}
diff --git a/core/src/components/input-otp/test/basic/input-otp.e2e.ts b/core/src/components/input-otp/test/basic/input-otp.e2e.ts
index 3dfb627589a..2a50c1abd5c 100644
--- a/core/src/components/input-otp/test/basic/input-otp.e2e.ts
+++ b/core/src/components/input-otp/test/basic/input-otp.e2e.ts
@@ -2,6 +2,16 @@ import { expect } from '@playwright/test';
import type { Locator } from '@playwright/test';
import { configs, test } from '@utils/test/playwright';
+/**
+ * Simulates an autofill event in an input element with the given value
+ */
+async function simulateAutofill(input: any, value: string) {
+ await input.evaluate((input: any, value: string) => {
+ (input as HTMLInputElement).value = value;
+ input.dispatchEvent(new Event('input', { bubbles: true }));
+ }, value);
+}
+
/**
* Simulates a paste event in an input element with the given value
*/
@@ -334,7 +344,10 @@ configs({ modes: ['ios'] }).forEach(({ title, config }) => {
const firstInput = page.locator('ion-input-otp input').first();
await firstInput.focus();
- await page.keyboard.type('أبجد123');
+ // We need to type the numbers separately because the browser
+ // does not properly handle the script text when mixed with numbers
+ await page.keyboard.type('123');
+ await page.keyboard.type('أبجد');
// Because Arabic is a right-to-left script, JavaScript's handling of RTL text
// causes the array values to be reversed while input boxes maintain LTR order.
@@ -431,6 +444,87 @@ configs({ modes: ['ios'] }).forEach(({ title, config }) => {
});
});
+ test.describe(title('input-otp: autofill functionality'), () => {
+ test('should handle autofill correctly', async ({ page }) => {
+ await page.setContent(`Description`, config);
+
+ const firstInput = page.locator('ion-input-otp input').first();
+ await firstInput.focus();
+
+ await simulateAutofill(firstInput, '1234');
+
+ const inputOtp = page.locator('ion-input-otp');
+ await verifyInputValues(inputOtp, ['1', '2', '3', '4']);
+
+ const lastInput = page.locator('ion-input-otp input').last();
+ await expect(lastInput).toBeFocused();
+ });
+
+ test('should handle autofill correctly when it exceeds the length', async ({ page }) => {
+ await page.setContent(`Description`, config);
+
+ const firstInput = page.locator('ion-input-otp input').first();
+ await firstInput.focus();
+
+ await simulateAutofill(firstInput, '123456');
+
+ const inputOtp = page.locator('ion-input-otp');
+ await verifyInputValues(inputOtp, ['1', '2', '3', '4']);
+
+ const lastInput = page.locator('ion-input-otp input').last();
+ await expect(lastInput).toBeFocused();
+ });
+
+ test('should handle autofill correctly when it is less than the length', async ({ page }) => {
+ await page.setContent(`Description`, config);
+
+ const firstInput = page.locator('ion-input-otp input').first();
+ await firstInput.focus();
+
+ await simulateAutofill(firstInput, '12');
+
+ const inputOtp = page.locator('ion-input-otp');
+ await verifyInputValues(inputOtp, ['1', '2', '', '']);
+
+ const thirdInput = page.locator('ion-input-otp input').nth(2);
+ await expect(thirdInput).toBeFocused();
+ });
+
+ test('should handle autofill correctly when using autofill after typing 1 character', async ({ page }) => {
+ await page.setContent(`Description`, config);
+
+ const firstInput = page.locator('ion-input-otp input').first();
+ await firstInput.focus();
+
+ await page.keyboard.type('9');
+
+ const secondInput = page.locator('ion-input-otp input').nth(1);
+ await secondInput.focus();
+
+ await simulateAutofill(secondInput, '1234');
+
+ const inputOtp = page.locator('ion-input-otp');
+ await verifyInputValues(inputOtp, ['1', '2', '3', '4']);
+
+ const lastInput = page.locator('ion-input-otp input').last();
+ await expect(lastInput).toBeFocused();
+ });
+
+ test('should handle autofill correctly when autofill value contains invalid characters', async ({ page }) => {
+ await page.setContent(`Description`, config);
+
+ const firstInput = page.locator('ion-input-otp input').first();
+ await firstInput.focus();
+
+ await simulateAutofill(firstInput, '1234');
+
+ const inputOtp = page.locator('ion-input-otp');
+ await verifyInputValues(inputOtp, ['', '', '', '']);
+
+ await expect(firstInput).toBeFocused();
+ });
+ });
+
test.describe(title('input-otp: focus functionality'), () => {
test('should focus the first input box when tabbed to', async ({ page }) => {
await page.setContent(`Description`, config);
@@ -614,6 +708,18 @@ configs({ modes: ['ios'] }).forEach(({ title, config }) => {
await verifyInputValues(inputOtp, ['1', '2', '3', '4']);
});
+
+ test('should paste mixed language text into all input boxes', async ({ page }) => {
+ await page.setContent(`Description`, config);
+
+ const firstInput = page.locator('ion-input-otp input').first();
+ await firstInput.focus();
+ await simulatePaste(firstInput, 'أبجد123');
+
+ const inputOtp = page.locator('ion-input-otp');
+
+ await verifyInputValues(inputOtp, ['أ', 'ب', 'ج', 'د', '1', '2']);
+ });
});
});