Hacktoberfest 2026 : les issues que les mainteneurs ont marquées pour octobre, ouvertes et accessibles aux débutants. Parcourir les issues Hacktoberfest

iOS scroll-correction deferral: per-row record, iOS 27 momentum, and index-scroll exits

Ouverte
#1,307 7 commentaires 0 réactions 0 personnes assignées Voir sur GitHub

Les mainteneurs répondent en général sous 1 jour

Personne n'a encore pris cette issue.

Évaluation

Difficulté
5/5
Temps estimé
Plus d'une semaine
Accessibilité débutants
25/100
Type d'issue
Bug
Clarté
Plutôt claire
Activité
Active
Stack technique
react, typescript

Piste de recherche

Start with the implementation paths that maintain scroll offsets, defer iOS corrections, and retarget index scrolls, then read the failing cases and event scripts in #1308 and the proposed differential test in tests/ios-differential.test.ts. The plan splits the work into separate PRs, each with its own tests and device checks; first determine which step is available and follow its named failure. Done means that step's test.fails cases pass, the option sweep and required suites are green, and the specified iOS device checks are complete.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Description

Plan for #1307

The anchor proposal in the issue is dropped: it needs history under momentum and changes which resizes get compensated. This plan is built from the measurements in this thread (iOS 18.2, 26.5 and 27.0 simulators, iPhone on 27.0.1) and from the tests pinned in #1308.

What we know

Phase iOS ≤ 26 iOS 27+
Finger down a scrollTop write is ignored a scrollTop write is ignored
Momentum a write stops the momentum the write lands and momentum continues
  • The deferral is needed under a finger on every version, and during momentum only on ≤ 26. On 27+ deferring during momentum just leaves the content shifted until the flush.
  • Detecting 27+: the UA's OS token is frozen (iPhone OS 18_7 on 26 and 27; iPad reports a Mac). Version/ is real in Safari but likely missing in in-app web views and Chrome/Firefox on iOS. CSS.supports('overflow-anchor: auto') is true only on 27 in the measured versions, doesn't read the UA, and is false (keep deferring) when absent.
  • One accumulated delta loses information the flush needs: a correction is replayed after the gesture has carried the viewport above the resized row (the reverse flick).
  • Several bugs aren't iOS-specific: the stale scrollOffset before a write's read-back, and the pull-back of an index scroll that never reached its target.

Steps

Each step is its own PR, fixes named test.fails from #1308 (removing .fails), and leaves the rest green. Each runs a full option sweep (align, lanes, padding, scrollPaddingStart, scrollMargin, horizontal, custom adjust predicate, anchorTo, prepend, iOS UA) before it is opened, test:pr, and the chat e2e suites repeatedly.

1. Stale scrollOffset before a write's read-back (all platforms): #1309

While a scroll command's write hasn't been read back, resizes are left to reconcile instead of being compensated against the old offset; scrollBy builds on the written offset.

  • Fixes: "scrollToIndex (#1307): a resize classified against the stale offset does not overwrite the index scroll".
  • Adds tests/ios-differential.test.ts (same event script on iOS and non-iOS, compares where they settle, flags a reconcile that needs the safety valve).
2. Pull-back of an index scroll that never landed (all platforms): #1310

A scroll event that isn't the read-back of our own write ends the index scroll whether or not it reached its target (today only after hasReachedTarget). On iOS this also covers the finger-down case, where the write is ignored and the scroll can never land.

  • Fixes: "a drag that never reached the target is not pulled back when the target moves", "a scroll away from a target that moved before landing is not pulled back, non-iOS".
  • Watch: smooth scrolls produce intermediate scroll events that aren't our read-back, and a smooth retarget resets hasReachedTarget for that reason. They need their own rule, likely "a smooth scroll is interrupted only by an event moving away from the target".
2a. Refactor: the scroll state as an explicit phase (no behaviour change): #1311

ScrollState described its lifecycle through flags: behavior (switched from 'smooth' to 'auto' by reconcile for the final approach, which also re-enables size compensation), hasReachedTarget, and since #1310 startedSmooth (WebKit delivers smooth-animation events after that switch). Their combinations encode four situations, so they become one phase:

smooth           a smooth animation is travelling to the target
smoothLanded     a smooth scroll is at its target, waiting to retire
smoothFinishing  a smooth scroll switched to instant writes for the final
                 approach; its animation can still report positions
instant          instant writes

