Conversation
Use Visual Viewport API to dynamically position the modal toolbar above the mobile keyboard when it opens. Created useKeyboardAwarePosition hook that listens to viewport resize/scroll events and calculates the correct position. Applied only to phone screens using position: absolute with transform, while tablet/desktop retain the existing fixed positioning. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
WalkthroughA new custom React hook calculates the toolbar's top offset for positioning above the on-screen keyboard on mobile devices using the Visual Viewport API with fallbacks. The Modal component imports and uses this hook to adjust toolbar positioning dynamically on mobile, while preserving existing behavior on tablets and desktops. Changes
Sequence DiagramsequenceDiagram
participant Mobile as Mobile Device
participant Modal as Modal Component
participant Hook as useKeyboardAwarePosition Hook
participant VVApi as Visual Viewport API
Mobile->>VVApi: Keyboard appears (viewport shrinks)
VVApi-->>Hook: Trigger resize/scroll event
Hook->>VVApi: Read viewport dimensions
Hook->>Hook: Calculate: top = viewport.bottom - toolbarHeight
Hook-->>Modal: Return updated top offset
Modal->>Modal: Apply transform: translateY(toolbarTop)
Modal-->>Mobile: Toolbar repositions above keyboard
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In @app/hooks/useKeyboardAwarePosition.ts:
- Line 9: The hook initializes top to 0 which can cause a layout flash; change
the useState initialization in useKeyboardAwarePosition to use a function
initializer that computes the initial top synchronously (calling the same logic
you use after mount) and guard any window access with typeof window !==
'undefined' so SSR won't break; update references to top/setTop as before and
keep the existing effect/resize/keyboard listeners but remove the initial jump
by computing and returning the correct initial position.
- Around line 12-21: In handleResize, the position calculation mixes visual
viewport and layout viewport coords; when window.visualViewport exists use its
offsetTop rather than window.scrollY — compute const viewportOffset =
window.visualViewport ? window.visualViewport.offsetTop : window.scrollY || 0,
use viewportHeight = window.visualViewport ? window.visualViewport.height :
window.innerHeight, then set newTop = viewportHeight + viewportOffset -
elementHeight and call setTop(newTop); ensure null-safe access to
window.visualViewport and preserve the existing fallback behavior.
📜 Review details
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (2)
app/components/common/Modal.tsxapp/hooks/useKeyboardAwarePosition.ts
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: NolanPic
Repo: NolanPic/churchfeed PR: 80
File: app/components/Feed.module.css:2-2
Timestamp: 2025-10-07T04:44:10.445Z
Learning: The repository uses a CSS Anchor Positioning polyfill loaded conditionally in app/layout.tsx via a Script component that checks for CSS anchor positioning support and loads from unpkg when unavailable. This means `anchor-name` and related CSS anchor positioning properties are safe to use throughout the codebase.
📚 Learning: 2025-10-07T04:44:10.445Z
Learnt from: NolanPic
Repo: NolanPic/churchfeed PR: 80
File: app/components/Feed.module.css:2-2
Timestamp: 2025-10-07T04:44:10.445Z
Learning: The repository uses a CSS Anchor Positioning polyfill loaded conditionally in app/layout.tsx via a Script component that checks for CSS anchor positioning support and loads from unpkg when unavailable. This means `anchor-name` and related CSS anchor positioning properties are safe to use throughout the codebase.
Applied to files:
app/hooks/useKeyboardAwarePosition.ts
📚 Learning: 2025-08-18T23:21:28.872Z
Learnt from: NolanPic
Repo: NolanPic/churchfeed PR: 64
File: app/components/editor/EditorToolbar.tsx:4-4
Timestamp: 2025-08-18T23:21:28.872Z
Learning: The Button component in app/components/common/Button.tsx was refactored to use only default export, removing the previous named export to eliminate redundancy.
Applied to files:
app/components/common/Modal.tsx
🧬 Code graph analysis (1)
app/components/common/Modal.tsx (1)
app/hooks/useKeyboardAwarePosition.ts (1)
useKeyboardAwarePosition(8-40)
🔇 Additional comments (2)
app/components/common/Modal.tsx (2)
246-256: The toolbar positioning is correct and requires no changes.The toolbar is a direct sibling of
.modalwithin.modalWrapper(which hasposition: fixed), not nested inside the scrollable modal. On mobile,position: "absolute"positions it relative to.modalWrapper, which provides a proper positioning context. The toolbar will not scroll with the modal content, and the layout works as intended.Likely an incorrect or invalid review comment.
61-64: The hardcodedtoolbarHeightcalculation is inaccurate in its breakdown but arrives at the correct value.The comment states: IconButton (48px) + padding (24px) = 72px
However, the actual values are:
- IconButton default size: 56px (not 48px)
- Toolbar padding:
var(--spacing4)= 16px (not 24px)This yields: 56px + 16px = 72px, which correctly matches the hardcoded value. Update the comment to reflect the actual component dimensions.
Likely an incorrect or invalid review comment.
| * @returns top position in pixels for use with position: absolute and transform | ||
| */ | ||
| export function useKeyboardAwarePosition(elementHeight: number) { | ||
| const [top, setTop] = useState(0); |
There was a problem hiding this comment.
Minor: Initial state may cause brief positioning flash.
The top state initializes to 0, and the correct position is calculated after the component mounts. This could cause a brief visual flash where the toolbar appears at the wrong position before jumping to the correct location.
Consider initializing with a calculated value or using useMemo to compute the initial position synchronously, though this would require accessing window conditionally (checking typeof window !== 'undefined').
🤖 Prompt for AI Agents
In @app/hooks/useKeyboardAwarePosition.ts at line 9, The hook initializes top to
0 which can cause a layout flash; change the useState initialization in
useKeyboardAwarePosition to use a function initializer that computes the initial
top synchronously (calling the same logic you use after mount) and guard any
window access with typeof window !== 'undefined' so SSR won't break; update
references to top/setTop as before and keep the existing effect/resize/keyboard
listeners but remove the initial jump by computing and returning the correct
initial position.
| function handleResize() { | ||
| // Use Visual Viewport API if available, fallback to window.innerHeight | ||
| const viewportHeight = | ||
| window.visualViewport?.height ?? window.innerHeight; | ||
| const scrollY = window.scrollY || 0; | ||
|
|
||
| // Calculate position: bottom of visible viewport minus element height | ||
| const newTop = viewportHeight + scrollY - elementHeight; | ||
| setTop(newTop); | ||
| } |
There was a problem hiding this comment.
Critical: Incorrect coordinate system mixing in position calculation.
The calculation mixes two incompatible coordinate systems:
visualViewport.heightis in visual viewport coordinates (the currently visible portion)window.scrollYis in layout viewport coordinates (the full page scroll)
When the Visual Viewport API is available, you should use visualViewport.offsetTop instead of window.scrollY to get the correct position of the visual viewport relative to the layout viewport.
🐛 Proposed fix
function handleResize() {
// Use Visual Viewport API if available, fallback to window.innerHeight
- const viewportHeight =
- window.visualViewport?.height ?? window.innerHeight;
- const scrollY = window.scrollY || 0;
+ const visualViewport = window.visualViewport;
+
+ if (visualViewport) {
+ // Visual Viewport API: use offsetTop + height for correct positioning
+ const newTop = visualViewport.offsetTop + visualViewport.height - elementHeight;
+ setTop(newTop);
+ } else {
+ // Fallback: use window dimensions
+ const scrollY = window.scrollY || 0;
+ const newTop = window.innerHeight + scrollY - elementHeight;
+ setTop(newTop);
+ }
-
- // Calculate position: bottom of visible viewport minus element height
- const newTop = viewportHeight + scrollY - elementHeight;
- setTop(newTop);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function handleResize() { | |
| // Use Visual Viewport API if available, fallback to window.innerHeight | |
| const viewportHeight = | |
| window.visualViewport?.height ?? window.innerHeight; | |
| const scrollY = window.scrollY || 0; | |
| // Calculate position: bottom of visible viewport minus element height | |
| const newTop = viewportHeight + scrollY - elementHeight; | |
| setTop(newTop); | |
| } | |
| function handleResize() { | |
| // Use Visual Viewport API if available, fallback to window.innerHeight | |
| const visualViewport = window.visualViewport; | |
| if (visualViewport) { | |
| // Visual Viewport API: use offsetTop + height for correct positioning | |
| const newTop = visualViewport.offsetTop + visualViewport.height - elementHeight; | |
| setTop(newTop); | |
| } else { | |
| // Fallback: use window dimensions | |
| const scrollY = window.scrollY || 0; | |
| const newTop = window.innerHeight + scrollY - elementHeight; | |
| setTop(newTop); | |
| } | |
| } |
🤖 Prompt for AI Agents
In @app/hooks/useKeyboardAwarePosition.ts around lines 12 - 21, In handleResize,
the position calculation mixes visual viewport and layout viewport coords; when
window.visualViewport exists use its offsetTop rather than window.scrollY —
compute const viewportOffset = window.visualViewport ?
window.visualViewport.offsetTop : window.scrollY || 0, use viewportHeight =
window.visualViewport ? window.visualViewport.height : window.innerHeight, then
set newTop = viewportHeight + viewportOffset - elementHeight and call
setTop(newTop); ensure null-safe access to window.visualViewport and preserve
the existing fallback behavior.
Use Visual Viewport API to dynamically position the modal toolbar above the mobile keyboard when it opens. Created useKeyboardAwarePosition hook that listens to viewport resize/scroll events and calculates the correct position. Applied only to phone screens using position: absolute with transform, while tablet/desktop retain the existing fixed positioning.
🤖 Generated with Claude Code
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.