Files
fusion/docs/solutions/ui-bugs/mobile-ios-restore-document-scroll-drift.md
gsxdsm 38007db549 FN-6364: reset iOS mobile viewport on restore
Recover stale iOS document scroll after returning to the Fusion dashboard.

- Add an iOS-only restore hook that clears orphaned body offsets and scrolls the document back to the origin when unlocked.
- Wire the restore reset from the dashboard app shell for mobile layouts.
- Cover visible/page-show restores, platform no-ops, active-lock guards, and orphaned style cleanup.
- Document the mobile restore drift solution for future regressions.

Files changed:
 .../mobile-ios-restore-document-scroll-drift.md    |  59 ++++++++++
 packages/dashboard/app/App.tsx                     |   4 +-
 .../hooks/__tests__/useMobileScrollLock.test.ts    | 121 ++++++++++++++++++++-
 .../dashboard/app/hooks/useMobileScrollLock.ts     |  61 +++++++++++
 4 files changed, 243 insertions(+), 2 deletions(-)

Fusion-Task-Id: FN-6364

Fusion-Task-Lineage: 9243cab8-5fbb-4332-ac4d-aabefde4161c
2026-06-13 10:29:24 -07:00

3.1 KiB

title, date, category, module, problem_type, component, applies_when, symptoms, root_cause, resolution_type, severity, related_components, tags
title date category module problem_type component applies_when symptoms root_cause resolution_type severity related_components tags
Mobile iOS restore document scroll drift 2026-06-13 ui-bugs packages/dashboard/app/hooks/useMobileScrollLock ui_bug frontend_mobile_layout An iOS Safari/PWA dashboard tab is restored from background or bfcache after the document has stale scroll or orphaned body offset.
Returning to Fusion on iOS can leave the header/board pushed above the top of the screen
A large empty gap appears at the bottom even though the soft keyboard is down
The dashboard resting layout should have document scroll at the origin because body overflow is hidden
ios_restore_left_stale_document_scroll_or_body_offset code_fix medium
packages/dashboard/app/App.tsx
packages/dashboard/app/hooks/useMobileScrollLock.ts
packages/dashboard/app/hooks/useMobileKeyboard.ts
FN-6362
FN-6364
ios-safari
mobile-keyboard
document-scroll
visualviewport
bfcache

Mobile iOS restore document scroll drift

Problem

On iOS Safari/PWA, switching away from Fusion and returning can leave the layout viewport visually misaligned with the dashboard. The document may retain window.scrollY > 0, or a stale inline body offset from an earlier lock, even though Fusion's base shell uses body { overflow: hidden } and the resting document scroll position should be (0, 0).

The visible symptom is the board/header appearing shifted upward with an empty gap at the bottom after foregrounding the app, including cases where no input is currently focused.

Solution

Keep keyboard metrics recovery and document-scroll recovery as separate concerns:

  • FN-6362 resets useMobileKeyboard metrics on visibilitychange/pageshow so --vv-offset-top consumers stop seeing a stale keyboard-open state.
  • FN-6364 adds useMobileViewportRestoreReset in useMobileScrollLock.ts and wires it once from App.tsx for mobile layouts.

The restore hook only runs on iOS mobile devices. On document.visibilitychange it acts only when document.visibilityState === "visible", and on window.pageshow it handles normal and bfcache restores. If no fullscreen scroll lock or keyboard viewport lock is active, it clears orphaned body fixed-position offset styles and calls window.scrollTo(0, 0) when stale document scroll is present.

Do not run this reset on Android or desktop, and do not run it while useMobileScrollLock or useMobileKeyboardViewportLock is active; live locks own their own restore path.

Regression coverage

Cover the invariant at the useMobileScrollLock hook seam:

  • iOS mobile + visibilitychange to visible + scrollY > 0 calls scrollTo(0, 0).
  • iOS mobile + pageshow with persisted: false calls scrollTo(0, 0).
  • Android and desktop restore events are no-ops.
  • visibilitychange to hidden is a no-op.
  • Active fullscreen scroll locks and keyboard viewport locks prevent the restore hook from fighting the live lock.
  • scrollY === 0 is idempotent.
  • Orphaned body position: fixed / top offset is cleared only when no lock is active.