Skip to content

NXT-21053: Fix VirtualList native scroll animation stopping at an outdated position - #3468

Merged
vJIYEv merged 5 commits into
release/5.6.x.developfrom
feature/NXT-21053
Nov 6, 2026
Merged

vJIYEv merged 5 commits into
release/5.6.x.developfrom
feature/NXT-21053

Conversation

@vJIYEv

@vJIYEv vJIYEv commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Checklist

  • I have read and understand the contribution guide
  • A CHANGELOG entry is included
  • At least one test case is included for this feature or bug fix
  • I have run automated testing and it is passed
  • Documentation was added or is not needed
  • This is an API breaking change

Issue Resolved / Feature Added

With scrollMode="native", holding a 5-way key (long press) on a horizontal/vertical VirtualList can leave the list scrolled short of the focused item. The focus reaches the first/last item but the list stops at an intermediate position, so the focused item is clipped or completely out of the viewport.

It happens when frames are delayed (e.g. a busy device right after boot), so it is intermittent.

Resolution

VirtualListBasic.animateScroll() started a new requestAnimationFrame loop on every call without stopping the ongoing one. During key repeat, several loops ran concurrently:

  • each loop keeps scrolling toward its own (older) target, and
  • when frames are delayed, a stale loop hits the 500ms timeout and its fallback node.scrollTo({left/top: <old target>, behavior: 'smooth'}) overrides the latest target.

Observed on a device (horizontal list, max scroll 745.5): scroll requests were 204 → 384.5 → 565 → 745.5, but stale fallbacks re-issued scrollTo(204), scrollTo(384.5), scrollTo(565) after the final request, and the list ended at 385/565.

The fix keeps a single animation loop and retargets it instead of starting another one:

  • A new request only updates the target position and the timeout start time. A loop starts only when none is running, so the loop and its timeout fallback always use the latest target.
  • The step per frame is scrollFactor × (number of items left to scroll). A single key press scrolls at the same speed as before. While a key is held, the speed keeps up with the focus, like the overlapping loops did before, without their stale targets.

The first revision of this PR cancelled the ongoing animation before starting a new one. Based on review feedback, it was changed because:

  1. Long-press scrolling became too slow to keep up with the focus.
  2. cancelScrollAnimation() set scrolling to false until the next frame, so onScrollEnd in ui/useScroll was no longer blocked and onScrollStop fired in the middle of a long-press scroll. With retargeting, the loop is never interrupted and scrolling stays true.

Additional Considerations

  • Tests (VirtualList-native-specs.js): retargeting keeps scrolling true, reaching the latest target, timeout fallback to the latest target, timeout restart on a new request, and faster steps while several items are left. The retarget and speed tests fail with the first revision.
  • Before/after comparison with a test app (horizontal VirtualList, 14 items, scrollMode="native", 4K viewport, long press with 50ms key repeat; frames delayed by deferring requestAnimationFrame):
Scenario Before (5.6.0) 1st revision (cancel) This PR (retarget)
Frame delay 50ms, long press → FAIL (stops short) PASS PASS
Frame delay 50/100ms, long press ← FAIL (stops short) PASS PASS
Max px of the focused item hidden while holding (delay 50ms, →) ~880 1123 821
onScrollStop while holding (no delay, → / ←) 0 / 1 3 / 4 0 / 1
40 items, 4s hold, no delay: focused index at release / onScrollStop while holding 31 / 0 24 / 15 31 / 0
  • ui/Scroller animateScroll has the same pattern (no cancel before restart); not changed here.

Links

NXT-21053

Comments

Enact-DCO-1.0-Signed-off-by: Kyuman Lim (kyuman.lim@lge.com)

🤖 Generated with Claude Code

@codecov

codecov Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 86.33%. Comparing base (6231f2e) to head (2d9b7ef).

Additional details and impacted files
@@                    Coverage Diff                    @@
##           release/5.6.x.develop    #3468      +/-   ##
=========================================================
+ Coverage                  86.31%   86.33%   +0.01%     
=========================================================
  Files                        156      156              
  Lines                       7615     7624       +9     
  Branches                    2013     2012       -1     
=========================================================
+ Hits                        6573     6582       +9     
  Misses                       831      831              
  Partials                     211      211              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@vJIYEv vJIYEv closed this Oct 29, 2026
@bongsok bongsok reopened this Nov 6, 2026
kyuman and others added 4 commits November 6, 2026 17:54
…estarting it

Cancelling the ongoing animation on every request made the scroll too slow
to keep up with the focus while a 5-way key is held, and it reset
`scrolling` to false until the next frame, so `onScrollEnd` could fire
`onScrollStop` in the middle of a long-press scroll.

Keep a single animation loop alive and update its target (and timeout start)
on each request, and scale the step by the number of items left to scroll so
the speed keeps up with the focus. A single key press scrolls at the same
speed as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The previous rAF mock kept only the last callback, so the stale concurrent
loop from before the fix was never run and the tests passed without the fix.

Also fix the CHANGELOG wording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vJIYEv
vJIYEv merged commit 010b6ab into release/5.6.x.develop Nov 6, 2026
7 checks passed
@vJIYEv
vJIYEv deleted the feature/NXT-21053 branch November 6, 2026 10:04
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.

3 participants