Skip to content

fix(react-aria-components): handle multiple IntersectionObserver entries in Sheet - #10949

Open
bhumikabiyani wants to merge 2 commits into
adobe:mainfrom
bhumikabiyani:fix/sheet-intersection-observer-10897
Open

bhumikabiyani wants to merge 2 commits into
adobe:mainfrom
bhumikabiyani:fix/sheet-intersection-observer-10897

Conversation

@bhumikabiyani

@bhumikabiyani bhumikabiyani commented Nov 10, 2026 •

Copy link
Copy Markdown

Closes #10897

Summary

Problem

SheetOverlay previously processed only the first entry (entries[0]) in an IntersectionObserver callback. When multiple entries were delivered for the observed Sheet content, this could cause the latest zero-intersection state to be missed.

Consequently, the react-aria-sheet-close event might not be dispatched, leaving the Sheet stuck in its data-exiting state and preventing page interactions.

Solution

  1. Target filtering: Ignore entries that do not belong to the observed [data-sheet-content] element.
  2. Cumulative entrance tracking: Track whether the Sheet has entered the viewport across all relevant entries in the callback.
  3. Chronological selection: Select the entry with the greatest IntersectionObserverEntry.time, rather than relying on array order.
  4. Lifecycle completion: Update expanded and visibility states using the latest entry and dispatch the close event when the Sheet has entered and is no longer intersecting.

Test Instructions

Automated Tests

yarn jest packages/react-aria-components/test/Sheet.test.js
yarn jest packages/react-aria-components/test/Dialog.test.js

Results:

  • packages/react-aria-components/test/Sheet.test.js: 70 passed, 70 total
  • packages/react-aria-components/test/Dialog.test.js: 24 passed, 24 total

Regression Coverage

Added deterministic tests covering:

  • Sequential single-entry callbacks.
  • Multiple entries in chronological order.
  • Non-chronological entry arrays.
  • Reverse array ordering.
  • Preventing premature closure when the latest entry is visible.
  • Ignoring unrelated intersection targets, including callbacks containing only unrelated targets.

Linting and Formatting

yarn format:check packages/react-aria-components/src/Sheet.tsx packages/react-aria-components/test/Sheet.test.js
yarn run oxlint packages/react-aria-components/src/Sheet.tsx packages/react-aria-components/test/Sheet.test.js
git diff --check

Results:

  • oxfmt: All matched files use the correct format.
  • oxlint: Found 0 warnings and 0 errors.
  • git diff --check: Clean (exit code 0).

Release Impact

  • Fixes a bug or improves existing code without changing how apps use it.
  • Only changes docs, tests, CI, or development tools.
  • Adds a feature or API.
  • May break existing usage.

✅ PR Checklist

  • I followed the contributing guide.
  • I tested this change locally and included the results in Test Instructions, or explained why testing does not apply.
  • I understand and can explain every change in this PR. If I used AI assistance, I followed the AI contribution guidance.

🧢 Your Project

React Aria Components

expect(tree.queryByRole('dialog')).toBeNull();
});

it('selects the latest zero-intersection entry in reverse array order to complete exit', async () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

how is this test different than the one above it?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch! The original test was overlapping with the previous non-chronological ordering test. I've updated it to remove the initial intersection callback, so the Sheet starts in an unentered state. The test now specifically verifies that a positive intersection entry marks the Sheet as entered, while the latest zero-intersection entry in the same callback completes the exit. This covers cumulative entrance tracking in addition to timestamp ordering.

expect(tree.queryByRole('dialog')).toBeNull();
});

it('ignores callbacks where all entries belong to unrelated targets', async () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

how is this different than the test before it?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks for pointing this out! These tests cover two different cases: the preceding test verifies filtering when valid and unrelated targets are present in the same callback, while this test verifies the case where all entries belong to unrelated targets. I've clarified the test name and added a comment to highlight that it specifically exercises the empty-matches early-return guard.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sheet stays exiting when the IntersectionObserver batches the ratio 0 entry behind another

2 participants