fix: Safari UnderlineNav zero-size root race in OverflowObserverProvider#8183
fix: Safari UnderlineNav zero-size root race in OverflowObserverProvider#8183mattcosta7 with Copilot wants to merge 4 commits into
Conversation
🦋 Changeset detectedLatest commit: 4589f21 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
Co-authored-by: mattcosta7 <8616962+mattcosta7@users.noreply.github.com>
…ove ResizeObserver mock Co-authored-by: mattcosta7 <8616962+mattcosta7@users.noreply.github.com>
|
There was a problem hiding this comment.
Pull request overview
This PR addresses a Safari-specific IntersectionObserver race where the initial callback can run before the clipping root has a real layout box (clientWidth === 0), causing UnderlineNav (and other useIsClipped consumers) to incorrectly mark all items as overflowing and get stuck in the “More” menu state.
Changes:
- Add a guard to ignore
IntersectionObserverbatches delivered against a zero-width root. - Add a root-only
ResizeObserverto re-run observation when the root transitions from0 → non-zerowidth. - Loosen the “fully visible” threshold to
intersectionRatio >= 0.99to avoid Safari sub-pixel rounding false positives, and add unit tests + a patch changeset.
Show a summary per file
| File | Description |
|---|---|
| packages/react/src/internal/hooks/tests/useOverflowObserver.test.tsx | Adds unit tests covering the zero-width root guard, the resize-driven re-observe path, and the 0.999 ratio case. |
| packages/react/src/internal/components/OverflowObserverProvider.tsx | Implements the Safari guard, resize-driven re-observation, and the intersectionRatio < 0.99 overflow heuristic. |
| .changeset/fix-safari-underlinenav-overflow-race.md | Patch changeset describing the Safari UnderlineNav overflow race fix. |
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 2
- Review effort level: Low
| if (width > 0 && lastWidth === 0) { | ||
| observedElementsRef.current.clear() | ||
| observeSubscribedElements() | ||
| } | ||
| lastWidth = width | ||
| }) |
| const observer = MockIntersectionObserver.instances.at(-1)! | ||
| observer.trigger([{target: item, isIntersecting: false, intersectionRatio: 0}]) | ||
| expect(item.getAttribute('data-overflowing')).toBe('false') | ||
|
|
||
| // Root gains size — ResizeObserver fires the 0→non-zero transition, which clears | ||
| // observedElementsRef and calls observeSubscribedElements() to re-attach the IO. | ||
| clientWidth = 400 | ||
| const ro = MockResizeObserver.instances.at(-1)! | ||
| ro.trigger(root) | ||
|
|
||
| // Now that items are re-observed, a fresh IO batch with correct intersections is delivered. | ||
| const freshObserver = MockIntersectionObserver.instances.at(-1)! | ||
| freshObserver.trigger([{target: item, isIntersecting: true, intersectionRatio: 1}]) | ||
| expect(item.getAttribute('data-overflowing')).toBe('false') |
…8184) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: mattcosta7 <8616962+mattcosta7@users.noreply.github.com>
|
Integration test results from github/github-ui PR:
|
Safari intermittently delivers the
IntersectionObserver's initial callback batch before the root element has a real layout box (clientWidth === 0). With a zero-size root, every observed<li>reportsisIntersecting: false, collapsing all tabs into the "More" menu. Because collapsing removes the overflow from the DOM, the observer never re-fires — the bad state is sticky until an unrelated resize occurs.Changes in
OverflowObserverProvider.tsxroot.clientWidth === 0, suppressing false-overflow reports from a not-yet-laid-out root.ResizeObserverwatches the root element exclusively. On the0 → non-zerowidth transition it clears observed state and re-callsobserveSubscribedElements(), delivering a fresh correct IO batch. No-ops on all other resizes. SSR-safe.getIsOverflowingnow usesintersectionRatio < 0.99instead of< 1, eliminating Safari's0.999…false positives for fully-visible elements.All three changes are scoped to
OverflowObserverProvider.tsx, so every consumer ofuseIsClipped(UnderlineNav, ActionBar) benefits.Changelog
New
Changed
OverflowObserverProvider: ignoreIntersectionObserverbatches delivered against a zero-width root; re-observe when root width transitions from0to non-zero; treatintersectionRatio ≥ 0.99as fully visibleRemoved
Rollout strategy
Testing & Reviewing
Three new unit tests cover the fix:
0 → non-zerotransition re-observes and produces correct stateintersectionRatio: 0.999is not treated as overflow