diff --git a/.github/workflows/assign-issues.yml b/.github/workflows/assign-issues.yml
index d06c1f52e10..4608d2323dd 100644
--- a/.github/workflows/assign-issues.yml
+++ b/.github/workflows/assign-issues.yml
@@ -13,6 +13,6 @@ jobs:
- name: 'Auto-assign issue'
uses: pozil/auto-assign-issue@39c06395cbac76e79afc4ad4e5c5c6db6ecfdd2e # v2.2.0
with:
- assignees: brandyscarney, thetaPC, ShaneK
+ assignees: brandyscarney, ShaneK
numOfAssignee: 1
allowSelfAssign: false
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 144a9ce9cd3..a7120fcc50c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,23 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+## [8.6.5](https://github.com/ionic-team/ionic-framework/compare/v8.6.4...v8.6.5) (2025-07-16)
+
+
+### Bug Fixes
+
+* **input-otp:** improve autofill detection and invalid character handling ([#30541](https://github.com/ionic-team/ionic-framework/issues/30541)) ([8b4023d](https://github.com/ionic-team/ionic-framework/commit/8b4023d520212c254395a5be6d3a76dcbee6f2da)), closes [#30459](https://github.com/ionic-team/ionic-framework/issues/30459)
+* **input:** prevent layout shift when hiding password toggle ([#30533](https://github.com/ionic-team/ionic-framework/issues/30533)) ([f1defba](https://github.com/ionic-team/ionic-framework/commit/f1defba2acb417c6f243b2902923d85efbb6f879)), closes [#29562](https://github.com/ionic-team/ionic-framework/issues/29562)
+* **item:** allow nested content to be conditionally interactive ([#30519](https://github.com/ionic-team/ionic-framework/issues/30519)) ([3f730ab](https://github.com/ionic-team/ionic-framework/commit/3f730ab1d77be54d1faf14168eee9e9dc41002d6)), closes [#29763](https://github.com/ionic-team/ionic-framework/issues/29763)
+* **modal:** dismiss child modals when parent is dismissed ([#30540](https://github.com/ionic-team/ionic-framework/issues/30540)) ([9b0099f](https://github.com/ionic-team/ionic-framework/commit/9b0099f462fda6d40b49dde1a1c97afbbbee2287)), closes [#30389](https://github.com/ionic-team/ionic-framework/issues/30389)
+* **modal:** dismiss modal when parent element is removed from DOM ([#30544](https://github.com/ionic-team/ionic-framework/issues/30544)) ([850338c](https://github.com/ionic-team/ionic-framework/commit/850338cbd5c76addbc2cc3068b93071dea14c0af)), closes [#30389](https://github.com/ionic-team/ionic-framework/issues/30389)
+* **modal:** improve card modal background transition from portrait to landscape ([#30551](https://github.com/ionic-team/ionic-framework/issues/30551)) ([d37b9b8](https://github.com/ionic-team/ionic-framework/commit/d37b9b8e468b7b2c9cda8b27fe7019bb905ad2bf))
+* **segment-view:** scroll to correct content when height is not set ([#30547](https://github.com/ionic-team/ionic-framework/issues/30547)) ([d14311f](https://github.com/ionic-team/ionic-framework/commit/d14311fb65ae3de7ba7578791ce1ea44f186c413)), closes [#30543](https://github.com/ionic-team/ionic-framework/issues/30543)
+
+
+
+
+
## [8.6.4](https://github.com/ionic-team/ionic-framework/compare/v8.6.3...v8.6.4) (2025-07-09)
diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md
index 3c8d4dca3f6..0bd1d8a9288 100644
--- a/core/CHANGELOG.md
+++ b/core/CHANGELOG.md
@@ -3,6 +3,23 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+## [8.6.5](https://github.com/ionic-team/ionic-framework/compare/v8.6.4...v8.6.5) (2025-07-16)
+
+
+### Bug Fixes
+
+* **input-otp:** improve autofill detection and invalid character handling ([#30541](https://github.com/ionic-team/ionic-framework/issues/30541)) ([8b4023d](https://github.com/ionic-team/ionic-framework/commit/8b4023d520212c254395a5be6d3a76dcbee6f2da)), closes [#30459](https://github.com/ionic-team/ionic-framework/issues/30459)
+* **input:** prevent layout shift when hiding password toggle ([#30533](https://github.com/ionic-team/ionic-framework/issues/30533)) ([f1defba](https://github.com/ionic-team/ionic-framework/commit/f1defba2acb417c6f243b2902923d85efbb6f879)), closes [#29562](https://github.com/ionic-team/ionic-framework/issues/29562)
+* **item:** allow nested content to be conditionally interactive ([#30519](https://github.com/ionic-team/ionic-framework/issues/30519)) ([3f730ab](https://github.com/ionic-team/ionic-framework/commit/3f730ab1d77be54d1faf14168eee9e9dc41002d6)), closes [#29763](https://github.com/ionic-team/ionic-framework/issues/29763)
+* **modal:** dismiss child modals when parent is dismissed ([#30540](https://github.com/ionic-team/ionic-framework/issues/30540)) ([9b0099f](https://github.com/ionic-team/ionic-framework/commit/9b0099f462fda6d40b49dde1a1c97afbbbee2287)), closes [#30389](https://github.com/ionic-team/ionic-framework/issues/30389)
+* **modal:** dismiss modal when parent element is removed from DOM ([#30544](https://github.com/ionic-team/ionic-framework/issues/30544)) ([850338c](https://github.com/ionic-team/ionic-framework/commit/850338cbd5c76addbc2cc3068b93071dea14c0af)), closes [#30389](https://github.com/ionic-team/ionic-framework/issues/30389)
+* **modal:** improve card modal background transition from portrait to landscape ([#30551](https://github.com/ionic-team/ionic-framework/issues/30551)) ([d37b9b8](https://github.com/ionic-team/ionic-framework/commit/d37b9b8e468b7b2c9cda8b27fe7019bb905ad2bf))
+* **segment-view:** scroll to correct content when height is not set ([#30547](https://github.com/ionic-team/ionic-framework/issues/30547)) ([d14311f](https://github.com/ionic-team/ionic-framework/commit/d14311fb65ae3de7ba7578791ce1ea44f186c413)), closes [#30543](https://github.com/ionic-team/ionic-framework/issues/30543)
+
+
+
+
+
## [8.6.4](https://github.com/ionic-team/ionic-framework/compare/v8.6.3...v8.6.4) (2025-07-09)
diff --git a/core/Dockerfile b/core/Dockerfile
index 50bce82ff07..5f24265654f 100644
--- a/core/Dockerfile
+++ b/core/Dockerfile
@@ -1,5 +1,5 @@
# Get Playwright
-FROM mcr.microsoft.com/playwright:v1.53.1
+FROM mcr.microsoft.com/playwright:v1.54.1
# Set the working directory
WORKDIR /ionic
diff --git a/core/package-lock.json b/core/package-lock.json
index 2af538f7f89..14b15f2b403 100644
--- a/core/package-lock.json
+++ b/core/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@ionic/core",
- "version": "8.6.4",
+ "version": "8.6.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@ionic/core",
- "version": "8.6.3",
+ "version": "8.6.5",
"license": "MIT",
"dependencies": {
"@phosphor-icons/core": "^2.1.1",
@@ -23,7 +23,7 @@
"@clack/prompts": "^0.11.0",
"@ionic/eslint-config": "^0.3.0",
"@ionic/prettier-config": "^2.0.0",
- "@playwright/test": "^1.53.2",
+ "@playwright/test": "^1.54.1",
"@rollup/plugin-node-resolve": "^8.4.0",
"@rollup/plugin-virtual": "^2.0.3",
"@stencil/angular-output-target": "^0.10.0",
@@ -1933,12 +1933,12 @@
}
},
"node_modules/@playwright/test": {
- "version": "1.53.2",
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.2.tgz",
- "integrity": "sha512-tEB2U5z74ebBeyfGNZ3Jfg29AnW+5HlWhvHtb/Mqco9pFdZU1ZLNdVb2UtB5CvmiilNr2ZfVH/qMmAROG/XTzw==",
+ "version": "1.54.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.1.tgz",
+ "integrity": "sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==",
"dev": true,
"dependencies": {
- "playwright": "1.53.2"
+ "playwright": "1.54.1"
},
"bin": {
"playwright": "cli.js"
@@ -9680,12 +9680,12 @@
}
},
"node_modules/playwright": {
- "version": "1.53.2",
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.2.tgz",
- "integrity": "sha512-6K/qQxVFuVQhRQhFsVZ9fGeatxirtrpPgxzBYWyZLEXJzqYwuL4fuNmfOfD5et1tJE4GScKyPNeLhZeRwuTU3A==",
+ "version": "1.54.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.1.tgz",
+ "integrity": "sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==",
"dev": true,
"dependencies": {
- "playwright-core": "1.53.2"
+ "playwright-core": "1.54.1"
},
"bin": {
"playwright": "cli.js"
@@ -9698,9 +9698,9 @@
}
},
"node_modules/playwright-core": {
- "version": "1.53.2",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.2.tgz",
- "integrity": "sha512-ox/OytMy+2w1jcYEYlOo1Hhp8hZkLCximMTUTMBXjGUA1KoFfiSZ+DU+3a739jsPY0yoKH2TFy9S2fsJas8yAw==",
+ "version": "1.54.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.1.tgz",
+ "integrity": "sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==",
"dev": true,
"bin": {
"playwright-core": "cli.js"
diff --git a/core/package.json b/core/package.json
index 6e50a52641b..11cfa4f1265 100644
--- a/core/package.json
+++ b/core/package.json
@@ -1,6 +1,6 @@
{
"name": "@ionic/core",
- "version": "8.6.4",
+ "version": "8.6.5",
"description": "Base components for Ionic",
"keywords": [
"ionic",
@@ -45,7 +45,7 @@
"@clack/prompts": "^0.11.0",
"@ionic/eslint-config": "^0.3.0",
"@ionic/prettier-config": "^2.0.0",
- "@playwright/test": "^1.53.2",
+ "@playwright/test": "^1.54.1",
"@rollup/plugin-node-resolve": "^8.4.0",
"@rollup/plugin-virtual": "^2.0.3",
"@stencil/angular-output-target": "^0.10.0",
diff --git a/core/src/components/input-otp/input-otp.tsx b/core/src/components/input-otp/input-otp.tsx
index 7c3de512860..3e8684505ab 100644
--- a/core/src/components/input-otp/input-otp.tsx
+++ b/core/src/components/input-otp/input-otp.tsx
@@ -49,6 +49,7 @@ export class InputOTP implements ComponentInterface {
@State() private inputValues: string[] = [];
@State() hasFocus = false;
+ @State() private previousInputValues: string[] = [];
/**
* Indicates whether and how the text value should be automatically capitalized as it is entered/edited by the user.
@@ -337,6 +338,7 @@ export class InputOTP implements ComponentInterface {
});
// Update the value without emitting events
this.value = this.inputValues.join('');
+ this.previousInputValues = [...this.inputValues];
}
/**
@@ -526,19 +528,12 @@ export class InputOTP implements ComponentInterface {
}
/**
- * Handles keyboard navigation and input for the OTP component.
+ * Handles keyboard navigation 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
*/
private onKeyDown = (index: number) => (event: KeyboardEvent) => {
const { length } = this;
@@ -596,34 +591,32 @@ export class InputOTP implements ComponentInterface {
// 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();
- }
};
+ /**
+ * Processes all input scenarios for each input box.
+ *
+ * This function manages:
+ * 1. Autofill handling
+ * 2. Input validation
+ * 3. Full selection replacement or typing in an empty box
+ * 4. Inserting in the middle with available space (shifting)
+ * 5. Single character replacement
+ */
private onInput = (index: number) => (event: InputEvent) => {
const { length, validKeyPattern } = this;
- const value = (event.target as HTMLInputElement).value;
-
- // If the value is longer than 1 character (autofill), split it into
- // characters and filter out invalid ones
- if (value.length > 1) {
+ const input = event.target as HTMLInputElement;
+ const value = input.value;
+ const previousValue = this.previousInputValues[index] || '';
+
+ // 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
+ // user is typing a single character into an input box containing a value
+ // as that will trigger this function with a value length of 2 characters.
+ const isAutofill = value.length - previousValue.length > 1;
+ if (isAutofill) {
+ // Distribute valid characters across input boxes
const validChars = value
.split('')
.filter((char) => validKeyPattern.test(char))
@@ -640,8 +633,10 @@ export class InputOTP implements ComponentInterface {
});
}
- // Update the value of the input group and emit the input change event
- this.value = validChars.join('');
+ for (let i = 0; i < length; i++) {
+ this.inputValues[i] = validChars[i] || '';
+ this.inputRefs[i].value = validChars[i] || '';
+ }
this.updateValue(event);
// Focus the first empty input box or the last input box if all boxes
@@ -652,23 +647,85 @@ export class InputOTP implements ComponentInterface {
this.inputRefs[nextIndex]?.focus();
}, 20);
+ this.previousInputValues = [...this.inputValues];
return;
}
- // Only allow input if it matches the pattern
- if (value.length > 0 && !validKeyPattern.test(value)) {
- this.inputRefs[index].value = '';
- this.inputValues[index] = '';
+ // 2. Input validation
+ // If the character entered is invalid (does not match the pattern),
+ // restore the previous value and exit
+ if (value.length > 0 && !validKeyPattern.test(value[value.length - 1])) {
+ input.value = this.inputValues[index] || '';
+ this.previousInputValues = [...this.inputValues];
return;
}
- // For single character input, fill the current box
- this.inputValues[index] = value;
- this.updateValue(event);
-
- if (value.length > 0) {
+ // 3. Full selection replacement or typing in an empty box
+ // If the user selects all text in the input box and types, or if the
+ // input box is empty, replace only this input box. If the box is empty,
+ // move to the next box, otherwise stay focused on this box.
+ const isAllSelected = input.selectionStart === 0 && input.selectionEnd === value.length;
+ const isEmpty = !this.inputValues[index];
+ if (isAllSelected || isEmpty) {
+ this.inputValues[index] = value;
+ input.value = value;
+ this.updateValue(event);
this.focusNext(index);
+ this.previousInputValues = [...this.inputValues];
+ return;
}
+
+ // 4. Inserting in the middle with available space (shifting)
+ // If typing in a filled input box and there are empty boxes at the end,
+ // shift all values starting at the current box to the right, and insert
+ // the new character at the current box.
+ const hasAvailableBoxAtEnd = this.inputValues[this.inputValues.length - 1] === '';
+ if (this.inputValues[index] && hasAvailableBoxAtEnd && value.length === 2) {
+ // Get the inserted character (from event or by diffing value/previousValue)
+ let newChar = (event as InputEvent).data;
+ if (!newChar) {
+ newChar = value.split('').find((c, i) => c !== previousValue[i]) || value[value.length - 1];
+ }
+ // Validate the new character before shifting
+ if (!validKeyPattern.test(newChar)) {
+ input.value = this.inputValues[index] || '';
+ this.previousInputValues = [...this.inputValues];
+ return;
+ }
+ // Shift values right from the end to the insertion point
+ for (let i = this.inputValues.length - 1; i > index; i--) {
+ this.inputValues[i] = this.inputValues[i - 1];
+ this.inputRefs[i].value = this.inputValues[i] || '';
+ }
+ this.inputValues[index] = newChar;
+ this.inputRefs[index].value = newChar;
+ this.updateValue(event);
+ this.previousInputValues = [...this.inputValues];
+ return;
+ }
+
+ // 5. Single character replacement
+ // Handles replacing a single character in a box containing a value based
+ // on the cursor position. We need the cursor position to determine which
+ // character was the last character typed. For example, if the user types "2"
+ // in an input box with the cursor at the beginning of the value of "6",
+ // the value will be "26", but we want to grab the "2" as the last character
+ // typed.
+ const cursorPos = input.selectionStart ?? value.length;
+ const newCharIndex = cursorPos - 1;
+ const newChar = value[newCharIndex] ?? value[0];
+
+ // Check if the new character is valid before updating the value
+ if (!validKeyPattern.test(newChar)) {
+ input.value = this.inputValues[index] || '';
+ this.previousInputValues = [...this.inputValues];
+ return;
+ }
+
+ this.inputValues[index] = newChar;
+ input.value = newChar;
+ this.updateValue(event);
+ this.previousInputValues = [...this.inputValues];
};
/**
@@ -712,12 +769,8 @@ export class InputOTP implements ComponentInterface {
// Focus the next empty input after pasting
// If all boxes are filled, focus the last input
- const nextEmptyIndex = validChars.length;
- if (nextEmptyIndex < length) {
- inputRefs[nextEmptyIndex]?.focus();
- } else {
- inputRefs[length - 1]?.focus();
- }
+ const nextEmptyIndex = validChars.length < length ? validChars.length : length - 1;
+ inputRefs[nextEmptyIndex]?.focus();
};
/**
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 2a50c1abd5c..2067a000209 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
@@ -442,6 +442,67 @@ configs({ modes: ['ios'] }).forEach(({ title, config }) => {
await verifyInputValues(inputOtp, ['1', '9', '3', '']);
});
+
+ test('should replace the last value when typing one more than the length', async ({ page }) => {
+ await page.setContent(`
This is my inline modal content!
+ + + +This is the child modal content!
+When the parent modal is dismissed, this child modal should also be dismissed automatically.
+ + + +