From d1721104f947a8023769f4b72321856a92aa9e58 Mon Sep 17 00:00:00 2001 From: KanhaiyaPandey Date: Sun, 1 Mar 2026 13:20:26 +0530 Subject: [PATCH 1/5] fix(input-otp): prevent deletion when readonly is true --- core/src/components/input-otp/input-otp.tsx | 19 ++++++++++++++++++- .../input-otp/test/basic/input-otp.e2e.ts | 15 +++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/core/src/components/input-otp/input-otp.tsx b/core/src/components/input-otp/input-otp.tsx index a93eabd926d..2270ca6407d 100644 --- a/core/src/components/input-otp/input-otp.tsx +++ b/core/src/components/input-otp/input-otp.tsx @@ -535,10 +535,17 @@ export class InputOTP implements ComponentInterface { * - Tab: Allows normal tab navigation between components */ private onKeyDown = (index: number) => (event: KeyboardEvent) => { - const { length } = this; + const { length, readonly } = this; const rtl = isRTL(this.el); const input = event.target as HTMLInputElement; + if (readonly) { + if (event.key === 'Backspace' || event.key === 'Delete') { + event.preventDefault(); + } + return; + } + // 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']; @@ -603,6 +610,11 @@ export class InputOTP implements ComponentInterface { * 5. Single character replacement */ private onInput = (index: number) => (event: InputEvent) => { + if (this.readonly) { + event.preventDefault(); + return; + } + const { length, validKeyPattern } = this; const input = event.target as HTMLInputElement; const value = input.value; @@ -735,6 +747,11 @@ export class InputOTP implements ComponentInterface { * the next empty input after pasting. */ private onPaste = (event: ClipboardEvent) => { + if (this.readonly) { + event.preventDefault(); + return; + } + const { inputRefs, length, validKeyPattern } = this; event.preventDefault(); 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 2067a000209..1f4d72daa71 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 @@ -690,6 +690,21 @@ configs({ modes: ['ios'] }).forEach(({ title, config }) => { }); test.describe(title('input-otp: backspace functionality'), () => { + test('should not modify values with Backspace or Delete when readonly', async ({ page }) => { + await page.setContent(`Description`, config); + + const inputOtp = page.locator('ion-input-otp'); + const ionInput = await page.spyOnEvent('ionInput'); + + await page.keyboard.press('Tab'); + await page.keyboard.press('Backspace'); + await page.keyboard.press('Delete'); + await page.keyboard.type('5'); + + await verifyInputValues(inputOtp, ['1', '2', '3', '4']); + await expect(ionInput).not.toHaveReceivedEvent(); + }); + test('should backspace the first input box when backspace is pressed twice from the 2nd input box and the first input box has a value', async ({ page, }) => { From faed22a942c4c82b7f5b4c5c49b32dc3c34f0caa Mon Sep 17 00:00:00 2001 From: Brandy Smith <6577830+brandyscarney@users.noreply.github.com> Date: Wed, 25 Mar 2026 12:48:08 -0400 Subject: [PATCH 2/5] fix(input-otp): allow navigating with arrow keys and copying on readonly --- core/src/components/input-otp/input-otp.tsx | 25 ++++++++++----------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/core/src/components/input-otp/input-otp.tsx b/core/src/components/input-otp/input-otp.tsx index 2270ca6407d..01bfb4b2d80 100644 --- a/core/src/components/input-otp/input-otp.tsx +++ b/core/src/components/input-otp/input-otp.tsx @@ -542,8 +542,8 @@ export class InputOTP implements ComponentInterface { if (readonly) { if (event.key === 'Backspace' || event.key === 'Delete') { event.preventDefault(); + return; } - return; } // Meta shortcuts are used to copy, paste, and select text @@ -610,16 +610,16 @@ export class InputOTP implements ComponentInterface { * 5. Single character replacement */ private onInput = (index: number) => (event: InputEvent) => { - if (this.readonly) { - event.preventDefault(); - return; - } - - const { length, validKeyPattern } = this; + const { length, readonly, validKeyPattern } = this; const input = event.target as HTMLInputElement; const value = input.value; const previousValue = this.previousInputValues[index] || ''; + if (readonly) { + event.preventDefault(); + return; + } + // 1. Autofill handling // If the length of the value increases by more than 1 from the previous // value, treat this as autofill. This is to prevent the case where the @@ -747,15 +747,14 @@ export class InputOTP implements ComponentInterface { * the next empty input after pasting. */ private onPaste = (event: ClipboardEvent) => { - if (this.readonly) { - event.preventDefault(); - return; - } - - const { inputRefs, length, validKeyPattern } = this; + const { inputRefs, length, readonly, validKeyPattern } = this; event.preventDefault(); + if (readonly) { + return; + } + const pastedText = event.clipboardData?.getData('text'); // If there is no pasted text, still emit the input change event From ceaeca829cd730b1865179bdda74bbd3c5db181d Mon Sep 17 00:00:00 2001 From: Brandy Smith <6577830+brandyscarney@users.noreply.github.com> Date: Wed, 25 Mar 2026 12:49:09 -0400 Subject: [PATCH 3/5] test(input-otp): add more test coverage for paste and arrow key functionality --- .../input-otp/test/basic/input-otp.e2e.ts | 54 +++++++++++++++---- 1 file changed, 45 insertions(+), 9 deletions(-) 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 1f4d72daa71..1389a86f0a5 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 @@ -687,24 +687,34 @@ configs({ modes: ['ios'] }).forEach(({ title, config }) => { const firstInput = page.locator('ion-input-otp input').first(); await expect(firstInput).toBeFocused(); }); - }); - test.describe(title('input-otp: backspace functionality'), () => { - test('should not modify values with Backspace or Delete when readonly', async ({ page }) => { + test('should allow arrow key navigation when readonly is true', async ({ page }) => { await page.setContent(`Description`, config); const inputOtp = page.locator('ion-input-otp'); - const ionInput = await page.spyOnEvent('ionInput'); + const inputBoxes = page.locator('ion-input-otp input'); + await inputBoxes.nth(1).focus(); - await page.keyboard.press('Tab'); - await page.keyboard.press('Backspace'); - await page.keyboard.press('Delete'); - await page.keyboard.type('5'); + const isRTL = await page.evaluate(() => document.dir === 'rtl'); + if (isRTL) { + await page.keyboard.press('ArrowLeft'); + await expect(inputBoxes.nth(2)).toBeFocused(); + + await page.keyboard.press('ArrowRight'); + await expect(inputBoxes.nth(1)).toBeFocused(); + } else { + await page.keyboard.press('ArrowRight'); + await expect(inputBoxes.nth(2)).toBeFocused(); + + await page.keyboard.press('ArrowLeft'); + await expect(inputBoxes.nth(1)).toBeFocused(); + } await verifyInputValues(inputOtp, ['1', '2', '3', '4']); - await expect(ionInput).not.toHaveReceivedEvent(); }); + }); + test.describe(title('input-otp: backspace functionality'), () => { test('should backspace the first input box when backspace is pressed twice from the 2nd input box and the first input box has a value', async ({ page, }) => { @@ -752,6 +762,21 @@ configs({ modes: ['ios'] }).forEach(({ title, config }) => { const inputOtp = page.locator('ion-input-otp'); await verifyInputValues(inputOtp, ['1', '3', '', '']); }); + + test('should not modify values with backspace or delete when readonly is true', async ({ page }) => { + await page.setContent(`Description`, config); + + const inputOtp = page.locator('ion-input-otp'); + const ionInput = await page.spyOnEvent('ionInput'); + + await page.keyboard.press('Tab'); + await page.keyboard.press('Backspace'); + await page.keyboard.press('Delete'); + await page.keyboard.type('5'); + + await verifyInputValues(inputOtp, ['1', '2', '3', '4']); + await expect(ionInput).not.toHaveReceivedEvent(); + }); }); test.describe(title('input-otp: paste functionality'), () => { @@ -843,6 +868,17 @@ configs({ modes: ['ios'] }).forEach(({ title, config }) => { await verifyInputValues(inputOtp, ['أ', 'ب', 'ج', 'د', '1', '2']); }); + + test('should not paste text when readonly is true', async ({ page }) => { + await page.setContent(`Description`, config); + + const firstInput = page.locator('ion-input-otp input').first(); + await firstInput.focus(); + await simulatePaste(firstInput, '5678'); + + const inputOtp = page.locator('ion-input-otp'); + await verifyInputValues(inputOtp, ['1', '2', '3', '4']); + }); }); }); From 8bf26df5fc99538c4779ed98ab1b825dd520548c Mon Sep 17 00:00:00 2001 From: Brandy Smith <6577830+brandyscarney@users.noreply.github.com> Date: Wed, 25 Mar 2026 13:12:01 -0400 Subject: [PATCH 4/5] fix(input-otp): also check for disabled on paste and input --- core/src/components/input-otp/input-otp.tsx | 14 +++++++++----- .../input-otp/test/basic/input-otp.e2e.ts | 11 +++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/core/src/components/input-otp/input-otp.tsx b/core/src/components/input-otp/input-otp.tsx index 01bfb4b2d80..95d7454fce1 100644 --- a/core/src/components/input-otp/input-otp.tsx +++ b/core/src/components/input-otp/input-otp.tsx @@ -535,10 +535,14 @@ export class InputOTP implements ComponentInterface { * - Tab: Allows normal tab navigation between components */ private onKeyDown = (index: number) => (event: KeyboardEvent) => { - const { length, readonly } = this; + const { disabled, length, readonly } = this; const rtl = isRTL(this.el); const input = event.target as HTMLInputElement; + if (disabled) { + return; + } + if (readonly) { if (event.key === 'Backspace' || event.key === 'Delete') { event.preventDefault(); @@ -610,12 +614,12 @@ export class InputOTP implements ComponentInterface { * 5. Single character replacement */ private onInput = (index: number) => (event: InputEvent) => { - const { length, readonly, validKeyPattern } = this; + const { disabled, length, readonly, validKeyPattern } = this; const input = event.target as HTMLInputElement; const value = input.value; const previousValue = this.previousInputValues[index] || ''; - if (readonly) { + if (disabled || readonly) { event.preventDefault(); return; } @@ -747,11 +751,11 @@ export class InputOTP implements ComponentInterface { * the next empty input after pasting. */ private onPaste = (event: ClipboardEvent) => { - const { inputRefs, length, readonly, validKeyPattern } = this; + const { disabled, inputRefs, length, readonly, validKeyPattern } = this; event.preventDefault(); - if (readonly) { + if (disabled || readonly) { return; } 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 1389a86f0a5..306e72f873d 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 @@ -869,6 +869,17 @@ configs({ modes: ['ios'] }).forEach(({ title, config }) => { await verifyInputValues(inputOtp, ['أ', 'ب', 'ج', 'د', '1', '2']); }); + test('should not paste text when disabled is true', async ({ page }) => { + await page.setContent(`Description`, config); + + const firstInput = page.locator('ion-input-otp input').first(); + await firstInput.focus(); + await simulatePaste(firstInput, '5678'); + + const inputOtp = page.locator('ion-input-otp'); + await verifyInputValues(inputOtp, ['1', '2', '3', '4']); + }); + test('should not paste text when readonly is true', async ({ page }) => { await page.setContent(`Description`, config); From c6bb32cabd68110300e388a32dbe007d38a1b0aa Mon Sep 17 00:00:00 2001 From: Brandy Smith <6577830+brandyscarney@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:44:50 -0400 Subject: [PATCH 5/5] refactor(input-otp): remove preventDefault from onInput --- core/src/components/input-otp/input-otp.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/components/input-otp/input-otp.tsx b/core/src/components/input-otp/input-otp.tsx index 95d7454fce1..c6b4f394599 100644 --- a/core/src/components/input-otp/input-otp.tsx +++ b/core/src/components/input-otp/input-otp.tsx @@ -620,7 +620,6 @@ export class InputOTP implements ComponentInterface { const previousValue = this.previousInputValues[index] || ''; if (disabled || readonly) { - event.preventDefault(); return; }