Skip to content

fix(sheets): pin and un-pin the page in the same commit as the sheet, and never scroll for a sheet's own history pop - #257

Merged
DuarteSantos8 merged 1 commit into
DuarteSantos8:mainfrom
kurktchiev:gh/sheet-close
Sep 28, 2026
Merged

DuarteSantos8 merged 1 commit into
DuarteSantos8:mainfrom
kurktchiev:gh/sheet-close

Conversation

@kurktchiev

@kurktchiev kurktchiev commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

One commit. Two small changes that have to go together, because each on its own only moves the jump
around.

What breaks

Reported from a phone, mid-session. Picking an RIR level, one tap that closes the picker sheet and
ticks the set, made the page "jump down to the bottom and back up to the exercise". A plain tick did
not, so it was the sheet.

Modals pins the body behind a sheet (position: fixed; top: -y) and puts the scroll back when the
sheet goes. Both ran in a passive effect, after paint. iOS applies scrolls asynchronously, so there
was a frame with the body un-pinned at scroll 0 before the restore, and the 350 ms keyboard retry then
pulled the page back up. That is the jump, and back.

App.jsx made it worse from the other side. Its back-navigation scroll restore fires on every POP,
and Modals pushes a history entry per sheet so the system back button closes it. Closing a sheet is
therefore a POP that lands on the screen you are already on, and the restore read the recorded
position a frame later and added a scroll of its own on top of the sheet's.

Reproduce

On iOS, scrolled some way down the workout screen, tick a set with effort ratings on. The picker
opens, you pick, and the page jumps to the bottom and comes back.

What changed

  • The pin is a useLayoutEffect, so the un-pin and the restore land in the same frame as the sheet
    leaving. There is no frame at scroll 0 to see.
  • The restore is said once more on the next animation frame, guarded on the body no longer being
    pinned, because iOS can apply a scroll issued in the same task a frame late or against the
    still-short layout.
  • App.jsx ignores a POP that lands on the same path. That is a sheet's history entry going away, not
    a page change, and it has no scroll of its own to restore.

The 350 ms keyboard retry that was already there stays; a sheet closed with the keyboard still up (tap
"+" in the picker, then finish) makes iOS scroll the page again after everything above has run.

Tests

Modals.test.jsx, one case: the body is pinned on open with the scroll position folded into top,
and on close the scroll is restored three times over, in order. At once, in the same commit; again on
the next animation frame; and again after the keyboard's dismiss animation. Against main it fails on
the second of those, because the passive effect only ever issued one.

Desktop WebKit paints the un-pin and the restore together, so it does not show the symptom. The test
pins the order rather than the pixels.

Checklist

  • npx vitest run passes in frontend/: 1585 passing across 126 files (main is 1584; +1)
  • npm test in api/ untouched by this PR
  • No new runtime dependency
  • No user-facing strings, so no locale change
  • CHANGELOG.md is left alone

🤖 Generated with Claude Code

…tory pop

Reported from a phone, mid-session. Picking an RIR level, one tap that closes
the picker sheet and ticks the set, made the page "jump down to the bottom and
back up to the exercise". A plain tick did not, so it was the sheet. Modals pins
the body behind a sheet (position:fixed; top:-y) and puts the scroll back when
the sheet goes; both ran in a passive effect, after paint. iOS applies scrolls
asynchronously, so it showed a frame un-pinned at scroll 0 before the restore,
with the 350 ms keyboard retry pulling it back.

The pin is a layout effect now, so the un-pin and the restore land in the same
frame as the sheet leaving, and the restore is said once more on the next frame.
App.jsx's back-navigation scroll restore also ignores a POP that lands on the
same screen. That is a sheet's history entry going away (Modals pushes one per
sheet for the system back button), not a page change, and the recorded position
a frame later only added a scroll of its own.

Desktop WebKit paints the un-pin and the restore together, so it does not show
the symptom; the test pins the order instead: restore at once, on the next
frame, and after the keyboard's animation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@DuarteSantos8
DuarteSantos8 merged commit 463c9d9 into DuarteSantos8:main Sep 28, 2026
4 checks passed
@DuarteSantos8

Copy link
Copy Markdown
Owner

Thanks @kurktchiev, no more jump on iOS when a sheet closes.

Released in v1.3.9: https://github.com/DuarteSantos8/openGym/releases/tag/v1.3.9

@kurktchiev
kurktchiev deleted the gh/sheet-close branch September 29, 2026 11:46
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.

2 participants