From 3b3318da513b199128f3822bd8226797cd118b0f Mon Sep 17 00:00:00 2001 From: Shane Date: Tue, 13 Jan 2026 10:42:50 -0800 Subject: [PATCH] fix(input): prevent placeholder from overlapping start slot during scroll assist (#30896) Issue number: resolves internal --------- ## 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 ## Other information Current dev build: ``` 8.7.16-dev.11767042721.11309185 ``` --- core/src/components/input/input.scss | 8 ++++++-- core/src/components/textarea/textarea.scss | 8 ++++++-- core/src/utils/input-shims/hacks/common.ts | 19 +++++++++++++++++-- .../utils/input-shims/hacks/scroll-assist.ts | 10 ++++++++-- 4 files changed, 37 insertions(+), 8 deletions(-) diff --git a/core/src/components/input/input.scss b/core/src/components/input/input.scss index 2161cc3dfb..476cb012be 100644 --- a/core/src/components/input/input.scss +++ b/core/src/components/input/input.scss @@ -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; } diff --git a/core/src/components/textarea/textarea.scss b/core/src/components/textarea/textarea.scss index a23893b8b4..58c00824f6 100644 --- a/core/src/components/textarea/textarea.scss +++ b/core/src/components/textarea/textarea.scss @@ -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; } diff --git a/core/src/utils/input-shims/hacks/common.ts b/core/src/utils/input-shims/hacks/common.ts index 2f42749c2e..7553c07e10 100644 --- a/core/src/utils/input-shims/hacks/common.ts +++ b/core/src/utils/input-shims/hacks/common.ts @@ -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)`; }; diff --git a/core/src/utils/input-shims/hacks/scroll-assist.ts b/core/src/utils/input-shims/hacks/scroll-assist.ts index fb2b190020..9d4685cb8d 100644 --- a/core/src/utils/input-shims/hacks/scroll-assist.ts +++ b/core/src/utils/input-shims/hacks/scroll-assist.ts @@ -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