diff --git a/core/src/components/input-otp/input-otp.tsx b/core/src/components/input-otp/input-otp.tsx
index 7c46ca59d42..1c84b3c9a42 100644
--- a/core/src/components/input-otp/input-otp.tsx
+++ b/core/src/components/input-otp/input-otp.tsx
@@ -43,6 +43,12 @@ export class InputOTP implements ComponentInterface {
*/
private isKeyboardNavigation = false;
+ /**
+ * The `length` watcher runs before new inputs render, so
+ * `componentDidRender` updates their tab indexes instead.
+ */
+ private updateTabIndexesAfterRender = false;
+
@Element() el!: HTMLIonInputOtpElement;
@State() private inputValues: string[] = [];
@@ -197,6 +203,12 @@ export class InputOTP implements ComponentInterface {
this.updateTabIndexes();
}
+ @Watch('length')
+ lengthChanged() {
+ this.initializeValues();
+ this.updateTabIndexesAfterRender = true;
+ }
+
/**
* Processes the separators prop into an array of numbers.
*
@@ -272,6 +284,13 @@ export class InputOTP implements ComponentInterface {
this.updateTabIndexes();
}
+ componentDidRender() {
+ if (this.updateTabIndexesAfterRender) {
+ this.updateTabIndexesAfterRender = false;
+ this.updateTabIndexes();
+ }
+ }
+
/**
* Get the regex pattern for allowed characters.
* If a pattern is provided, use it to create a regex pattern
@@ -851,7 +870,14 @@ export class InputOTP implements ComponentInterface {
tabIndex={index === tabbableIndex ? 0 : -1}
value={inputValues[index] || ''}
autocomplete="one-time-code"
- ref={(el) => (inputRefs[index] = el as HTMLInputElement)}
+ ref={(el) => {
+ if (el) {
+ inputRefs[index] = el as HTMLInputElement;
+ } else {
+ // The input was removed, so drop its ref and any after it.
+ inputRefs.splice(index);
+ }
+ }}
onInput={this.onInput(index)}
onBlur={this.onBlur}
onFocus={this.onFocus(index)}
diff --git a/core/src/components/input-otp/test/a11y/input-otp.e2e.ts b/core/src/components/input-otp/test/a11y/input-otp.e2e.ts
index 88b0be04800..7cc5374d8ef 100644
--- a/core/src/components/input-otp/test/a11y/input-otp.e2e.ts
+++ b/core/src/components/input-otp/test/a11y/input-otp.e2e.ts
@@ -46,6 +46,27 @@ configs().forEach(({ title, config }) => {
await expect(inputBoxes.nth(3)).toHaveAttribute('aria-hidden', 'true');
});
+ test('should update aria-hidden when length increases', async ({ page }, testInfo) => {
+ testInfo.annotations.push({
+ type: 'issue',
+ description: 'https://github.com/ionic-team/ionic-framework/issues/31484',
+ });
+
+ await page.setContent(``, config);
+
+ const inputOtp = page.locator('ion-input-otp');
+ await inputOtp.evaluate((el: HTMLIonInputOtpElement) => {
+ el.length = 4;
+ });
+
+ const inputBoxes = page.locator('ion-input-otp input');
+
+ await expect(inputBoxes.nth(0)).toHaveAttribute('aria-hidden', 'false');
+ await expect(inputBoxes.nth(1)).toHaveAttribute('aria-hidden', 'true');
+ await expect(inputBoxes.nth(2)).toHaveAttribute('aria-hidden', 'true');
+ await expect(inputBoxes.nth(3)).toHaveAttribute('aria-hidden', 'true');
+ });
+
test('should update aria-hidden when typing a value', async ({ page }) => {
await page.setContent(``, config);
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 306e72f873d..a135f502f0a 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
@@ -71,6 +71,39 @@ configs({ modes: ['ios'] }).forEach(({ title, config }) => {
await verifyInputValues(inputOtp, ['1', '2', '3', '4', '5', '6', '7', '8']);
});
+ test('should synchronize the value when length changes', async ({ page }, testInfo) => {
+ testInfo.annotations.push({
+ type: 'issue',
+ description: 'https://github.com/ionic-team/ionic-framework/issues/31484',
+ });
+
+ await page.setContent(`Description`, config);
+
+ const inputOtp = page.locator('ion-input-otp');
+ await inputOtp.evaluate((el) => el.setAttribute('length', '2'));
+
+ await verifyInputValues(inputOtp, ['1', '2']);
+ });
+
+ test('should add empty input boxes when length increases', async ({ page }, testInfo) => {
+ testInfo.annotations.push({
+ type: 'issue',
+ description: 'https://github.com/ionic-team/ionic-framework/issues/31484',
+ });
+
+ await page.setContent(`Description`, config);
+
+ const inputOtp = page.locator('ion-input-otp');
+ await inputOtp.evaluate((el: HTMLIonInputOtpElement) => {
+ el.length = 4;
+ });
+
+ const inputBoxes = page.locator('ion-input-otp input');
+ await expect(inputBoxes).toHaveCount(4);
+
+ await verifyInputValues(inputOtp, ['1', '2', '', '']);
+ });
+
test('should accept numbers only by default', async ({ page }) => {
await page.setContent(`Description`, config);
@@ -1033,6 +1066,36 @@ configs({ modes: ['ios'], directions: ['ltr'] }).forEach(({ title, config }) =>
await expect(ionChange).toHaveReceivedEventTimes(1);
});
+ test('should emit ionChange event when blurring with a new value after length decreases', async ({
+ page,
+ }, testInfo) => {
+ testInfo.annotations.push({
+ type: 'issue',
+ description: 'https://github.com/ionic-team/ionic-framework/issues/31484',
+ });
+
+ await page.setContent(`Description`, config);
+
+ const ionChange = await page.spyOnEvent('ionChange');
+
+ const inputOtp = page.locator('ion-input-otp');
+ await inputOtp.evaluate((el: HTMLIonInputOtpElement) => {
+ el.length = 2;
+ });
+ await page.waitForChanges();
+
+ const firstInput = page.locator('ion-input-otp input').first();
+ await firstInput.focus();
+
+ await page.keyboard.type('12');
+
+ // Click outside the input to trigger the blur event
+ await page.mouse.click(0, 0);
+
+ await ionChange.next();
+ await expect(ionChange).toHaveReceivedEventDetail({ value: '12', event: { isTrusted: true } });
+ });
+
test('should not emit ionChange event when blurring with the same value', async ({ page }) => {
await page.setContent(`Description`, config);