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.
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
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
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 main2026-07-18 07:09:03 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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 viaalign-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/dvhunits don't account for the keyboard, so this needs the VisualViewport API.Fix
window.visualViewport(height+offsetTop, onresizeandscroll) 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).max-height: 100%,min-height: min(55vh, 100%)— instead ofvh, so a keyboard-shortened overlay still fits the whole sheet.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
🤖 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 (.modaloverflow:hidden +.contentflex: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