Skip to content

Comments

[#1729] Fix no trap method with trapStack global#1731

Merged
stefcameron merged 2 commits intomasterfrom
fix-trap-stack-no-new-method
Jan 10, 2026
Merged

[#1729] Fix no trap method with trapStack global#1731
stefcameron merged 2 commits intomasterfrom
fix-trap-stack-no-new-method

Conversation

@stefcameron
Copy link
Member

@stefcameron stefcameron commented Jan 10, 2026

Fixes #1729

With #1691, the trap object has a new internal method, _setSubtreeIsolation(), but when using the trapStack option to allow multiple versions of Focus-trap libraries to run in the same DOM and share the same stack, older versions will not have this method and whenever the newer Focus-trap instance tries to manage the stack, a preexistingTrap._setSubtreeIsolation is not a function error will result because it expects the new method to exist on all traps in the stack.

Fix this by using optional chaining when calling the new method. That will cause it to be skipped on older traps that don't have it.

Of course, this implies that the newer isolateSubtrees option cannot be used on older traps, but that should be a given anyway.

If you're using multiple Focus-trap libraries in the same DOM, make sure they all support that new feature if you want to use it. Otherwise, results will not be as expected (and not supported).

PR Checklist

Please leave this checklist in your PR.

  • Issue being fixed is referenced.
  • Source changes maintain stated browser compatibility.
  • Web APIs introduced have deep browser coverage, including Safari (often very late to adopt new APIs).
  • Includes updated docs demo bundle if source/docs code was changed (run npm run demo-bundle in your branch and include the /docs/demo-bundle.js file that gets generated in your PR).
  • Unit test coverage added/updated.
  • E2E (i.e. demos) test coverage added/updated.
    • ⚠️ Non-covered demos (look for // TEST MANUALLY comments here) that can't be fully tested in Cypress have been manually verified.
  • Typings added/updated.
  • Changes do not break SSR:
    • Careful to test typeof document/window !== 'undefined' before using it in code that gets executed on load.
  • README updated (API changes, instructions, etc.).
  • Changes to dependencies explained.
  • Changeset added (run npm run changeset locally to add one, and follow the prompts).
    • EXCEPTION: A Changeset is not required if the change does not affect any of the source files that produce the package bundle. For example, demo changes, tooling changes, test updates, or a new dev-only dependency to run tests more efficiently should not have a Changeset since it will not affect package consumers.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed a crash when using focus-trap with trapStack in certain DOM configurations with older versions.
    • Improved inert element detection and focusability checks to better respect inert containers and avoid focusing elements inside them.

✏️ Tip: You can customize this high-level summary in your review settings.

Fixes #1729

With #1691, the trap object has a new internal method, `_setSubtreeIsolation()`,
but when using the `trapStack` option to allow multiple versions of Focus-trap
libraries to run in the same DOM and share the same stack, older versions will
not have this method and whenever the newer Focus-trap instance tries to manage
the stack, a `preexistingTrap._setSubtreeIsolation is not a function` error
will result because it expects the new method to exist on all traps in the stack.

Fix this by using optional chaining when calling the new method. That will cause
it to be skipped on older traps that don't have it.

Of course, this implies that the newer `isolateSubtrees` option cannot be used
on older traps, but that should be a given anyway.

If you're using multiple Focus-trap libraries in the same DOM, make sure they
all support that new feature if you want to use it. Otherwise, results will
not be as expected (and not supported).
@changeset-bot
Copy link

changeset-bot bot commented Jan 10, 2026

🦋 Changeset detected

Latest commit: f4ec2f0

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

This PR includes changesets to release 1 package
Name Type
focus-trap 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

@coderabbitai
Copy link

coderabbitai bot commented Jan 10, 2026

Walkthrough

Guards calls to a possibly-missing _setSubtreeIsolation on preexisting traps (preventing crashes when mixing focus-trap versions on a shared trapStack), updates demo bundle inert-aware selectors and tabbable version, and adds a changeset entry documenting the patch.

Changes

Cohort / File(s) Summary
Changeset Documentation
​.changeset/six-deer-wink.md
Adds a patch release entry noting the fix for crashes when older focus-trap instances lack _setSubtreeIsolation.
Core Library
index.js
Replaces direct calls to preexistingTrap._setSubtreeIsolation(...) with guarded invocations (?.(...)) to avoid calling undefined methods during activation/deactivation and error paths.
Demo Bundle / Focusability & Inert Logic
docs/demo-bundle.js
Bumps tabbable header (6.3.0 → 6.4.0); expands candidate selectors to include :not([inert]) and :not([inert] *) variants; changes isInert param type to Node; prefers node.closest('[inert]') with manual fallback; applies guarded invocation pattern for _setSubtreeIsolation across activation/deactivation flows.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

(Short, sharp, and slightly less dramatic than the bug it fixes.)

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly references the issue (#1729) and concisely describes the fix for trapStack compatibility with older focus-trap versions.
Description check ✅ Passed Description comprehensively addresses the issue, explains the root cause, implementation approach, and includes the full PR checklist with appropriate guidance.
Linked Issues check ✅ Passed Code changes directly address #1729 by using optional chaining to prevent runtime errors when calling _setSubtreeIsolation on older trap instances.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing trapStack compatibility: optional chaining in index.js, demo bundle updates, and changeset documentation—no extraneous modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
index.js (1)

1051-1053: Verify: Optional chaining on trap itself seems overly defensive.

Since trap is the current instance being created in this same file, and _setSubtreeIsolation is always defined via Object.defineProperties at line 1231, the optional chaining here shouldn't be necessary for the current trap.

However, I can see this might be for consistency or to prevent issues if someone creates a partial mock/stub in tests. If intentional, a brief comment explaining the reasoning would help future readers understand this isn't just copy-paste.

Not a blocker - just something to consider for clarity.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between c214581 and 4dac7dc.

⛔ Files ignored due to path filters (1)
  • docs/demo-bundle.js.map is excluded by !**/*.map
📒 Files selected for processing (3)
  • .changeset/six-deer-wink.md
  • docs/demo-bundle.js
  • index.js
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: e2e (firefox)
  • GitHub Check: e2e (chrome)
🔇 Additional comments (17)
.changeset/six-deer-wink.md (1)

1-5: LGTM!

Changeset is properly formatted with the correct patch bump level. The description clearly explains the fix and links to the issue. 👍

index.js (7)

896-896: LGTM!

Optional chaining correctly guards against calling _setSubtreeIsolation on traps that may not have this method. This location handles the collectAdjacentElements function where subtree isolation is toggled off before re-collecting elements.


1029-1031: LGTM!

This is the key fix location mentioned in the issue. When activating a new trap, we need to disable isolation on the preexisting trap from the stack. If that trap is an older version without _setSubtreeIsolation, the optional chaining gracefully skips the call. No more is not a function explosions! 💥➡️😌


1066-1075: LGTM!

Error recovery path is properly guarded. If activation throws, we attempt to restore the preexisting trap's isolation state - but only if it supports the method. Good catch on this edge case! 🎣


1099-1101: LGTM!

Deactivation path correctly guarded. Consistency is maintained across all call sites.


1179-1181: LGTM!

Container update path properly protected.


1210-1212: LGTM!

Pause state transition correctly handles the optional method call.


1219-1221: LGTM!

Unpause transition properly guarded. All the pause/unpause symmetry is preserved with the same defensive pattern.

docs/demo-bundle.js (9)

380-386: LGTM on tabbable library update.

Version bump from 6.3.0 to 6.4.0 and the expanded candidateSelector patterns with :not([inert] *) variants look correct. These ensure elements inside inert containers are properly excluded.


1720-1723: LGTM!

Transpiled optional chaining in collectAdjacentElements matches the source fix.


1854-1857: LGTM!

The preexisting trap guard during activation is correctly transpiled. This is the transpiled equivalent of the key fix at line 1030 in index.js.


1873-1876: LGTM!

Activation finish path correctly transpiled.


1887-1890: LGTM!

Error recovery path transpilation is correct.


1911-1914: LGTM!

Deactivation path properly transpiled.


1968-1971: LGTM!

Container update path transpilation looks good.


1989-2004: LGTM!

All pause/unpause state transition guards are correctly transpiled. The pattern is consistent throughout.


1-4: This is a generated file—changes look good.

Confirmed: docs/demo-bundle.js is generated by npm run compile:demo (which runs Rollup with Babel transpilation), and it's properly gitignored. The changes correctly reflect the Babel-transpiled version of the optional chaining fix from index.js. No manual edits needed here—this will be regenerated during the build process.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
index.js (1)

1076-1079: Pattern is solid and mirrors the activation path correctly.

The optional chaining properly handles cleanup when activation fails, ensuring older traps can be reverted without errors even if they lack _setSubtreeIsolation. The defensive coding here matches line 1033 (activation) and follows the same #1729 safeguard.

No explicit test coverage for trapStack mixed-version scenarios was found in the codebase, so consider adding a test for this edge case if not already implicitly covered by existing test suites.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge base: Disabled due to data retention organization setting

📥 Commits

Reviewing files that changed from the base of the PR and between 4dac7dc and f4ec2f0.

⛔ Files ignored due to path filters (1)
  • docs/demo-bundle.js.map is excluded by !**/*.map
📒 Files selected for processing (2)
  • docs/demo-bundle.js
  • index.js
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: e2e (chrome)
  • GitHub Check: e2e (firefox)
🔇 Additional comments (4)
index.js (1)

1030-1033: LGTM! Clean fix for version compatibility.

The optional chaining elegantly handles the case where older focus-trap versions on a shared trapStack don't have the _setSubtreeIsolation method. The inline comment clearly explains the rationale.

docs/demo-bundle.js (3)

380-385: LGTM! Tabbable 6.4.0 brings improved inert handling.

The updated selectors with :not([inert]):not([inert] *) properly exclude elements inside inert containers, and the refactored isInert logic now prefers the more efficient node.closest('[inert]') approach. Nice upgrade! 🎯

Also applies to: 398-421, 954-954


1853-1857: Transpiled optional chaining looks correct.

This is the bundled/transpiled version of the optional chaining from index.js, properly converted to browser-compatible code. The null/undefined checks before calling the method are functionally equivalent to the ?.() operator in the source.


1888-1892: Error recovery path properly transpiled.

The transpiled optional chaining in the error recovery block correctly mirrors the source code, ensuring cleanup works even with mixed focus-trap versions.

@stefcameron stefcameron merged commit bb36e15 into master Jan 10, 2026
3 checks passed
@stefcameron stefcameron deleted the fix-trap-stack-no-new-method branch January 10, 2026 21:59
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.

Isolate trap breaks compatibility with previous minor version on global trap stack

1 participant