fix(input): prevent placeholder from overlapping start slot during scroll assist (#30896)

Issue number: resolves internal

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
On iOS, when focusing an `ion-input` or `ion-textarea` that requires
scrolling into view (scroll assist), the placeholder text shifts to the
left and overlaps any content in the start slot (e.g., icons). This
occurs because the cloned input used during scroll assist is positioned
at the container's left edge rather than at the native input's actual
position. Additionally, when quickly switching between inputs before
scroll assist completes, focus jumps back to the original input.

## What is the new behavior?
The cloned input is now positioned at the same offset as the native
input, preventing the placeholder from shifting or overlapping start
slot content during scroll assist. This works correctly for both LTR and
RTL layouts. Also, scroll assist no longer steals focus back if the user
has moved focus to another element while scrolling was in progress.

## Does this introduce a breaking change?

- [ ] Yes
- [X] No

<!--
  If this introduces a breaking change:
1. Describe the impact and migration path for existing applications
below.
  2. Update the BREAKING.md file with the breaking change.
3. Add "BREAKING CHANGE: [...]" to the commit description when merging.
See
https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer
for more information.
-->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

Current dev build:
```
8.7.16-dev.11767042721.11309185
```
This commit is contained in:
Shane
2026-01-13 10:42:50 -08:00
committed by GitHub
parent 07b46d745a
commit 3b3318da51
4 changed files with 37 additions and 8 deletions

View File

@@ -165,9 +165,13 @@
// otherwise the .input-cover will not be rendered at all
// The input cover is not clickable when the input is disabled
.cloned-input {
@include position(0, null, 0, 0);
position: absolute;
top: 0;
bottom: 0;
// Reset height since absolute positioning with top/bottom handles sizing
height: auto;
max-height: none;
pointer-events: none;
}

View File

@@ -205,9 +205,13 @@
// otherwise the .input-cover will not be rendered at all
// The input cover is not clickable when the input is disabled
.cloned-input {
@include position(0, null, 0, 0);
position: absolute;
top: 0;
bottom: 0;
// Reset height since absolute positioning with top/bottom handles sizing
height: auto;
max-height: none;
pointer-events: none;
}

View File

@@ -68,11 +68,26 @@ const addClone = (
if (disabledClonedInput) {
clonedEl.disabled = true;
}
/**
* Position the clone at the same horizontal offset as the native input
* to prevent the placeholder from overlapping start slot content (e.g., icons).
*/
const doc = componentEl.ownerDocument!;
const isRTL = doc.dir === 'rtl';
if (isRTL) {
const parentWidth = (parentEl as HTMLElement).offsetWidth;
const startOffset = parentWidth - inputEl.offsetLeft - inputEl.offsetWidth;
clonedEl.style.insetInlineStart = `${startOffset}px`;
} else {
clonedEl.style.insetInlineStart = `${inputEl.offsetLeft}px`;
}
parentEl.appendChild(clonedEl);
cloneMap.set(componentEl, clonedEl);
const doc = componentEl.ownerDocument!;
const tx = doc.dir === 'rtl' ? 9999 : -9999;
const tx = isRTL ? 9999 : -9999;
componentEl.style.pointerEvents = 'none';
inputEl.style.transform = `translate3d(${tx}px,${inputRelativeY}px,0) scale(0)`;
};

View File

@@ -291,8 +291,14 @@ const jsSetFocus = async (
// give the native text input focus
relocateInput(componentEl, inputEl, false, scrollData.inputSafeY);
// ensure this is the focused input
setManualFocus(inputEl);
/**
* If focus has moved to another element while scroll assist was running,
* don't steal focus back. This prevents focus jumping when users
* quickly switch between inputs or tap other elements.
*/
if (document.activeElement === inputEl) {
setManualFocus(inputEl);
}
/**
* When the input is about to be blurred