Skip to content

AI & Search input scrolls to bottom when editing in the middle #70

@SevaAIgnatyev

Description

@SevaAIgnatyev

Summary (Raw)

When the user types inside the AI & Search input while it already contains multiple lines of text, editing in the middle of the content causes the scroll position to jump to the bottom of the input. In this specific case, the caret should stay where the user is typing and the visible text block should remain anchored around the caret, not auto-scroll to the bottom.

This affects both:

  • The native bar: GlobalBottomBar (React Native TextInput wrapped in a ScrollView)
  • The web-only bar: GlobalBottomBarWeb (HTML <textarea> with custom scroll logic)

Where it’s implemented

  • Native bottom bar

    • File: app/app/components/GlobalBottomBar.tsx
    • Relevant area: dynamic height + scroll logic around the input:
      • Height/lines calculation and dynamicHeight / barHeight (rawLines, visibleLines, dynamicHeight, barHeight).
      • Scroll syncing and snapping:
        • onScrollViewContentSizeChangescrollRef.current.scrollToEnd({ animated: false });
        • Effect that scrolls on first 7th line appearance:
          if (rawLines === 7 && dynamicHeight >= MAX_BAR_HEIGHT && scrollY === 0) {
            scrollRef.current?.scrollTo({ y: INNER_PADDING, animated: false });
          }
      • Custom scrollbar math using scrollY, scrollRange, contentHeightWithGaps.
  • Web-only bottom bar

    • File: app/app/components/GlobalBottomBarWeb.tsx
    • Relevant area: DOM-based mirror + textarea scroll logic:
      • domMirrorHeight, contentHeight, dynamicHeight, barHeight.
      • measureAndResize and handleInput (using requestAnimationFrame).
      • Effects that:
        • Snap the textarea to bottom when in scroll mode (8+ lines).
        • Shift content when the 7th line first appears.
      • Custom scrollbar math using scrollY, domScrollRange, contentHeightWithGaps.

These two implementations are meant to stay in sync (native vs web reference implementation), so any fix should likely touch both files.

Steps to reproduce

  1. Open the app (either in TMA or local web) and focus the AI & Search bottom bar.
  2. Type/paste a long multi-line prompt (e.g. 8–10 lines) so the internal scroll mode is active.
  3. Scroll the input slightly and place the caret in the middle of the text (not at the very bottom).
  4. Start typing or deleting characters in the middle.

Actual behavior

  • As soon as you type in the middle of the text, the internal scroll position snaps to the bottom (or very close to it).
  • The caret effectively jumps down with the content, so the user loses context and can no longer see the lines they were editing.

Expected behavior

  • When the caret is positioned in the middle of the text, typing or deleting:
    • Keeps the visible content anchored around the caret.
    • Does not auto-scroll the internal ScrollView / <textarea> to the bottom unless the caret is at (or close to) the last line.
    • Custom scrollbar position should reflect the new scroll state without forcing a jump.

In short: typing in the middle should not force a bottom snap; the user should remain at their current edit position.

Likely root causes

Native (GlobalBottomBar.tsx):

  • The combination of:
    • onScrollViewContentSizeChange calling scrollToEnd whenever h > viewportHeight.
    • The 7th-line alignment effect that scrolls by INNER_PADDING when rawLines === 7 and scrollY === 0.
  • These behaviors assume “user is editing near the bottom and we want to keep the last line visible”, but they don’t distinguish between:
    • The user editing at the bottom (good to snap).
    • The user editing in the middle (snap is wrong).

Web (GlobalBottomBarWeb.tsx):

  • Similar logic:
    • Scroll-to-bottom effect in the “scroll mode” effect where it sets el.scrollTop = range when isScrollMode becomes true.
    • 7th-line alignment effect that nudges the content by INNER_PADDING.
  • These always push the textarea to the bottom of content, regardless of caret position.

Because the two implementations intentionally mirror each other, the bug is visible on both platforms.

Possible solutions

High level idea: only auto-scroll when the caret is near the bottom, not when the user is editing in the middle.

Option A – Track caret position and gate bottom snap

Native (GlobalBottomBar.tsx):

  • Track caret position via TextInput events:
    • Use onSelectionChange to capture selection.start / selection.end.
    • Derive whether the caret is on the last visible line (or within N characters of the end).
  • Update the scroll behavior:
    • In onScrollViewContentSizeChange, only call scrollToEnd if:
      • caretIsNearEnd === true, and
      • h > viewportHeight.
    • In the 7th-line effect, only auto-shift when caretIsNearEnd is true.

Web (GlobalBottomBarWeb.tsx):

  • Track caret via DOM APIs:
    • On <textarea>: use selectionStart / selectionEnd in an onSelect or onInput handler.
    • Determine if caret is near the end of the text.
  • Gate the “scroll to bottom” logic similarly:
    • Only set el.scrollTop = range when caretIsNearEnd is true.

Pros:

  • Behavior matches user intent: when they type at the bottom, it stays pinned; when they edit in the middle, it stays there.

Cons:

  • Slightly more complex state (track caret position, maybe debounce selection updates).

Option B – Hybrid: threshold-based auto-scroll

Instead of checking exact caret position, use a scroll position threshold:

  • If we are already near the bottom (e.g. last N pixels of scrollRange), allow auto scroll-to-end.
  • If we are scrolled somewhere in the middle, do not touch scrollTop.

This avoids tracking caret, but still respects “user scrolled up to read context”.

Notes

  • Whatever fix is chosen, it should be applied to both:
    • GlobalBottomBar.tsx (React Native / TMA)
    • GlobalBottomBarWeb.tsx (web-only reference)
  • The behavior should be validated specifically in Telegram Mini App on mobile, where the keyboard + viewport resizing can interact with scroll logic in subtle ways.

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions