fix(compare): keep add-school sheet above the mobile keyboard (VisualViewport) #57

Merged
tudor merged 1 commits from fix/modal-keyboard-viewport into main 2026-07-18 07:09:03 +00:00
Owner

Follow-up to #56: the add-school bottom sheet was hidden behind the on-screen keyboard on mobile.

Cause

The modal overlay is position: fixed; inset: 0, which sizes to the layout viewport. When the mobile keyboard opens it shrinks the visual viewport but not the layout one, so the overlay keeps full height — the sheet (anchored to the overlay's bottom via align-items: flex-end) and the lower half of the dim backdrop end up behind the keyboard. The search input and results become unreachable, and scrolling exposes uncovered background. vh/dvh units don't account for the keyboard, so this needs the VisualViewport API.

Fix

  • While the modal is open, track window.visualViewport (height + offsetTop, on resize and scroll) and pin the overlay to the visible region. Backdrop and sheet now stay entirely above the keyboard. On desktop / no-keyboard this is a no-op (visual == layout viewport).
  • Size the sheet against the overlay — max-height: 100%, min-height: min(55vh, 100%) — instead of vh, so a keyboard-shortened overlay still fits the whole sheet.
  • Drop the results list's own 400px inner scroll on mobile so the sheet is a single scroll container (no double scrollbars, input never trapped above a short window).

Verified by simulating a 360px keyboard on staging: before, the results sit behind the keyboard; after, the header, input and multiple results are all visible above it with the backdrop covering the full visible area.

100 frontend tests green.

🤖 Generated with Claude Code

https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB

Follow-up to #56: the add-school bottom sheet was hidden behind the on-screen keyboard on mobile. ## Cause The modal overlay is `position: fixed; inset: 0`, which sizes to the **layout** viewport. When the mobile keyboard opens it shrinks the **visual** viewport but not the layout one, so the overlay keeps full height — the sheet (anchored to the overlay's bottom via `align-items: flex-end`) and the lower half of the dim backdrop end up *behind* the keyboard. The search input and results become unreachable, and scrolling exposes uncovered background. `vh`/`dvh` units don't account for the keyboard, so this needs the VisualViewport API. ## Fix - While the modal is open, track `window.visualViewport` (`height` + `offsetTop`, on `resize` and `scroll`) and pin the overlay to the visible region. Backdrop and sheet now stay entirely above the keyboard. On desktop / no-keyboard this is a no-op (visual == layout viewport). - Size the sheet against the overlay — `max-height: 100%`, `min-height: min(55vh, 100%)` — instead of `vh`, so a keyboard-shortened overlay still fits the whole sheet. - Drop the results list's own 400px inner scroll on mobile so the sheet is a single scroll container (no double scrollbars, input never trapped above a short window). Verified by simulating a 360px keyboard on staging: before, the results sit behind the keyboard; after, the header, input and multiple results are all visible above it with the backdrop covering the full visible area. 100 frontend tests green. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB
tudor added 1 commit 2026-07-18 07:00:50 +00:00
fix(compare): keep the add-school sheet above the on-screen keyboard
PR Checks / Frontend Typecheck + Tests (pull_request) Successful in 1m6s
PR Checks / Backend Smoke (pull_request) Successful in 7s
PR Checks / Build Backend (no push) (pull_request) Successful in 11s
PR Checks / Build Frontend (no push) (pull_request) Successful in 47s
PR Checks / Build Pipeline (no push) (pull_request) Successful in 10s
PR Checks / AI Code Review (Claude) (pull_request) Successful in 1m31s
2fd997bfe6
The bottom-sheet overlay was position:fixed inset:0, sized to the LAYOUT
viewport, so when the mobile keyboard opened it kept full height: the
sheet (anchored to the overlay's bottom) and the lower half of the dim
backdrop sat behind the keyboard, hiding the search input and results.

Track window.visualViewport (height + offsetTop) while the modal is open
and pin the overlay to the visible region, so backdrop and sheet stay
above the keyboard. Size the sheet against the overlay (max-height:100%,
min-height:min(55vh,100%)) instead of vh, and drop the results list's own
inner scroll on mobile so the sheet is a single scroll area.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146VHeLAWjDVE2B5uU67jCB

🤖 AI Code Review (Claude Code)

This PR fixes a mobile-keyboard bug where the bottom-sheet search modal (and its backdrop) could be pushed behind the on-screen keyboard because the overlay was sized against the layout viewport instead of the visual viewport. It adds a visualViewport-tracking effect in Modal.tsx that pins the overlay's top/height inline, switches the sheet's CSS sizing to be relative to that overlay (min/max via 100%) instead of vh units, and removes the results list's own inner scroll on mobile so Modal's existing .content (overflow-y:auto) is the single scroll container. I verified the CSS wiring (.modal overflow:hidden + .content flex:1/overflow-y:auto) supports the 'one scroll container' comment's claim, and the fixed-position overlay override (explicit top/height/bottom:auto) correctly gives child percentage heights a definite containing block. No correctness, security, or deploy issues found.

🟡 Minor

  • nextjs-app/components/Modal.tsx: The visualViewport sync runs in useEffect rather than useLayoutEffect, so on initial mount there can be one paint where the overlay is still sized/positioned by the static CSS (inset:0, full layout viewport) before JS corrects it to the visual viewport — a brief flash if the keyboard is already open when the modal mounts (e.g. re-opening while a previous input retains focus).
## 🤖 AI Code Review (Claude Code) This PR fixes a mobile-keyboard bug where the bottom-sheet search modal (and its backdrop) could be pushed behind the on-screen keyboard because the overlay was sized against the layout viewport instead of the visual viewport. It adds a visualViewport-tracking effect in Modal.tsx that pins the overlay's top/height inline, switches the sheet's CSS sizing to be relative to that overlay (min/max via 100%) instead of vh units, and removes the results list's own inner scroll on mobile so Modal's existing `.content` (overflow-y:auto) is the single scroll container. I verified the CSS wiring (`.modal` overflow:hidden + `.content` flex:1/overflow-y:auto) supports the 'one scroll container' comment's claim, and the fixed-position overlay override (explicit top/height/bottom:auto) correctly gives child percentage heights a definite containing block. No correctness, security, or deploy issues found. ### 🟡 Minor - **nextjs-app/components/Modal.tsx**: The visualViewport sync runs in useEffect rather than useLayoutEffect, so on initial mount there can be one paint where the overlay is still sized/positioned by the static CSS (inset:0, full layout viewport) before JS corrects it to the visual viewport — a brief flash if the keyboard is already open when the modal mounts (e.g. re-opening while a previous input retains focus).
tudor merged commit a1128bd801 into main 2026-07-18 07:09:03 +00:00
Sign in to join this conversation.
No Reviewers
No labels
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: tudor/school_compare#57