Skip to content

iOS Safari / mobile web: pasting large text in composer pushes toolbar under virtual keyboard and blocks scroll (calc(50dvh - 3rem)) #3950

Description

@VKirill

Summary

When drafting or pasting messages in the thread composer on iOS (both in the official @bb/mobile iOS app and in mobile Safari):

  1. Composer expansion & toolbar overflow: The composer expands beyond the visible screen area (calc(50dvh - 3rem) in existing threads, and calc(70dvh - 3rem) in new chats / root-compose), pushing the bottom action bar (model selector, voice microphone button, paperclip, send button) beneath the on-screen keyboard.
  2. Caret trapping in text: Single-finger swipe inside the focused ProseMirror text area is captured by WebKit as cursor navigation rather than scrolling data-promptbox-editor-scroll. Users cannot scroll down to place the cursor (caret |) at the end of their text.
  3. Voice dictation blocked: Because the editor doesn't scroll to the end and the bottom toolbar is pushed under the keyboard, users cannot place the cursor at the end of their draft to activate the microphone button for appended voice input.
  4. Timeline occlusion & no way to dismiss keyboard: The expanded composer (~400px–550px) + software keyboard (~340px) occupy the entire screen (~850px), reducing the visible chat history to an unusable ~0–20px sliver. Because hideKeyboardAccessoryView hides the native iOS "Done" bar and BottomAnchoredScrollBody does not blur the input on timeline touch, users cannot dismiss the keyboard or scroll up to read previous assistant messages while composing.

Versions and environment

  • Client: Official iOS App (@bb/mobile, React Native WKWebView shell) and mobile Safari
  • bb: 0.43.1
  • Source reference on main: c1a64f4b49b0659e92a7aa4434e79d062b3e814f
  • Investigation thread: thr_rj96urndqv

Steps to reproduce

Scenario A: In an existing chat

  1. Open a thread with conversation history on an iPhone (in the native iOS app or in Safari).
  2. Tap the thread composer to focus and bring up the software keyboard.
  3. Paste or type a multi-line message (15–20+ lines).
  4. Observe that the composer takes up the entire space between the top navigation bar and the keyboard, leaving 0px to view previous messages.
  5. Attempt to scroll down inside the composer to reach the bottom, tap at the end of the text, or tap the microphone button.

Scenario B: In a new chat ("New thread" / root-compose)

  1. Tap "New thread" on an iPhone.
  2. Type or paste a long prompt.
  3. Observe that with calc(70dvh - 3rem), the composer card expands even further (~550px), pushing the microphone and submit buttons completely off-screen beneath the keyboard.
  4. Attempt to scroll down to place the cursor at the end to dictate via microphone.

Expected vs actual

Expected:
1. The composer and its action toolbar (including mic and submit buttons) remain fully visible above the software keyboard in both thread and new-chat layouts.
2. The user can scroll inside the promptbox to easily place the cursor anywhere (including the very end) and tap the microphone button.
3. Swiping the message timeline blurs the composer and dismisses the keyboard (keyboardDismissMode="on-drag"), freeing the entire screen to read past messages, or a "Done" / collapse button is available.

Actual:
1. In both existing chats (50dvh) and new chats (70dvh), the composer grows taller than the visible viewport above the keyboard; action buttons (microphone, model selector, send) are pushed beneath the keyboard.
2. Single-finger swipe inside the text area moves the text caret instead of scrolling the container.
3. The chat history is completely occluded (0–20px visible). There is no "Done" button on the keyboard, and scrolling the timeline does not dismiss the keyboard, trapping the user in the focused composer.

Root Cause Analysis

1. dvh does not shrink on iOS when the virtual keyboard is open

In apps/app/src/components/promptbox/ComposerEditorSlot.tsx:

const COMPOSER_EDITOR_MAX_HEIGHT_BY_LAYOUT: Record<ComposerEditorLayout, string> = {
  thread: "calc(50dvh - 3rem)",
  "root-compose": "calc(70dvh - 3rem)",
};

Per W3C CSS Values and Units Level 4 spec, dvh (Dynamic Viewport Height) is not affected by the virtual keyboard.

  • In existing threads (50dvh - 3rem), editor slot grows to ~377px.
  • In new chats (70dvh - 3rem), editor slot grows to ~547px!
  • When the iOS virtual keyboard appears (~340px), visible viewport height drops to ~510px.
  • Deducting top navigation header (~50px) and safe areas leaves only ~440px.
  • In new chats, a 547px editor + 70px card toolbar = ~617px, which exceeds the available 440px viewport by ~177px, guaranteeing that the microphone button, attachments, and send button are pushed beneath the keyboard.

2. Timeline occlusion and keyboard trap in @bb/mobile

In apps/mobile/src/screens/webview/ProfileWebViewScreen.tsx:

<ShellWebView
  contentInsetAdjustmentBehavior="never"
  automaticallyAdjustContentInsets={false}
  hideKeyboardAccessoryView // <--- Hides native iOS "Done" accessory bar!
  ...
