Skip to content

Conversation

fpapado
Copy link

@fpapado fpapado commented Oct 1, 2025

This PR closes #1800, by introducing a 'full-native' displayCheck option. The option is done as a progressive enhancement on top of 'full', meaning that if Element#checkVisibility is supported, then it is used and its value returned verbatim. Otherwise, we follow the existing codepaths with manual checks, that resemble the browser behavior.

I parameterised the tests tests to include this option, and they pass the same as before, which gives me high confidence (very extensive test suite btw 😌).

Open questions

I have some open questions, that would be good to cover.

Tests for new cases

The nice thing about Element#checkVisibility is that it supports opacity checks, among other things. I could come up with some small fixtures to show case it. To add to that, maybe you have any ideas from past open issues? I imagine various visibility checks have come up in the past, and the 'full' displayCheck might have fallen short.

Should this be the default in a future major?

For a minor version, I think it makes sense to do this additively, as a new option. However, for wider adoption, it might be good to make this the default in a future major. While folks who follow changelogs and want to get the closest native behaviour might discover this option through the docs, my hunch is that the default will drive the largest change.

Benchmarking?

I saw this line in the README:

To reliably check if an element is tabbable/focusable, Tabbable defaults to the most reliable option to keep consistent with browser behavior, however this comes at a cost since every node needs to be validated as displayed using Web APIs that cause layout reflow.

