Skip to content

fix mobile editor toolbar covered by keyboard#111

Open
NolanPic wants to merge 1 commit intomainfrom
np/fix-editor-toolbar
Open

fix mobile editor toolbar covered by keyboard#111
NolanPic wants to merge 1 commit intomainfrom
np/fix-editor-toolbar

Conversation

@NolanPic
Copy link
Copy Markdown
Owner

@NolanPic NolanPic commented Jan 10, 2026

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

  • New Features
    • Improved mobile keyboard interaction: The interface now intelligently repositions elements on mobile devices when the on-screen keyboard appears, keeping important content and toolbar visible. This enhancement improves usability when filling out forms or entering information.

✏️ Tip: You can customize this high-level summary in your review settings.

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]>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jan 10, 2026

Walkthrough

A 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

Cohort / File(s) Summary
Keyboard-aware positioning hook
app/hooks/useKeyboardAwarePosition.ts
New hook that computes top offset for element positioning above the mobile keyboard using Visual Viewport API with window.innerHeight fallback. Listens to viewport resize/scroll and window resize events, updating position dynamically.
Modal toolbar positioning
app/components/common/Modal.tsx
Integrated useKeyboardAwarePosition hook with hardcoded toolbar height (72px) and padding (16px). Applied conditional inline styles for mobile to position toolbar absolutely with transform translation based on calculated offset.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 A keyboard appears, the viewport shrinks low,
Our hook watches, calculates where things should go,
With Visual Viewport's keen eye up above,
The toolbar now dances with keyboard-aware love,
Mobile users rejoice, no more hidden views! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix mobile editor toolbar covered by keyboard' directly and accurately summarizes the main change: positioning the editor toolbar above the keyboard on mobile devices.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between f8b40a8 and 880bdd7.

📒 Files selected for processing (2)
  • app/components/common/Modal.tsx
  • app/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 .modal within .modalWrapper (which has position: 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 hardcoded toolbarHeight calculation 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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +12 to +21
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);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Incorrect coordinate system mixing in position calculation.

The calculation mixes two incompatible coordinate systems:

  • visualViewport.height is in visual viewport coordinates (the currently visible portion)
  • window.scrollY is 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.

Suggested change
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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant