Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion core/src/components/input-otp/input-otp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)}
Expand Down
21 changes: 21 additions & 0 deletions core/src/components/input-otp/test/a11y/input-otp.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(`<ion-input-otp length="2"></ion-input-otp>`, 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(`<ion-input-otp></ion-input-otp>`, config);

Expand Down
63 changes: 63 additions & 0 deletions core/src/components/input-otp/test/basic/input-otp.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(`<ion-input-otp length="4" value="1234">Description</ion-input-otp>`, 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(`<ion-input-otp length="2" value="12">Description</ion-input-otp>`, 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(`<ion-input-otp>Description</ion-input-otp>`, config);

Expand Down Expand Up @@ -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(`<ion-input-otp length="4">Description</ion-input-otp>`, 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(`<ion-input-otp value="12">Description</ion-input-otp>`, config);

Expand Down
Loading