Which is definitely correct; any advanced check for visibility will need to cause some reflow. Did you have any benchmarks for this, by any chance? We could see how the native version stacks against that (e.g. it might be causing style invalidations, but it's unclear to me if it causes layout reflow, per https://drafts.csswg.org/cssom-view-1/#dom-element-checkvisibility:~:text=Note%3A%20The%20checkVisibility()%20method%20provides%20a%20set%20of%20simple%20checks).

I imagine the native version to be similarly fast or faster, and at least more accurate. But good to check if we have the time 😅

CI / local E2E parity

At the moment, Cypress on CI runs fairly old browsers, and especially a very old version of node (18, when current LTS is 22). Locally, Cypress runs more contemporary browser versions (whatever is installed locally, and browsers make it quite hard to install specific versions). Node LTS is also used in other CI workflows, so there is some mismatch and relevant npm warnings about it. While all the tests pass in this PR, it took a bit of digging with browser versions, which I think could be streamlined (especially as a way to codify browser support!)

My suggestion would be to create a Dockerfile which includes a "approved" set of browsers to run local E2E tests in. Cypress has somewhat spotty support for Docker and ARM64, so to support all contributors we'd need some more work. I can pick this in a follow-up 🎁

PR Checklist

Please leave this checklist in your PR.

  • Source changes maintain stated browser compatibility.
  • Web APIs introduced have deep browser coverage, including Safari (often very late to adopt new APIs).
  • Issue being fixed is referenced.
  • [] Unit test coverage added/updated.
  • E2E test coverage added/updated.
  • - 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.
  • EADME 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

  • New Features

    • Added a displayCheck option "full-native" that leverages native visibility checks with graceful fallback.
  • Documentation

    • README updated to document full-native behavior, ordering, defaults, caveats, and fallback notes.
  • Tests

    • Expanded unit and E2E coverage to include full-native across tabbable/focusable and inert scenarios; added visibility-focused fixtures and a browser-version helper; test setup now initializes via document.
  • Chores

    • Added a changeset for a minor release and clarified CI notes on required browser versions.

Copy link

changeset-bot bot commented Oct 1, 2025

🦋 Changeset detected

Latest commit: 22d0f74

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

This PR includes changesets to release 1 package
Name Type
tabbable Minor

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

@fpapado fpapado marked this pull request as ready for review October 1, 2025 14:05
Copy link

coderabbitai bot commented Oct 1, 2025

Walkthrough

Adds a new displayCheck value "full-native" that uses Element.checkVisibility when available (falls back to manual checks otherwise); updates core logic, types, README, CI comment, a changeset, renames an E2E helper to setupTestDocument, adds a Firefox helper, expands unit/E2E tests and fixtures.

Changes

Cohort / File(s) Summary
Core visibility logic
src/index.js
Add branch for displayCheck === 'full-native': call Element.checkVisibility with specific options when available and return negated visibility; fall back to existing manual visibility checks; include 'full-native' alongside other full-check branches.
Type declarations
index.d.ts
Extend CheckOptions.displayCheck union to include 'full-native'.
Documentation
README.md
Document new full-native option in displayCheck type and prose, describe semantics, fallback behavior and caveats; update examples and ordering.
CI workflow note
.github/workflows/ci.yml
Expand comment about required browser versions for E2E images (Chrome 102 for inert, Chrome 105 / Firefox 106 for checkVisibility support); image tag unchanged.
Release metadata
.changeset/late-tires-remain.md
Add changeset entry describing a minor bump and the new full-native feature (metadata only).
E2E helpers
test/e2e/e2e.helpers.js
Rename setupTestWindow(done)setupTestDocument(done) and switch to cy.document().then(done); add isFirefoxLowerThan125() helper.
E2E tests
test/e2e/focusable.cy.js, test/e2e/isFocusable.cy.js, test/e2e/isTabbable.cy.js, test/e2e/tabbable.cy.js, test/e2e/shadow-dom.cy.js
Replace setupTestWindow imports/usages with setupTestDocument; adapt beforeEach callbacks to receive doc; add 'full-native' to displayCheck test matrices; adjust expected IDs and inertness permutations; import/use isFirefoxLowerThan125 where needed.
Fixtures
test/fixtures/basic.html
Add elements for content-visibility and opacity:0 scenarios (IDs like contentVisibilityHidden-button, contentVisibilityHidden-parent, opacityZero-button, opacityZeroParent-button).
Unit tests
test/unit/unit.test.js
Update expected ID lists to include new fixture IDs; add tests for displayCheck: 'full-native' fallback behavior and a spy asserting getComputedStyle call counts when checkVisibility is absent.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Caller
  participant Lib as tabbable.isHidden()
  participant Browser as Element.checkVisibility

  Caller->>Lib: isHidden(el, {displayCheck: "full-native"})
  alt Browser supports checkVisibility
    Lib->>Browser: el.checkVisibility({disableOpacityChecks:false, contentVisibility:true, visibility:true, cssAlias:true})
    Browser-->>Lib: {visible: true/false}
    Lib-->>Caller: return !visible
  else Browser lacks checkVisibility
    Lib->>Lib: run existing manual visibility checks (getComputedStyle, rects, etc.)
    Lib-->>Caller: return manual-result
  end
  Note right of Lib #dfe7ff: 'full-native' is then treated alongside 'full'/'legacy-full' in subsequent logic
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

(Short, factual — and yes, browsers keep inventing new ways to hide buttons.)

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Out of Scope Changes Check ❓ Inconclusive The vast majority of changes are directly scoped to implementing and testing the 'full-native' displayCheck feature. However, the renaming of setupTestWindow to setupTestDocument across multiple test files (test/e2e/e2e.helpers.js, focusable.cy.js, isFocusable.cy.js, shadow-dom.cy.js, tabbable.cy.js) appears to be a supporting refactor for test infrastructure rather than a core part of the feature implementation. While this change is likely beneficial for modernizing Cypress API usage and enabling the new Firefox version detection, it technically extends beyond the strict scope of "add 'full-native' displayCheck option" — it bundles test infrastructure improvements alongside the feature work. To clarify scope: determine whether the setupTestWindow → setupTestDocument refactor was required as a prerequisite for the new Firefox version detection and test assertions, or whether it could be separated into a follow-up PR. If it was essential to supporting the feature testing (which appears likely given isFirefoxLowerThan125 integration), then it's in scope; if it's just opportunistic cleanup, it might warrant a separate PR to keep changes focused.
✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The pull request title "feat: add 'full-native' displayCheck option, utilising Element#checkVisibility" accurately summarizes the primary change in the changeset. The title is concise, specific, and clearly communicates that this PR introduces a new feature (the 'full-native' option) and the technology it leverages (Element#checkVisibility). A team member reviewing the git history would immediately understand this PR adds a new displayCheck variant using the native browser API.
Linked Issues Check ✅ Passed The PR successfully implements the core objectives from linked issue #1800: introduce a displayCheck option that leverages Element.checkVisibility() as a progressive enhancement. The implementation includes feature detection with graceful fallback (src/index.js), type signature updates (index.d.ts), comprehensive documentation (README.md), E2E test parameterization covering the new 'full-native' option, unit tests validating fallback behavior when checkVisibility is unsupported, and a changeset entry documenting the minor version bump. Browser compatibility is maintained through feature detection, and cross-browser test adjustments (isFirefoxLowerThan125) address version-specific behavioral differences noted during development.
Description Check ✅ Passed The PR description adequately addresses the core requirements from the template, explaining that it closes issue #1800 and implementing a progressive-enhancement 'full-native' displayCheck option. The checklist is present and mostly completed, with most relevant items checked (browser compatibility, API coverage, issue reference, E2E tests, typings, SSR safety, README updates, changeset). However, the author marked "[] Unit test coverage added/updated" as unchecked, though the raw_summary clearly shows unit tests were added to test/unit/unit.test.js. The description also includes helpful "Open questions" context about test coverage, future defaults, benchmarking, and CI/local parity, which adds value though it extends beyond the template structure.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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: 1

🧹 Nitpick comments (1)
test/e2e/isFocusable.cy.js (1)

510-557: Consider adding test cases specific to checkVisibility capabilities.

The test matrix expansion looks good and properly exercises the new 'full-native' option. However, as you noted in the PR description, checkVisibility can detect additional visibility cases like opacity and content-visibility that the manual checks might miss.

Consider adding a dedicated test suite that verifies these edge cases when checkVisibility is available. Something like:

// Tests specifically for full-native's checkVisibility behavior
describe('full-native with checkVisibility', () => {
  before(function() {
    // Skip if checkVisibility not supported
    if (!Element.prototype.checkVisibility) {
      this.skip();
    }
  });

  it('returns false for elements with opacity: 0', () => {
    const container = document.createElement('div');
    container.innerHTML = '<div data-testid="invisible" tabindex="0" style="opacity: 0"></div>';
    document.body.append(container);
    
    expect(isFocusable(getByTestId(container, 'invisible'), { displayCheck: 'full-native' })).to.eql(false);
  });

  // Additional cases for content-visibility, filter: opacity(0), etc.
});

This would demonstrate the value of the native implementation and catch any regressions. Thoughts?

📜 Review details

Configuration used: CodeRabbit 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 ce7ef3a and 0615dd9.

📒 Files selected for processing (11)
  • .changeset/late-tires-remain.md (1 hunks)
  • .github/workflows/ci.yml (1 hunks)
  • README.md (2 hunks)
  • index.d.ts (1 hunks)
  • src/index.js (1 hunks)
  • test/e2e/e2e.helpers.js (1 hunks)
  • test/e2e/focusable.cy.js (3 hunks)
  • test/e2e/isFocusable.cy.js (3 hunks)
  • test/e2e/isTabbable.cy.js (3 hunks)
  • test/e2e/shadow-dom.cy.js (2 hunks)
  • test/e2e/tabbable.cy.js (3 hunks)
🔇 Additional comments (19)
.changeset/late-tires-remain.md (1)

1-6: LGTM!

Clean changeset entry. Minor bump is appropriate for the new feature, and the description accurately captures the essence of the change.

.github/workflows/ci.yml (1)

54-57: Good documentation of browser version requirements.

Clear note for maintainers about the minimum browser versions needed for the test suite. The existing container image (chrome106-ff106) already meets these requirements.

index.d.ts (1)

4-9: LGTM!

Type definition cleanly extended to include the new 'full-native' option. Backward compatible since the property remains optional.

README.md (3)

159-159: LGTM!

Type definition updated to include the new option, with the default correctly documented as full.


169-172: Clear documentation of the new option.

Good description of full-native behavior, with appropriate caveats about cross-browser differences and the fallback strategy. The note about potentially becoming the default in a future major version sets expectations nicely.


173-173: Good clarification.

Adding "(default)" here prevents confusion since full-native is listed first in the option list above.

src/index.js (1)

402-415: checkVisibility options and compatibility are accurate
The option flags (opacityProperty/visibilityProperty and their historic aliases checkOpacity/checkVisibilityCSS) and corresponding browser support (aliases in Chrome 105+/Firefox 106+ vs. spec names arriving in Chrome 121+/Firefox 122+) match the MDN docs. No changes needed.

test/e2e/e2e.helpers.js (1)

3-5: LGTM! Helper rename makes sense.

The switch from setupTestWindow to setupTestDocument with cy.document() aligns with Cypress best practices and simplifies the test setup flow.

test/e2e/isTabbable.cy.js (2)

3-3: Setup helper usage updated correctly.

Import and usage of setupTestDocument align with the helper rename. Clean transition.

Also applies to: 17-19


496-541: Good coverage expansion for 'full-native'.

The test matrix now includes 'full-native' alongside existing displayCheck options, ensuring the new native path gets exercised across in-document and detached-container scenarios. The expectations treat 'full-native' the same as 'full' and undefined (default), which makes sense—checkVisibility should match the manual checks for these cases.

test/e2e/shadow-dom.cy.js (1)

8-8: Setup flow updated consistently.

The switch to setupTestDocument with separate window retrieval via cy.window() works well for these shadow DOM tests. The reordering makes sense given the new helper API.

Also applies to: 18-26

test/e2e/focusable.cy.js (3)

3-3: Setup helper migration complete.

Import and usage of setupTestDocument consistent with the broader test refactor.

Also applies to: 18-20


28-109: Solid coverage for 'full-native' in basic example tests.

The test matrix expansion ensures 'full-native' is tested across in-document and detached-container scenarios. The expectations correctly treat it the same as the default and 'full' options for these fixtures.


114-191: Inertness tests now cover 'full-native'.

Extending the displayCheck variants to include 'full-native' in inertness tests ensures the native visibility path handles inert elements correctly. Good thoroughness! 💪

test/e2e/tabbable.cy.js (3)

3-3: Helper migration looks good.

Consistent use of setupTestDocument following the helper rename pattern.

Also applies to: 18-20


28-107: 'full-native' integrated into basic example tests.

The displayCheck matrix expansion ensures the new native visibility option is tested thoroughly. Expectations are consistent with existing options, which is what we'd expect for these test cases.


110-187: Inertness coverage extended to 'full-native'.

The inertness test suite now exercises all displayCheck options including 'full-native', ensuring the native path correctly handles inert containers and nested scenarios. Nice and thorough! ✨

test/e2e/isFocusable.cy.js (2)

526-553: Test assertions correctly handle the new option.

The conditional logic properly accounts for the different displayCheck behaviors:

  • 'full-native', 'full', and default treat elements with display: none or visibility: hidden as unfocusable when in the document
  • 'legacy-full' has the quirky behavior of treating everything as focusable when not in the document

The approach of treating 'full-native' the same as 'full' in assertions makes sense—it's a progressive enhancement that falls back gracefully. Nice work keeping the test suite comprehensive without adding unnecessary complexity! 🎯


3-3: Helper rename is consistent across all E2E tests. setupTestWindow has officially retired; every E2E test now imports setupTestDocument.

@fpapado
Copy link
Author

fpapado commented Oct 2, 2025

@stefcameron, I opened this PR after discussing in #1800. The GitHub UI does not allow me to assign reviewers on this repository, so sending this ping just in case ✉️

@stefcameron
Copy link
Member

@fpapado Thank for the PR, saw it! I will review within the next couple of days.

@stefcameron
Copy link
Member

@fpapado Had a look! Looks pretty solid. But the one thing I'm not seeing, or missing seeing, is at least one test that verifies the new full-native option is behaving as expected in contrast to full, and also the fallback to full when checkVisibility() isn't defined.

I'd like to see one test that shows full-native finds something that full doesn't, or the opposite, which ever, and another that shows full-native drops to full when the API isn't available.

Is that possible?

@fpapado
Copy link
Author

fpapado commented Oct 7, 2025

@fpapado Had a look! Looks pretty solid. But the one thing I'm not seeing, or missing seeing, is at least one test that verifies the new full-native option is behaving as expected in contrast to full, and also the fallback to full when checkVisibility() isn't defined.

I'd like to see one test that shows full-native finds something that full doesn't, or the opposite, which ever, and another that shows full-native drops to full when the API isn't available.

Is that possible?

Yeah, these are both doable. I added a test for something that full-native supports (content-visibility: hidden parent) now in 5bf6ca7.

Testing the fallback in end-to-end proved slightly cumbersome, because both Chrome and Firefox in the existing CI docker image support checkVisibility. One of Cypress' Node 16 images do have matching browser versions, but I wanted to avoid going further into unsupported Node versions on CI. My personal hunch would be to create docker images with the specific versions of browsers that we'd want to test, but that's a bit of a rabbit hole.

Anyway, to work around that, I tested the fallback in unit tests, since JSDOM in this case conveniently does not support checkVisibility. The relevant change is at dd9f4fe, with a check to ensure that the assumptions about the environment are correct.

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 (2)
README.md (1)

172-174: Consider clarifying the fallback behavior.

The documentation mentions the fallback occurs "when Element#checkVisibility is not supported" but could be more explicit about what "fallback to full behavior" means in practice. Consider adding: "this strategy falls back to the manual visibility checks used by the 'full' option."

Current wording is clear enough for most users, but the extra clarity might help developers understand what happens under the hood.

- If Element#checkVisibility is not supported, this strategy falls back to the `full` behavior.
+ If Element#checkVisibility is not supported, this strategy falls back to the manual visibility checks used by the `full` option.
test/unit/unit.test.js (1)

152-178: Check API support on the prototype, not the instance

Change the feature‐detection guard to verify the existence of the API on the prototype:

-        if (typeof container.checkVisibility !== 'undefined') {
+        if (typeof Element.prototype.checkVisibility !== 'undefined') {
           // a tacit assumption in this test is that checkVisibility() is unsupported in JSDOM
           throw new Error(
             'checkVisibility seems to be supported in this version of JSDOM, making the test invalid. Consider changing to an end-to-end test with sufficiently old browsers.'
           );
         }
📜 Review details

Configuration used: CodeRabbit 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 d068a71 and dd9f4fe.

📒 Files selected for processing (7)
  • README.md (2 hunks)
  • index.d.ts (1 hunks)
  • src/index.js (2 hunks)
  • test/e2e/focusable.cy.js (3 hunks)
  • test/e2e/tabbable.cy.js (3 hunks)
  • test/fixtures/basic.html (1 hunks)
  • test/unit/unit.test.js (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • index.d.ts
🧰 Additional context used
🪛 markdownlint-cli2 (0.18.1)
README.md

170-170: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)


171-171: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)

🔇 Additional comments (9)
README.md (1)

159-159: LGTM! Type signature correctly updated.

The displayCheck type now includes 'full-native' alongside the existing options.

src/index.js (2)

401-419: LGTM! Clean implementation with appropriate fallback.

The early branch for 'full-native' correctly:

  • Checks for checkVisibility availability
  • Uses comprehensive options (contentVisibilityAuto, visibilityProperty, checkVisibilityCSS alias)
  • Returns the negated result to match isHidden's boolean semantics
  • Falls through gracefully when checkVisibility is unavailable

The fallback is properly handled by including 'full-native' in the condition on line 441 (see next comment).


436-442: LGTM! Fallback condition correctly includes 'full-native'.

This addresses the critical bug identified in past reviews. When checkVisibility is unavailable, 'full-native' now correctly executes the same manual visibility checks as 'full', ensuring the documented fallback behavior works as intended.

test/fixtures/basic.html (1)

56-65: LGTM! Well-chosen test fixtures for the new option.

The new elements appropriately test visibility edge cases:

  • content-visibility: hidden elements (which checkVisibility should detect)
  • opacity: 0 elements (which the comment correctly notes remain focusable/tabbable)

These fixtures align with the conditional expectations in the e2e tests where contentVisibilityHiddenParent-button is filtered out only for 'full-native'.

test/unit/unit.test.js (1)

54-57: LGTM! Expected element IDs correctly updated.

The new visibility-related element IDs align with the fixtures added in test/fixtures/basic.html.

test/e2e/tabbable.cy.js (2)

28-117: LGTM! Comprehensive test coverage for the new option.

The parameterized tests now include 'full-native' alongside existing displayCheck options, with appropriate conditional expectations:

  • Line 53-55: Correctly filters out contentVisibilityHiddenParent-button for 'full-native' since Element.checkVisibility can detect when a parent has content-visibility: hidden
  • Line 52: Keeps contentVisibilityHidden-button in all cases, as the element itself is still in the tab order (only its content is hidden)

This addresses one of stefcameron's requests for "a test demonstrating a behavioral difference between 'full-native' and 'full'". 🎯


121-196: LGTM! Inertness tests appropriately expanded.

All displayCheck variants (including 'full-native') are now tested with inert containers, ensuring the new option doesn't break inertness detection.

test/e2e/focusable.cy.js (2)

28-119: LGTM! Mirrored test coverage for focusable elements.

The test parameterization and conditional expectations mirror the tabbable tests, ensuring consistent behavior across both APIs. The filtering of contentVisibilityHiddenParent-button (lines 55-57) when displayCheck is 'full-native' demonstrates the same behavioral difference between 'full-native' and 'full' that stefcameron requested.


124-200: LGTM! Complete inertness coverage.

The inertness tests cover all displayCheck variants including 'full-native', ensuring the new option properly handles inert containers at various nesting levels.

<button id="opacityZero-button" style="opacity: 0;">foo</button>
<div style="opacity: 0;">
<button id="opacityZeroParent-button">foo</button>
</div>
Copy link
Author

Choose a reason for hiding this comment

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

@stefcameron, in my original description, I had mentioned opacity: 0 as something that checkVisibility can catch. While it can indeed catch that, I double-checked whether native browser behaviour (both for imperative HTMLElement.focus() and tabbing through with a keyboard) allows focusing/tabbing to an element with opacity: 0. It turns out that it does! Here's a relevant codepen.

CleanShot.2025-10-07.at.21.46.57.mp4

So in the interest of tracking the native focus/tab behaviour, I removed the opacityProperty option from the lines here https://github.com/focus-trap/tabbable/pull/1818/files#diff-bfe9874d239014961b1ae4e89875a6155667db834a410aaaa2ebe3cf89820556R406-R407, and added this test case as documentation.

Ultimately, I can imagine some libraries wanting to guard against developer error and checking opacity (ours at work does that, for example), but for tabbable I think matching the native behaviour is less surprising. The current README advice of using tabIndex (or even inert) to exclude unwanted elements can be used to exclude such invisible elements on demand.

Copy link
Member

Choose a reason for hiding this comment

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

This makes sense, thank you for the explanation. I'm going to leave this comment unresolved so that it remains visible if I'm ever referring back to this PR wondering why we excluded that setting.

@fpapado fpapado requested a review from stefcameron October 7, 2025 18:53
@stefcameron
Copy link
Member

@fpapado Just one last small request, if that works with you. Also FF failed in CI: https://github.com/focus-trap/tabbable/actions/runs/18322680365/job/52302880471?pr=1818

@fpapado
Copy link
Author

fpapado commented Oct 11, 2025

@fpapado Just one last small request, if that works with you. Also FF failed in CI: https://github.com/focus-trap/tabbable/actions/runs/18322680365/job/52302880471?pr=1818

The FF failure sent me down a small rabbit hole, when it comes to what Firefox considers visible and focusable 🔍

After some bisecting (full instructions in this repository, I used Playwright since I find it much easier to get a specific browser version on demand, compared to juggling docker image builds), it turns out that:

  • Firefox 106, which the CI uses, supports checkVisibility and the checkVisibilityCSS option, but considers elements with a content-visibility: hidden parent to be visible, and also focusable/tabbable
  • Firefox 125 is the first version that considers such elements both invisible and not focusable/tabbable.

Since the logic is internally consistent, I don't think it is a problem, because tabbable follows exactly what the browser (seems to be) doing. So, only in the tests, I added a check for Firefox < 125 and made the assertions accurate in 353c86a. Let me know if this makes sense!

@stefcameron
Copy link
Member

@fpapado Just one last small request, if that works with you. Also FF failed in CI: https://github.com/focus-trap/tabbable/actions/runs/18322680365/job/52302880471?pr=1818

The FF failure sent me down a small rabbit hole, when it comes to what Firefox considers visible and focusable 🔍

After some bisecting (full instructions in this repository, I used Playwright since I find it much easier to get a specific browser version on demand, compared to juggling docker image builds), it turns out that:

Thank you for investigating, much appreciated!

Since the logic is internally consistent, I don't think it is a problem, because tabbable follows exactly what the browser (seems to be) doing. So, only in the tests, I added a check for Firefox < 125 and made the assertions accurate in 353c86a. Let me know if this makes sense!

Yes, I think that's OK.

@stefcameron
Copy link
Member

Thanks for trying to fix the test earlier. FF is still giving some grief... 😬

@fpapado
Copy link
Author

fpapado commented Oct 19, 2025

Thanks for trying to fix the test earlier. FF is still giving some grief... 😬

Hello! I took a small break due to work, but I think I figured it out now in 22d0f74. Turns out that Cypress.browser.name is lowercase, so the FF < 125 detection was not working as intended 😅 Let's see what CI thinks!

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

♻️ Duplicate comments (1)
test/e2e/e2e.helpers.js (1)

3-5: Already reviewed and looks good.

The switch from cy.window() to cy.document() was discussed in past comments and approved. No new concerns here.

🧹 Nitpick comments (1)
test/e2e/e2e.helpers.js (1)

44-59: Smart workaround for Firefox version quirks.

The helper correctly detects Firefox < 125 to handle the content-visibility: hidden behavior difference, and the comment does a great job explaining why this exists (with a bonus link for the curious). The logic is clean and the use case is clear from the PR context about Firefox CI failures.

One tiny optional suggestion: you could add a defensive check like Cypress?.browser?.name in case the API shape changes, but given this is Cypress-specific test code and working fine in CI, it's probably overkill. 🤷

If you're feeling extra defensive (totally optional):

 export function isFirefoxLowerThan125() {
   return (
+    Cypress.browser &&
     // NOTE: `browser.name` is lowercase
     // @see https://docs.cypress.io/api/cypress-api/browser#Syntax
     Cypress.browser.name === 'firefox' && Cypress.browser.majorVersion < 125
   );
 }
📜 Review details

Configuration used: CodeRabbit 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 9087866 and 22d0f74.

📒 Files selected for processing (1)
  • test/e2e/e2e.helpers.js (2 hunks)

@fpapado fpapado requested a review from stefcameron October 22, 2025 06:04
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.

Idea: using Element checkVisibility() as a new displayCheck option

2 participants