Checks ask about the phase through two helpers: isSmoothScroll (compensation, anchor sync and far measurements hold off) and isAnimating (a scroll event that isn't our read-back may be the animation, not the user).

  • Behaviour-preserving: identical traces against the parent on 3,000 random event sequences, plus the #1308 tests, tests/ios-differential.test.ts, and the react-virtual smooth/scroll/cached-measurements e2e in Chromium, Firefox and WebKit.
  • Before step 4, which reworks the iOS deferral state anyway.
2b. Defer only for touch-driven scrolls (all iOS versions): next

Today the deferral is gated on isScrolling, which any scroll event sets, including the read-back of our own programmatic write. So a scrollToOffset landing paints sagged until scrolling settles and then snaps (#1250). Since #1305 a scrollToIndex landing sags for one frame until reconcile retargets. A programmatic scroll has no momentum to protect; only a touch does.

  • Gate the deferral (in applyScrollAdjustment, the prepend anchor sync, and the flush) on touch provenance: a finger is down, or the momentum tail after touchend. While the tail is open, each scroll event re-arms its timer, so it spans the fling and closes itself after the last frame; a scroll event with no preceding touch never opens it.
  • An absolute scroll command closes the tail (its write stops momentum on ≤ 26 anyway); touchcancel is handled like touchend.
  • Regression tests carried over from #1280: a tap that doesn't scroll must not latch the deferral, and a landing issued from a tap handler compensates before paint.
  • Fixes #1250. Gives step 3 a precise "momentum phase" to gate on instead of isScrolling.
  • A prototype of the gate on top of #1311 makes all 12 known iOS/non-iOS divergences in tests/ios-differential.test.ts converge and fixes three #1308 test.fails (cancelScroll, the safety valve, scrollBy replacing an index scroll). About 15 existing iOS tests model momentum as scroll events without touch events and need rewriting with explicit touchstart / touchend.
  • #1305's "drop corrections during an index scroll" branch only stays reachable while a finger is down, where #1310 already ends the index scroll on the finger's first scroll event. Remove it if the tests and the harness agree.
  • Taken from #1280, which bundled this with the prepend fix in step 5 and is closed in favour of this plan.
3. iOS 27+: don't defer during momentum (small, may ride with 2b)

Defer under a finger and in the post-touchend grace window as today; during momentum on 27+, write the correction immediately like every other platform.

  • Gate: isIOSWebKit() && CSS.supports('overflow-anchor: auto'), optionally confirmed by Version/ >= 27. Absent or false keeps today's behaviour.
  • Check first, with the #1308 page on the 27 simulator and the device: whether Safari 27's native scroll anchoring adjusts anything itself for transform-positioned rows (it would double the correction), and whether the virtualizer should set overflow-anchor: none on the scroller.
  • Tests: jsdom with CSS.supports stubbed for both answers; the existing touch-phase tests must pass unchanged.
  • With 2b's explicit momentum tail this is one condition on that tail.
  • Shrinks step 4: on 27+ only the finger-down phase still defers.
4. Per-row deferral record (conditional: decide after 2b and 3)

After 2b and 3, what's left for this step is iOS ≤ 26 during real touch momentum and drags, chiefly the reverse flick. Decide with the #1308 device page whether it is worth the complexity or whether accepting that case on older iOS is the better trade.

Replace _iosDeferredAdjustment with a record of the deferred corrections per row (key → delta, using the same predicate as today), plus prepend shifts.

  • At flush, apply only the corrections for rows that are still above the current viewport. That fixes the reverse flick and keeps momentum travel correct.
  • During an index scroll keep recording instead of dropping (#1305). A reconcile retarget write absorbs everything recorded before it, so it clears the record. Exits that end the scroll without landing (gesture, cancelScroll, scrollBy, safety valve) leave the record for the flush.
  • Fixes: "a correction is not replayed once the viewport is above the resized row", and the index-scroll exits: "a gesture before the index scroll retargets…", "cancelScroll before the index scroll retargets…", "the reconcile safety valve…", "scrollBy replacing an index scroll…".
  • Keeps: the five behaviour tests from #1308 (momentum baseline, forward vs backward re-measure, custom predicate opting out, first measure of the row spanning the viewport top, non-iOS gesture).
  • Test rework: 31 assertions on main read _iosDeferredAdjustment. The ones that pin what was deferred move to a small read-only accessor or to position assertions. Two #1308 test.fails have an intermediate check (_iosDeferredAdjustment === 50, an intermediate 2550) that would keep them green for the wrong reason; drop those before relying on them.
  • The differential harness gets one documented exception: iOS skips corrections for rows that ended below the viewport, which non-iOS applied at resize time.
5. Shared stalls and prepend fixes (separate PRs)

Found while reviewing steps 1 and 2, same on main:

  • scrollToOffset / scrollBy, then a row above grows after the read-back: the compensation moves off the fixed target and reconcile never rewrites it until the safety valve.
  • A prepend (anchorTo: 'end') before the read-back of a scrollToIndex: the anchor sync writes from the stale offset and overrides the index scroll until the safety valve.
  • A deferred prepend correction is applied twice (from #1280): setOptions folds the anchor delta into the tracked scrollOffset eagerly, and when the DOM sync is deferred under a finger, the flush writes scrollOffset + deferred. A fling to the top that lands on a prepend jumps by the prepend's height.

Related issues

  • #1250: fixed by step 2b (partly by #1305 already: scrollToIndex sags for one frame instead of snapping later; scrollToOffset still snaps).
  • #884: the reason the deferral exists; addressed on main since @tanstack/virtual-core 3.15.0 (#1168). On iOS 27 a write no longer stops momentum at all (step 3).
  • #1287: out of scope. Desktop Safari discards a write made during the rubber-band bounce; no touch and no iOS deferral are involved, so it needs its own fix.

Verification on devices

The #1308 page (react-virtual/e2e/app/ios-deferral) is the manual check for steps 2–4: the 18.2, 26.5 and 27.0 simulators and a 27 device, with real gestures, for the momentum baseline, the reverse flick, a resize under a finger, and scrollToIndex during momentum and under a finger. Not yet covered by anyone: in-app WKWebViews, Chrome/Firefox on iOS, and iOS versions between 26.5 and 27.0.

Open questions

  • Does Safari 27's native scroll anchoring touch transform-positioned rows? (Decides whether step 3 also needs overflow-anchor: none.)
  • Does CSS.supports('overflow-anchor: auto') hold in WKWebView and in Chrome/Firefox on iOS 27?
  • For step 2, should a user scroll during a smooth scrollToIndex cancel it? Today it doesn't until the target is reached.
Langage dominant
TypeScript
Étoiles
7.1k
Forks
472
Merge moyen
1 j 14 h
PR mergées (30 j)
18

Préparer son environnement

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Autres issues de TanStack/virtual

Toutes les issues de TanStack/virtual

Issues similaires

Plus d'issues TypeScript

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.