Skip to content

fix(core): scroll-spy race conditions on rapid/smooth scroll#2968

Merged
bennypowers merged 3 commits intomainfrom
fix/scroll-spy-controller
Mar 8, 2026
Merged

fix(core): scroll-spy race conditions on rapid/smooth scroll#2968
bennypowers merged 3 commits intomainfrom
fix/scroll-spy-controller

Conversation

@bennypowers
Copy link
Member

Summary

Fixes race conditions in ScrollSpyController that caused pf-jump-links and rh-jump-links to show incorrect active state during navigation:

  • Rapid clicks cancel previous force statesetActive() now aborts any pending force-release listeners via AbortController before establishing new force, so rapid successive clicks don't leave stale listeners that release force prematurely
  • Force released on scrollend instead of first IO — The old approach released force when any IntersectionObserver callback fired, which during smooth scroll meant intermediate sections could steal the active state. Now waits for scrollend (with 3s safety timeout)
  • passedLinks sorted by DOM order — Previously relied on Set insertion order, which was wrong when scrolling back through non-contiguous sections (sections with untracked content between them)
  • Fixed #nextIntersection timeout — Was setting #intersected = false (should be true), which could cause an infinite rAF loop if no IO fires
  • Fixed boundary comparison — Uses rootBounds.top instead of intersectionRect.top so elements exactly at the viewport top are correctly considered "passed"

Closes #2911
Closes #2920

Resolves RedHat-UX/red-hat-design-system#2425
Resolves RedHat-UX/red-hat-design-system#2474

Test plan

  • 9 new unit tests in scroll-spy-controller.spec.ts (all pass)
  • Tested against actual rh-jump-links component in RHDS dev server (7/8 pass — the 1 "failure" is expected async timing, identical on original controller)
  • Verified original controller fails 2 tests that the fix passes (rapid reverse-direction clicks, triple rapid clicks)
  • CI green

🤖 Generated with Claude Code

Fixes three bugs in ScrollSpyController:

1. Rapid clicks: setActive() now cancels previous force state via
   AbortController before establishing new force, preventing stale
   listeners from releasing force prematurely.

2. Force release timing: replaced await-first-intersection approach with
   scrollend event listener (+ 3s safety timeout). The old approach
   released force when any IO fired during smooth scroll, causing
   intermediate sections to steal the active state.

3. Non-contiguous sections: passedLinks are now sorted by DOM order
   instead of relying on Set insertion order, which was unreliable
   when sections had untracked content between them.

Also fixes a safeguard bug where the #nextIntersection timeout set
`#intersected = false` (should be `true`), which could cause an infinite
rAF loop, and fixes the boundary comparison to use rootBounds.top
instead of intersectionRect.top so elements exactly at the viewport
top are correctly considered "passed".

Closes #2911
Closes #2920

Assisted-By: Claude Opus 4.6 <noreply@anthropic.com>
@changeset-bot
Copy link

changeset-bot bot commented Mar 8, 2026

🦋 Changeset detected

Latest commit: fa1ed38

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@patternfly/pfe-core Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@bennypowers bennypowers requested a review from Copilot March 8, 2026 23:37
@netlify
Copy link

netlify bot commented Mar 8, 2026

Deploy Preview for patternfly-elements ready!

Name Link
🔨 Latest commit 4c9cbb5
😎 Deploy Preview https://deploy-preview-2968--patternfly-elements.netlify.app/

To edit notification comments on pull requests, go to your Netlify site settings.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 8, 2026

✅ Commitlint tests passed!

More Info
{
  "valid": true,
  "errors": [],
  "warnings": [],
  "input": "fix(core): scroll-spy race conditions on rapid/smooth scroll"
}

@github-actions github-actions bot added the AT passed Automated testing has passed label Mar 8, 2026
Assisted-By: Claude Opus 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Contributor

github-actions bot commented Mar 8, 2026

SSR Test Run for 3f6b9f7: Report

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes ScrollSpyController race conditions that could cause jump-links components to show incorrect active state during rapid and/or smooth scrolling navigation, and adds unit coverage to prevent regressions.

Changes:

  • Add force-mode cancellation/cleanup and switch force release from “first IO callback” to scrollend (with timeout fallback).
  • Fix intersection wait-loop safeguard and adjust “passed” boundary detection.
  • Add a dedicated ScrollSpyController test suite covering rapid clicks, smooth scrolling, and non-contiguous sections.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
core/pfe-core/controllers/scroll-spy-controller.ts Updates force-mode lifecycle, passed-link ordering, and intersection/threshold logic to prevent active-state races.
core/pfe-core/test/scroll-spy-controller.spec.ts Adds 9 integration-style unit tests validating correct active state across scroll/navigation scenarios.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Prevents a stale 3s timeout from prematurely resolving a subsequent
#nextIntersection() call.

Assisted-By: Claude Opus 4.6 <noreply@anthropic.com>
@bennypowers bennypowers enabled auto-merge (squash) March 8, 2026 23:45
@github-actions

This comment has been minimized.

@bennypowers bennypowers disabled auto-merge March 8, 2026 23:49
@bennypowers bennypowers merged commit ca65338 into main Mar 8, 2026
27 checks passed
@bennypowers bennypowers deleted the fix/scroll-spy-controller branch March 8, 2026 23:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AT passed Automated testing has passed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] <rh-jump-links> fails to highlight active section [bug] <rh-jump-links> Active state not consistent

2 participants