Skip to content

Commit 3b3318d

Browse files
authored
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 ```
1 parent 07b46d7 commit 3b3318d

File tree

4 files changed

+37
-8
lines changed

4 files changed

+37
-8
lines changed

core/src/components/input/input.scss

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,13 @@
165165
// otherwise the .input-cover will not be rendered at all
166166
// The input cover is not clickable when the input is disabled
167167
.cloned-input {
168-
@include position(0, null, 0, 0);
169-
170168
position: absolute;
169+
top: 0;
170+
bottom: 0;
171+
172+
// Reset height since absolute positioning with top/bottom handles sizing
173+
height: auto;
174+
max-height: none;
171175

172176
pointer-events: none;
173177
}

core/src/components/textarea/textarea.scss

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,13 @@
205205
// otherwise the .input-cover will not be rendered at all
206206
// The input cover is not clickable when the input is disabled
207207
.cloned-input {
208-
@include position(0, null, 0, 0);
209-
210208
position: absolute;
209+
top: 0;
210+
bottom: 0;
211+
212+
// Reset height since absolute positioning with top/bottom handles sizing
213+
height: auto;
214+
max-height: none;
211215

212216
pointer-events: none;
213217
}

core/src/utils/input-shims/hacks/common.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,26 @@ const addClone = (
6868
if (disabledClonedInput) {
6969
clonedEl.disabled = true;
7070
}
71+
72+
/**
73+
* Position the clone at the same horizontal offset as the native input
74+
* to prevent the placeholder from overlapping start slot content (e.g., icons).
75+
*/
76+
const doc = componentEl.ownerDocument!;
77+
const isRTL = doc.dir === 'rtl';
78+
79+
if (isRTL) {
80+
const parentWidth = (parentEl as HTMLElement).offsetWidth;
81+
const startOffset = parentWidth - inputEl.offsetLeft - inputEl.offsetWidth;
82+
clonedEl.style.insetInlineStart = `${startOffset}px`;
83+
} else {
84+
clonedEl.style.insetInlineStart = `${inputEl.offsetLeft}px`;
85+
}
86+
7187
parentEl.appendChild(clonedEl);
7288
cloneMap.set(componentEl, clonedEl);
7389

74-
const doc = componentEl.ownerDocument!;
75-
const tx = doc.dir === 'rtl' ? 9999 : -9999;
90+
const tx = isRTL ? 9999 : -9999;
7691
componentEl.style.pointerEvents = 'none';
7792
inputEl.style.transform = `translate3d(${tx}px,${inputRelativeY}px,0) scale(0)`;
7893
};

core/src/utils/input-shims/hacks/scroll-assist.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -291,8 +291,14 @@ const jsSetFocus = async (
291291
// give the native text input focus
292292
relocateInput(componentEl, inputEl, false, scrollData.inputSafeY);
293293

294-
// ensure this is the focused input
295-
setManualFocus(inputEl);
294+
/**
295+
* If focus has moved to another element while scroll assist was running,
296+
* don't steal focus back. This prevents focus jumping when users
297+
* quickly switch between inputs or tap other elements.
298+
*/
299+
if (document.activeElement === inputEl) {
300+
setManualFocus(inputEl);
301+
}
296302

297303
/**
298304
* When the input is about to be blurred

0 commit comments

Comments
 (0)