Skip to content

Derive overflow from root-scoped IntersectionObserver entries instead of offsetTop reads#8030

Merged
mattcosta7 merged 8 commits into
underline-nav-shared-observer-coalesced-rebuildfrom
copilot/underline-nav-shared-observer-coalesced-rebuild
Jun 23, 2026
Merged

Derive overflow from root-scoped IntersectionObserver entries instead of offsetTop reads#8030
mattcosta7 merged 8 commits into
underline-nav-shared-observer-coalesced-rebuildfrom
copilot/underline-nav-shared-observer-coalesced-rebuild

Conversation

Copilot AI commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Follow-up to the shared-overflow-observer work. The shared IntersectionObserver in the descendant registry now derives overflow directly from observer entries (isIntersecting / intersectionRatio) and is scoped to the clipping container via a new root, instead of being used only as a trigger to re-read offsetTop > 0. This closes the layout-thrash path (no synchronous offsetTop reads) and removes the per-item IntersectionObserver fallback.

Base branch is underline-nav-shared-observer-coalesced-rebuild, so this diff is scoped to the changes on top of that work — not main.

Changelog

New

  • ProviderProps accepts a rootRef that is passed to the shared IntersectionObserver as its root, scoping intersection to the actual clipping container (the ActionBar overflow container / UnderlineNav item list).
  • getIsOverflowing(entry) derives overflow state from the observed entry: any target that isn't fully visible (!isIntersecting || intersectionRatio < 1) counts as overflowing, guarding sub-pixel boundary cases.

Changed

  • The shared observer callback now computes isOverflowing per entry and fans that boolean out to subscribers, rather than firing a bare onChange() that forced an offsetTop re-read.
  • ObserveFn / useRegisterOverflowObserver now propagate the overflow boolean (onOverflowChange(isOverflowing)); useSyncExternalStore reads a tracked isOverflowingRef instead of ref.current.offsetTop > 0.
  • Observer is created with {root, threshold: [0, 1]}; when the root element changes, the provider re-observes all currently-subscribed elements against the new observer instance (observeSubscribedElements + an isomorphic layout effect).
  • ActionBar and UnderlineNav pass rootRef to the registry Provider (overflowContainerRef / listRef).
  • The registry overflow option is simplified from {threshold?: number} to an object flag — the explicit threshold: 1 / threshold: 0.75 configs on ActionBar and UnderlineNav are dropped, since overflow is now read from the entry rather than tuned via threshold.
  • SSR/availability guard extracted to supportsIntersectionObserver().

Removed

  • Per-item local IntersectionObserver fallback: the hook now tracks overflow only through the provider's shared observer and is inert (false) when no shared observer is configured.
  • All synchronous offsetTop layout reads from the overflow path (and the related code comments describing the offsetTop-trigger model).
  • The 0.75 threshold workaround comment in ActionBar (no longer needed without threshold tuning).

Rollout strategy

  • Patch release
  • Minor release
  • Major release; if selected, include a written rollout or migration plan
  • None; if selected, include a brief description as to why

Testing & Reviewing

Internal-only refactor; no public API or behavior change. Overflow detection moves from an offsetTop-trigger model to reading the observed IntersectionObserverEntry directly.

packages/react/src/utils/__tests__/descendant-registry.test.tsx is updated to match:

  • The mock observer now captures its options and exposes emit(entries) so tests can drive real entry shapes (target / isIntersecting / intersectionRatio) instead of a bare trigger().
  • New assertion that the single shared observer is created with the provided root element.
  • New test that an item updates from the observed entry without reading layoutoffsetTop is stubbed to throw to prove it is never read.
  • Existing fan-out, unobserve-on-unmount, and disconnect-on-provider-unmount coverage is retained (plus an explicit unobserve assertion).

Perf characteristics worth noting during review:

  • Shared observer, coalesced microtask rebuilds, and per-item useSyncExternalStore are all preserved — siblings don't re-render when one item's overflow flips.
  • Overflow is now derived from observer entries with no synchronous offsetTop/layout reads, so the layout-thrash path stays closed; the fallback removal prevents reintroducing an offsetTop-style local observer.
  • The registry-rebuild cascade (key bump forcing re-registration on membership/order change) is the main remaining cost and is unchanged here.

Merge checklist

@changeset-bot

changeset-bot Bot commented Jun 23, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 7b2cead

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes changesets to release 1 package
Name Type
@primer/react Patch

Click here to learn what changesets are, and how to add one.

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

Copilot AI and others added 4 commits June 23, 2026 01:40
Co-authored-by: mattcosta7 <8616962+mattcosta7@users.noreply.github.com>
Co-authored-by: mattcosta7 <8616962+mattcosta7@users.noreply.github.com>
Co-authored-by: mattcosta7 <8616962+mattcosta7@users.noreply.github.com>
Co-authored-by: mattcosta7 <8616962+mattcosta7@users.noreply.github.com>
Copilot AI changed the title [WIP] Optimize IntersectionObserver usage in shared registry Use root-scoped IntersectionObserver entries for overflow detection Jun 23, 2026
Copilot AI requested a review from mattcosta7 June 23, 2026 01:57
Co-authored-by: mattcosta7 <8616962+mattcosta7@users.noreply.github.com>
Copilot AI changed the title Use root-scoped IntersectionObserver entries for overflow detection Drop per-item overflow observer fallback; use callback-ref shared observer Jun 23, 2026
Copilot AI and others added 2 commits June 23, 2026 12:02
Co-authored-by: mattcosta7 <8616962+mattcosta7@users.noreply.github.com>
Co-authored-by: mattcosta7 <8616962+mattcosta7@users.noreply.github.com>
Copilot AI changed the title Drop per-item overflow observer fallback; use callback-ref shared observer Keep per-item observer fallback removal; revert callback-ref conversion Jun 23, 2026
@mattcosta7 mattcosta7 changed the title Keep per-item observer fallback removal; revert callback-ref conversion Use a shared intersection observer instead of per item observers Jun 23, 2026
@mattcosta7 mattcosta7 changed the title Use a shared intersection observer instead of per item observers Derive overflow from root-scoped IntersectionObserver entries instead of offsetTop reads Jun 23, 2026
@mattcosta7 mattcosta7 marked this pull request as ready for review June 23, 2026 14:50
@mattcosta7 mattcosta7 requested a review from a team as a code owner June 23, 2026 14:50
@mattcosta7 mattcosta7 requested a review from jonrohan June 23, 2026 14:50
@mattcosta7 mattcosta7 merged commit 13d8541 into underline-nav-shared-observer-coalesced-rebuild Jun 23, 2026
45 of 50 checks passed
@mattcosta7 mattcosta7 deleted the copilot/underline-nav-shared-observer-coalesced-rebuild branch June 23, 2026 14:50
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