/>
  • Setting hideKeyboardAccessoryView removes the standard iOS keyboard accessory toolbar where the user normally taps "Done" to dismiss the keyboard.
  • In apps/app/src/components/ui/bottom-anchored-scroll-body.tsx, touch events on the timeline (touchstart, touchmove) mark scroll intent but never call activeElement.blur().
  • Consequently, while typing/composing, the keyboard stays up indefinitely, the composer occupies the entire visible area, and the chat history cannot be read.

3. Touch scroll trapping in contenteditable (ProseMirror)

When .ProseMirror is focused, WebKit captures vertical single-finger touch gestures for text caret navigation / text selection rather than delegating pan gestures to the overflow scroll container ([data-promptbox-editor-scroll]).


Proposed Solution / PR Blueprint

Fix 1: Dynamically clamp maxHeight by window.visualViewport.height on mobile

In apps/app/src/components/promptbox/ComposerEditorSlot.tsx:

function useVisualViewportHeight(): number | null {
  const [height, setHeight] = useState<number | null>(() =>
    typeof window !== "undefined" && window.visualViewport ? window.visualViewport.height : null
  );

  useEffect(() => {
    const vv = window.visualViewport;
    if (!vv) return;
    const update = () => setHeight(vv.height);
    vv.addEventListener("resize", update);
    return () => vv.removeEventListener("resize", update);
  }, []);

  return height;
}

In ComposerEditorSlot:

const visualViewportHeight = useVisualViewportHeight();

// When virtual keyboard is open on small screens (< 650px visual viewport),
// clamp maxHeight to 35% of the visible viewport so composer + bottom action bar (mic, model, send) fit comfortably in both thread and root-compose layouts.
const dynamicMaxHeight = visualViewportHeight && visualViewportHeight < 650
  ? `${Math.max(120, Math.floor(visualViewportHeight * 0.35))}px`
  : COMPOSER_EDITOR_MAX_HEIGHT_BY_LAYOUT[layout];

Apply to style:

style={{
  minHeight: isCompactLayout ? "48px" : `${minHeight}px`,
  height: isCompactLayout ? "48px" : undefined,
  maxHeight: isCompactLayout ? "48px" : dynamicMaxHeight,
}}

Fix 2: Dismiss keyboard on timeline scroll (chat history reading)

In apps/app/src/components/ui/bottom-anchored-scroll-body.tsx:
When the user drags the timeline to read past messages, dismiss the keyboard so the timeline gets the full screen:

const markTouchMoveScrollIntent = useCallback(() => {
  markUserScrollIntent();
  // Dismiss software keyboard if the user starts dragging the chat history
  if (
    document.activeElement instanceof HTMLElement &&
    document.activeElement.closest("[data-app-composer]")
  ) {
    document.activeElement.blur();
  }
}, [markUserScrollIntent]);

And in apps/mobile/src/screens/webview/ProfileWebViewScreen.tsx:
Add keyboardDismissMode="on-drag" to <WebView> so native scroll gestures also drop the keyboard.

Fix 3: Enable smooth touch scrolling on iOS & Auto-scroll on paste

In apps/app/src/components/promptbox/ComposerEditorSlot.tsx:

className={cn(
  "w-full overflow-y-auto bg-transparent px-4 pb-1 pr-14 pt-3 outline-none",
  "touch-pan-y [-webkit-overflow-scrolling:touch]",
  COARSE_POINTER_TEXT_BASE_CLASS,
  "leading-relaxed",
  isCompactLayout && "h-12 overflow-hidden pb-0 pr-14 pt-0",
)}

Auto-scroll to bottom on paste:

useEffect(() => {
  if (!editor) return;
  const handlePaste = () => {
    requestAnimationFrame(() => {
      if (scrollContainerRef.current) {
        scrollContainerRef.current.scrollTop = scrollContainerRef.current.scrollHeight;
      }
    });
  };
  editor.view.dom.addEventListener("paste", handlePaste);
  return () => editor.view.dom.removeEventListener("paste", handlePaste);
}, [editor, scrollContainerRef]);

What you ruled out

  • Ruled out bb-plugin-beautiful-chat and other UI plugins: verified against clean BB web CSS.
  • Ruled out desktop browsers (macOS / Linux / Windows): reproducible exclusively on touch devices with virtual software keyboards (iOS Safari and @bb/mobile).

Checks

  • Traced and verified against apps/app/src/components/promptbox/ComposerEditorSlot.tsx, apps/app/src/components/ui/bottom-anchored-scroll-body.tsx, and apps/mobile/src/screens/webview/ProfileWebViewScreen.tsx on main.
  • Covers both thread layout (50dvh) and new-chat root-compose layout (70dvh), plus voice dictation caret positioning.
  • Includes complete reproduction, root cause geometry analysis, and ready-to-implement code fix.

AGENT GENERATED

Investigation thread: thr_rj96urndqv

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    mobileMobile web: iOS Safari, touch, layoutpartial-reproBug partially reproduced; some claims unverified; see linked reportuiApp shell, sidebar, composer, rendering

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions