Skip to content

Commit 79a12ce

Browse files
chore: merge main into next (#30557)
2 parents 060f554 + fef3016 commit 79a12ce

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1102
-192
lines changed

.github/workflows/assign-issues.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@ jobs:
1313
- name: 'Auto-assign issue'
1414
uses: pozil/auto-assign-issue@39c06395cbac76e79afc4ad4e5c5c6db6ecfdd2e # v2.2.0
1515
with:
16-
assignees: brandyscarney, thetaPC, ShaneK
16+
assignees: brandyscarney, ShaneK
1717
numOfAssignee: 1
1818
allowSelfAssign: false

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,23 @@
33
All notable changes to this project will be documented in this file.
44
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
55

6+
## [8.6.5](https://github.com/ionic-team/ionic-framework/compare/v8.6.4...v8.6.5) (2025-07-16)
7+
8+
9+
### Bug Fixes
10+
11+
* **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)
12+
* **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)
13+
* **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)
14+
* **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)
15+
* **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)
16+
* **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))
17+
* **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)
18+
19+
20+
21+
22+
623
## [8.6.4](https://github.com/ionic-team/ionic-framework/compare/v8.6.3...v8.6.4) (2025-07-09)
724

825

core/CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,23 @@
33
All notable changes to this project will be documented in this file.
44
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
55

6+
## [8.6.5](https://github.com/ionic-team/ionic-framework/compare/v8.6.4...v8.6.5) (2025-07-16)
7+
8+
9+
### Bug Fixes
10+
11+
* **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)
12+
* **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)
13+
* **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)
14+
* **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)
15+
* **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)
16+
* **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))
17+
* **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)
18+
19+
20+
21+
22+
623
## [8.6.4](https://github.com/ionic-team/ionic-framework/compare/v8.6.3...v8.6.4) (2025-07-09)
724

825

core/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Get Playwright
2-
FROM mcr.microsoft.com/playwright:v1.53.1
2+
FROM mcr.microsoft.com/playwright:v1.54.1
33

44
# Set the working directory
55
WORKDIR /ionic

core/package-lock.json

Lines changed: 14 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ionic/core",
3-
"version": "8.6.4",
3+
"version": "8.6.5",
44
"description": "Base components for Ionic",
55
"keywords": [
66
"ionic",
@@ -45,7 +45,7 @@
4545
"@clack/prompts": "^0.11.0",
4646
"@ionic/eslint-config": "^0.3.0",
4747
"@ionic/prettier-config": "^2.0.0",
48-
"@playwright/test": "^1.53.2",
48+
"@playwright/test": "^1.54.1",
4949
"@rollup/plugin-node-resolve": "^8.4.0",
5050
"@rollup/plugin-virtual": "^2.0.3",
5151
"@stencil/angular-output-target": "^0.10.0",

core/src/components/input-otp/input-otp.tsx

Lines changed: 102 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export class InputOTP implements ComponentInterface {
4949

5050
@State() private inputValues: string[] = [];
5151
@State() hasFocus = false;
52+
@State() private previousInputValues: string[] = [];
5253

5354
/**
5455
* 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 {
337338
});
338339
// Update the value without emitting events
339340
this.value = this.inputValues.join('');
341+
this.previousInputValues = [...this.inputValues];
340342
}
341343

342344
/**
@@ -526,19 +528,12 @@ export class InputOTP implements ComponentInterface {
526528
}
527529

528530
/**
529-
* Handles keyboard navigation and input for the OTP component.
531+
* Handles keyboard navigation for the OTP component.
530532
*
531533
* Navigation:
532534
* - Backspace: Clears current input and moves to previous box if empty
533535
* - Arrow Left/Right: Moves focus between input boxes
534536
* - Tab: Allows normal tab navigation between components
535-
*
536-
* Input Behavior:
537-
* - Validates input against the allowed pattern
538-
* - When entering a key in a filled box:
539-
* - Shifts existing values right if there is room
540-
* - Updates the value of the input group
541-
* - Prevents default behavior to avoid automatic focus shift
542537
*/
543538
private onKeyDown = (index: number) => (event: KeyboardEvent) => {
544539
const { length } = this;
@@ -596,34 +591,32 @@ export class InputOTP implements ComponentInterface {
596591
// Let all tab events proceed normally
597592
return;
598593
}
599-
600-
// If the input box contains a value and the key being
601-
// entered is a valid key for the input box update the value
602-
// and shift the values to the right if there is room.
603-
if (this.inputValues[index] && this.validKeyPattern.test(event.key)) {
604-
if (!this.inputValues[length - 1]) {
605-
for (let i = length - 1; i > index; i--) {
606-
this.inputValues[i] = this.inputValues[i - 1];
607-
this.inputRefs[i].value = this.inputValues[i] || '';
608-
}
609-
}
610-
this.inputValues[index] = event.key;
611-
this.inputRefs[index].value = event.key;
612-
this.updateValue(event);
613-
614-
// Prevent default to avoid the browser from
615-
// automatically moving the focus to the next input
616-
event.preventDefault();
617-
}
618594
};
619595

596+
/**
597+
* Processes all input scenarios for each input box.
598+
*
599+
* This function manages:
600+
* 1. Autofill handling
601+
* 2. Input validation
602+
* 3. Full selection replacement or typing in an empty box
603+
* 4. Inserting in the middle with available space (shifting)
604+
* 5. Single character replacement
605+
*/
620606
private onInput = (index: number) => (event: InputEvent) => {
621607
const { length, validKeyPattern } = this;
622-
const value = (event.target as HTMLInputElement).value;
623-
624-
// If the value is longer than 1 character (autofill), split it into
625-
// characters and filter out invalid ones
626-
if (value.length > 1) {
608+
const input = event.target as HTMLInputElement;
609+
const value = input.value;
610+
const previousValue = this.previousInputValues[index] || '';
611+
612+
// 1. Autofill handling
613+
// If the length of the value increases by more than 1 from the previous
614+
// value, treat this as autofill. This is to prevent the case where the
615+
// user is typing a single character into an input box containing a value
616+
// as that will trigger this function with a value length of 2 characters.
617+
const isAutofill = value.length - previousValue.length > 1;
618+
if (isAutofill) {
619+
// Distribute valid characters across input boxes
627620
const validChars = value
628621
.split('')
629622
.filter((char) => validKeyPattern.test(char))
@@ -640,8 +633,10 @@ export class InputOTP implements ComponentInterface {
640633
});
641634
}
642635

643-
// Update the value of the input group and emit the input change event
644-
this.value = validChars.join('');
636+
for (let i = 0; i < length; i++) {
637+
this.inputValues[i] = validChars[i] || '';
638+
this.inputRefs[i].value = validChars[i] || '';
639+
}
645640
this.updateValue(event);
646641

647642
// Focus the first empty input box or the last input box if all boxes
@@ -652,23 +647,85 @@ export class InputOTP implements ComponentInterface {
652647
this.inputRefs[nextIndex]?.focus();
653648
}, 20);
654649

650+
this.previousInputValues = [...this.inputValues];
655651
return;
656652
}
657653

658-
// Only allow input if it matches the pattern
659-
if (value.length > 0 && !validKeyPattern.test(value)) {
660-
this.inputRefs[index].value = '';
661-
this.inputValues[index] = '';
654+
// 2. Input validation
655+
// If the character entered is invalid (does not match the pattern),
656+
// restore the previous value and exit
657+
if (value.length > 0 && !validKeyPattern.test(value[value.length - 1])) {
658+
input.value = this.inputValues[index] || '';
659+
this.previousInputValues = [...this.inputValues];
662660
return;
663661
}
664662

665-
// For single character input, fill the current box
666-
this.inputValues[index] = value;
667-
this.updateValue(event);
668-
669-
if (value.length > 0) {
663+
// 3. Full selection replacement or typing in an empty box
664+
// If the user selects all text in the input box and types, or if the
665+
// input box is empty, replace only this input box. If the box is empty,
666+
// move to the next box, otherwise stay focused on this box.
667+
const isAllSelected = input.selectionStart === 0 && input.selectionEnd === value.length;
668+
const isEmpty = !this.inputValues[index];
669+
if (isAllSelected || isEmpty) {
670+
this.inputValues[index] = value;
671+
input.value = value;
672+
this.updateValue(event);
670673
this.focusNext(index);
674+
this.previousInputValues = [...this.inputValues];
675+
return;
671676
}
677+
678+
// 4. Inserting in the middle with available space (shifting)
679+
// If typing in a filled input box and there are empty boxes at the end,
680+
// shift all values starting at the current box to the right, and insert
681+
// the new character at the current box.
682+
const hasAvailableBoxAtEnd = this.inputValues[this.inputValues.length - 1] === '';
683+
if (this.inputValues[index] && hasAvailableBoxAtEnd && value.length === 2) {
684+
// Get the inserted character (from event or by diffing value/previousValue)
685+
let newChar = (event as InputEvent).data;
686+
if (!newChar) {
687+
newChar = value.split('').find((c, i) => c !== previousValue[i]) || value[value.length - 1];
688+
}
689+
// Validate the new character before shifting
690+
if (!validKeyPattern.test(newChar)) {
691+
input.value = this.inputValues[index] || '';
692+
this.previousInputValues = [...this.inputValues];
693+
return;
694+
}
695+
// Shift values right from the end to the insertion point
696+
for (let i = this.inputValues.length - 1; i > index; i--) {
697+
this.inputValues[i] = this.inputValues[i - 1];
698+
this.inputRefs[i].value = this.inputValues[i] || '';
699+
}
700+
this.inputValues[index] = newChar;
701+
this.inputRefs[index].value = newChar;
702+
this.updateValue(event);
703+
this.previousInputValues = [...this.inputValues];
704+
return;
705+
}
706+
707+
// 5. Single character replacement
708+
// Handles replacing a single character in a box containing a value based
709+
// on the cursor position. We need the cursor position to determine which
710+
// character was the last character typed. For example, if the user types "2"
711+
// in an input box with the cursor at the beginning of the value of "6",
712+
// the value will be "26", but we want to grab the "2" as the last character
713+
// typed.
714+
const cursorPos = input.selectionStart ?? value.length;
715+
const newCharIndex = cursorPos - 1;
716+
const newChar = value[newCharIndex] ?? value[0];
717+
718+
// Check if the new character is valid before updating the value
719+
if (!validKeyPattern.test(newChar)) {
720+
input.value = this.inputValues[index] || '';
721+
this.previousInputValues = [...this.inputValues];
722+
return;
723+
}
724+
725+
this.inputValues[index] = newChar;
726+
input.value = newChar;
727+
this.updateValue(event);
728+
this.previousInputValues = [...this.inputValues];
672729
};
673730

674731
/**
@@ -712,12 +769,8 @@ export class InputOTP implements ComponentInterface {
712769

713770
// Focus the next empty input after pasting
714771
// If all boxes are filled, focus the last input
715-
const nextEmptyIndex = validChars.length;
716-
if (nextEmptyIndex < length) {
717-
inputRefs[nextEmptyIndex]?.focus();
718-
} else {
719-
inputRefs[length - 1]?.focus();
720-
}
772+
const nextEmptyIndex = validChars.length < length ? validChars.length : length - 1;
773+
inputRefs[nextEmptyIndex]?.focus();
721774
};
722775

723776
/**

0 commit comments

Comments
 (0)