fix(web): require user intent to cancel initial scroll (#1291)

* fix(web): require user intent to cancel initial scroll

* fix(web): snapshot scroll intent before consumption
This commit is contained in:
SSU-WEI HUANG
2026-08-02 09:27:37 +08:00
committed by GitHub
parent 545af9b4e0
commit f213bc348c
2 changed files with 31 additions and 6 deletions
@@ -200,7 +200,18 @@ describe('scroll anchor helpers', () => {
distanceFromBottom: 182,
isScrollingUp: true
})
expect(shouldCancelInitialScrollSettling(intent)).toBe(true)
expect(shouldCancelInitialScrollSettling(intent, true)).toBe(true)
})
it('keeps initial scroll settling for programmatic upward movement', () => {
const intent = getScrollIntent({
scrollTop: 0,
previousScrollTop: 700,
scrollHeight: 1232,
clientHeight: 530
})
expect(shouldCancelInitialScrollSettling(intent, false)).toBe(false)
})
it('keeps initial scroll settling for negligible movement at the bottom', () => {
@@ -215,7 +226,7 @@ describe('scroll anchor helpers', () => {
distanceFromBottom: 0,
isScrollingUp: false
})
expect(shouldCancelInitialScrollSettling(intent)).toBe(false)
expect(shouldCancelInitialScrollSettling(intent, false)).toBe(false)
})
it('restores the captured message to the same viewport offset', () => {
@@ -158,8 +158,13 @@ export function getScrollIntent(params: {
}
}
export function shouldCancelInitialScrollSettling(intent: ScrollIntent): boolean {
return intent.isScrollingUp && intent.distanceFromBottom > MANUAL_SCROLL_EPSILON_PX
export function shouldCancelInitialScrollSettling(
intent: ScrollIntent,
hasExplicitUpwardIntent: boolean
): boolean {
return hasExplicitUpwardIntent
&& intent.isScrollingUp
&& intent.distanceFromBottom > MANUAL_SCROLL_EPSILON_PX
}
export function captureScrollAnchor(viewport: HTMLElement): ScrollAnchor | null {
@@ -608,8 +613,16 @@ export function HappyThread(props: {
let wheelIntentUntil = 0
let wheelLatched = false
const hasExplicitUpwardIntent = (intent: ScrollIntent): boolean => {
return intent.isScrollingUp && (
pointerResumeActive
|| keyboardResumeUntil >= Date.now()
|| wheelIntentUntil >= Date.now()
)
}
const consumeExplicitUpwardIntent = (intent: ScrollIntent): boolean => {
if (!intent.isScrollingUp) {
if (!hasExplicitUpwardIntent(intent)) {
return false
}
if (pointerResumeActive && !pointerResumeLatched) {
@@ -658,10 +671,11 @@ export function HappyThread(props: {
// Keep the keyboard/pointer intent armed while the user moves
// through ordinary history. Consume it only when the viewport
// actually reaches the preload area.
const hadExplicitUpwardIntent = hasExplicitUpwardIntent(intent)
const explicitUpwardIntent = needsCoverage && consumeExplicitUpwardIntent(intent)
if (isInitialScrollSettling()) {
if (shouldCancelInitialScrollSettling(intent)) {
if (shouldCancelInitialScrollSettling(intent, hadExplicitUpwardIntent)) {
initialScrollDeadlineRef.current = 0
clearInitialScrollTimers()
setAutoScrollMode(false)