Skip to content

ChatComposer: native-feeling mobile keyboard behavior (parity with claude.ai / chatgpt.com #494

Description

@martincarapia

Summary

On mobile (iOS Safari, iOS Cordova WKWebView, Android Chrome), ChatComposer feels noticeably worse than claude.ai and chatgpt.com: Return sends instead of inserting a newline, tapping Send bounces the keyboard closed and open, the composer doesn't track the keyboard, and the thread doesn't stay anchored. This ticket brings the composer (and the supporting pieces in this library) to parity. Consumed by ozwell-workspace.

Reference evidence (from live DOM inspection)

  • ChatGPT pads its scroll root with var(--screen-keyboard-height) (visualViewport-tracked keyboard inset) and marks its editor data-virtualkeyboard="true" (VirtualKeyboard API on Chromium). Input max-height: max(30svh, 5rem) / 13rem.
  • Claude sets enterkeyhint="enter" on the editor, floors input font with --cds-font-size-text-entry-floor (prevents iOS focus-zoom), caps input at 24rem, and uses cursor-text on the whole composer surface.

Scope

P0

  • Enter behavior on touch — handleKeyDown always sends on Enter. Add submitOnEnter?: 'desktop' | 'always' | 'never' (default 'desktop', detected via (hover: none) and (pointer: coarse)), so Return inserts a newline on touch devices. Set enterKeyHint accordingly ('enter' on touch, 'send' otherwise).
  • IME composition — ignore Enter while event.nativeEvent.isComposing (currently sends mid-composition for CJK input; affects desktop too).
  • Keep keyboard open on send — tapping Send/Stop blurs the textarea, the keyboard starts dismissing, then focus() re-opens it. Add onPointerDown={e => e.preventDefault()} to send/stop buttons so focus never leaves the textarea.
  • useKeyboardInset() hook (exported) — interactive-widget=resizes-content is ignored by iOS Safari/WKWebView and 100dvh does not shrink for the keyboard, so iOS scrolls the whole document to reveal the input (header scrolls away, content jumps). Hook listens to visualViewport resize/scroll, writes --keyboard-inset on <html> (innerHeight - visualViewport.height - visualViewport.offsetTop, clamped ≥ 0), resets stray document scroll, and prefers navigator.virtualKeyboard.overlaysContent + env(keyboard-inset-height) where supported. Expose a way for native hosts (Cordova keyboard plugin events) to feed the same value.

P1

  • Thread bottom-anchoring — AIChat only scrolls to bottom when messages changes. When the container shrinks (keyboard opens, composer grows), stay pinned to bottom if the user was already at the bottom (ResizeObserver).
  • Keyboard-aware max height — default/cap should use svh (e.g. max(30svh, 5rem)) rather than vh, which on iOS is the large viewport and lets the input grow past the visible area above the keyboard.
  • 16px font floor on mobile — default text-sm (14px) triggers iOS zoom-on-focus. Move text-base sm:text-sm into the library so every consumer gets it.
  • Touch-aware autoFocus — only honor autoFocus (and replyTo refocus) on fine pointers; on Android it pops the keyboard on every navigation.
  • Tap-anywhere-to-focus — pointer-down on non-interactive parts of the card focuses the textarea; add cursor-text to the card.
  • Input attributes — dir="auto" (RTL), autoCapitalize="sentences", autoCorrect="on", spellCheck, inputMode="text".

Out of scope (follow-ups)

  • Visual parity: card-level focus-within treatment instead of inner textarea ring; model selector inside the bottom row; swap mic↔send instead of always-visible disabled send.
  • Host (ozwell-workspace) changes: shell height calc(100dvh - var(--keyboard-inset, 0px)), collapsing safe-area-inset-bottom padding while the keyboard is open, adding cordova-plugin-ionic-keyboard and wiring its events into the hook.

Acceptance criteria

  • On iOS Safari, iOS Cordova, and Android Chrome: Return inserts a newline; Send button sends without the keyboard closing; the composer sits flush on top of the keyboard with no document scroll/jump; the last message stays visible when the keyboard opens and as the input grows; no zoom on focus.
  • Desktop behavior unchanged (Enter sends, Shift+Enter newline) except IME-composition fix.
  • New props/hook documented in stories; tests cover submitOnEnter, IME composition, and send-button focus retention.

Activity

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

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions