fix(client): keyboard-aware scroll + Enter-to-submit on forms (#254, #255) #258

Merged
james merged 2 commits from 254-255-keyboard-form-ux into main 2026-06-23 16:51:27 +00:00
Owner

Summary

Two related form-UX fixes on the universal client, bundled because they
sweep the same set of screens.

  • #254 — keyboard avoidance. Adds
    react-native-keyboard-controller
    (pinned to 1.21.10), mounts <KeyboardProvider> in
    app/_layout.tsx, and swaps each form-bearing screen's outer scroll
    container for KeyboardAwareScrollView. On focus the lib auto-scrolls
    the field into view above the soft keyboard. The library ships a
    no-op web shim (its bindings.ts resolves all native modules to
    plain Views and addListener returns a no-op subscription), so PWA
    behaviour is unchanged.
  • #255 — Enter-to-submit. For multi-field forms, non-terminal
    fields get returnKeyType="next" + blurOnSubmit={false} +
    onSubmitEditing={() => nextRef.current?.focus()}, and the terminal
    field calls the submit handler under returnKeyType="go" or
    "done". Single-field forms (skills add/rename, PAT create, server
    URL) submit directly on Enter. Multiline fields keep Enter as a
    newline — save remains on the explicit button.

Library choice (#254)

Picked react-native-keyboard-controller over the older
react-native-keyboard-aware-scroll-view: modern, actively
maintained, single-call API, and the autoscroll behaviour is the
load-bearing UX gap. Its peerDeps (react-native-reanimated >=3.0.0)
are already satisfied by the existing 4.3.1 install.

Screens touched

  • apps/client/app/login.tsx
  • apps/client/app/register.tsx
  • apps/client/app/server-setup.tsx
  • apps/client/app/(app)/profile.tsx (Basics card, contacts add row,
    contacts edit row)
  • apps/client/app/(app)/notes.tsx (create + edit forms; the inner
    FlatList collapses to a .map() so the
    KeyboardAwareScrollView owner can measure focused-input position)
  • apps/client/app/(app)/skills.tsx (add-section, add-skill, rename)
  • apps/client/app/(app)/experience.tsx (Education / Job / Position
    forms; Contribution form is all-multiline so it stays on the
    explicit submit button)
  • apps/client/app/(app)/account.tsx (PAT create)

Two commits so each fix is independently reviewable.

Test plan

  • pnpm install --frozen-lockfile
  • pnpm -F @carol/api typecheck / lint
  • pnpm -F @carol/api-client typecheck / lint / test / check
  • pnpm -F @carol/client typecheck / lint / test / export:web
  • Manual Expo Go smoke on Android — focus each TextInput on each
    form-bearing screen, confirm the field stays visible above the
    keyboard. Could not run on a physical device from this worktree;
    flagging for follow-up smoke.
  • Manual Expo Go smoke on Android — Enter on email field advances
    to password, Enter on password submits. Same chain on
    register.tsx, server-setup.tsx, and every multi-field edit
    form. Could not run on a device; flagging for follow-up smoke.

Notes / surprises

  • react-native-keyboard-controller's web shim is a clean no-op:
    bindings.ts (the default, non-.native entry) returns plain
    Views for every native component and no-op addListeners for
    every event emitter. KeyboardAwareScrollView still wraps a real
    Reanimated ScrollView on web, so layout is unchanged.
  • The notes screen previously used FlatList for note rows; that
    nested-virtualisation defeats KeyboardAwareScrollView's
    focused-input measurement. Inlined the rows as a flat .map()
    acceptable since per-user notes lists are short.
  • onSubmitEditing on RN-Web maps to the browser's Enter key, so the
    explicit wiring covers both platforms and there's no <form>
    element to lean on for native Enter-submit. Verified the typed
    ref<TextInput> pattern compiles under the RN-Web TextInput
    type.
  • returnKeyType="go" renders as "Go" on iOS, "↵" on Android, and
    "Enter" on the web. "done" renders as the locale's done-button
    glyph on native and Enter on the web. Both submit on Enter.

Closes #254.
Closes #255.

## Summary Two related form-UX fixes on the universal client, bundled because they sweep the same set of screens. - **#254 — keyboard avoidance.** Adds [`react-native-keyboard-controller`](https://kirillzyusko.github.io/react-native-keyboard-controller/) (pinned to `1.21.10`), mounts `<KeyboardProvider>` in `app/_layout.tsx`, and swaps each form-bearing screen's outer scroll container for `KeyboardAwareScrollView`. On focus the lib auto-scrolls the field into view above the soft keyboard. The library ships a no-op web shim (its `bindings.ts` resolves all native modules to plain Views and `addListener` returns a no-op subscription), so PWA behaviour is unchanged. - **#255 — Enter-to-submit.** For multi-field forms, non-terminal fields get `returnKeyType="next"` + `blurOnSubmit={false}` + `onSubmitEditing={() => nextRef.current?.focus()}`, and the terminal field calls the submit handler under `returnKeyType="go"` or `"done"`. Single-field forms (skills add/rename, PAT create, server URL) submit directly on Enter. Multiline fields keep Enter as a newline — save remains on the explicit button. ### Library choice (#254) Picked `react-native-keyboard-controller` over the older `react-native-keyboard-aware-scroll-view`: modern, actively maintained, single-call API, and the autoscroll behaviour is the load-bearing UX gap. Its peerDeps (`react-native-reanimated >=3.0.0`) are already satisfied by the existing `4.3.1` install. ### Screens touched - `apps/client/app/login.tsx` - `apps/client/app/register.tsx` - `apps/client/app/server-setup.tsx` - `apps/client/app/(app)/profile.tsx` (Basics card, contacts add row, contacts edit row) - `apps/client/app/(app)/notes.tsx` (create + edit forms; the inner `FlatList` collapses to a `.map()` so the `KeyboardAwareScrollView` owner can measure focused-input position) - `apps/client/app/(app)/skills.tsx` (add-section, add-skill, rename) - `apps/client/app/(app)/experience.tsx` (Education / Job / Position forms; Contribution form is all-multiline so it stays on the explicit submit button) - `apps/client/app/(app)/account.tsx` (PAT create) Two commits so each fix is independently reviewable. ## Test plan - [x] `pnpm install --frozen-lockfile` - [x] `pnpm -F @carol/api typecheck` / `lint` - [x] `pnpm -F @carol/api-client typecheck` / `lint` / `test` / `check` - [x] `pnpm -F @carol/client typecheck` / `lint` / `test` / `export:web` - [ ] Manual Expo Go smoke on Android — focus each `TextInput` on each form-bearing screen, confirm the field stays visible above the keyboard. Could not run on a physical device from this worktree; flagging for follow-up smoke. - [ ] Manual Expo Go smoke on Android — Enter on email field advances to password, Enter on password submits. Same chain on `register.tsx`, `server-setup.tsx`, and every multi-field edit form. Could not run on a device; flagging for follow-up smoke. ## Notes / surprises - `react-native-keyboard-controller`'s web shim is a clean no-op: `bindings.ts` (the default, non-`.native` entry) returns plain `View`s for every native component and no-op `addListener`s for every event emitter. `KeyboardAwareScrollView` still wraps a real Reanimated `ScrollView` on web, so layout is unchanged. - The notes screen previously used `FlatList` for note rows; that nested-virtualisation defeats `KeyboardAwareScrollView`'s focused-input measurement. Inlined the rows as a flat `.map()` — acceptable since per-user notes lists are short. - `onSubmitEditing` on RN-Web maps to the browser's Enter key, so the explicit wiring covers both platforms and there's no `<form>` element to lean on for native Enter-submit. Verified the typed `ref<TextInput>` pattern compiles under the RN-Web `TextInput` type. - `returnKeyType="go"` renders as "Go" on iOS, "↵" on Android, and "Enter" on the web. `"done"` renders as the locale's done-button glyph on native and Enter on the web. Both submit on Enter. Closes #254. Closes #255.
On Android, focusing a TextInput could leave the field hidden behind
the soft keyboard. Mount react-native-keyboard-controller's
KeyboardProvider in the root layout and swap each form-bearing
screen's outer scroll container for KeyboardAwareScrollView so the
focused input auto-scrolls into view. The lib ships a no-op web
shim, so the PWA behaviour is unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
fix(client): wire Enter-to-submit across form-bearing screens (#255)
Some checks failed
Commits / Conventional Commits (pull_request) Successful in 32s
PR / OpenAPI (pull_request) Successful in 2m0s
PR / Static analysis (pull_request) Successful in 1m42s
PR / Typecheck (pull_request) Successful in 2m54s
PR / Lint (pull_request) Successful in 3m18s
PR / Build (pull_request) Successful in 2m40s
PR / pnpm audit (pull_request) Successful in 2m12s
PR / Client (web export smoke) (pull_request) Successful in 2m29s
PR / Package age policy (soft) (pull_request) Successful in 1m5s
PR / Test (sqlite) (pull_request) Successful in 2m35s
Secrets / gitleaks (pull_request) Successful in 1m10s
PR / Test (postgres) (pull_request) Failing after 2m42s
PR / OSV-Scanner (pull_request) Successful in 2m11s
PR / Trivy (image) (pull_request) Successful in 3m15s
PR / Coverage (soft) (pull_request) Successful in 2m30s
2500c7a940
Multi-field forms now chain Enter through non-terminal fields via
useRef + onSubmitEditing + returnKeyType="next" (blurOnSubmit=false
to keep the keyboard up), with the terminal field firing the submit
under returnKeyType="go"/"done". Single-field forms (skills add/
rename, PAT create, server URL) submit directly on Enter. Multiline
fields keep Enter as a newline — save stays on the explicit button.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

📊 Test coverage

Patch coverage: no testable lines changed.

Overall (app/, lib/, db/, excluding UI per ADR-0019):

Metric Value Soft target
Lines 82.9% ≥ 50%
Branches 75.6% ≥ 75%
Functions 91.8% informational

Soft thresholds per ADR-0019. Coverage is informational and does not block merge.

<!-- coverage-comment --> ## 📊 Test coverage **Patch coverage:** no testable lines changed. **Overall** (`app/`, `lib/`, `db/`, excluding UI per ADR-0019): | Metric | Value | Soft target | |---|---|---| | Lines | 82.9% ✅ | ≥ 50% | | Branches | 75.6% ✅ | ≥ 75% | | Functions | 91.8% | informational | Soft thresholds per [ADR-0019](docs/adr/0019-coverage-soft-targets.md). Coverage is informational and does not block merge.
james merged commit dd5adeac8f into main 2026-06-23 16:51:27 +00:00
james deleted branch 254-255-keyboard-form-ux 2026-06-23 16:51:27 +00:00
Sign in to join this conversation.
No description provided.