Skip to content

fix(to-have-focus): resolve the active element through shadow roots - #739

Open
FrancoKaddour wants to merge 1 commit into
testing-library:mainfrom
FrancoKaddour:fix/to-have-focus-shadow-root
Open

FrancoKaddour wants to merge 1 commit into
testing-library:mainfrom
FrancoKaddour:fix/to-have-focus-shadow-root

Conversation

@FrancoKaddour

Copy link
Copy Markdown

Closes #722.

Problem

toHaveFocus checks element.ownerDocument.activeElement === element. When focus moves into a shadow root, document.activeElement resolves to the shadow host, not the element that actually has focus (which lives at shadowRoot.activeElement). As a result, toHaveFocus can never pass for any element inside a shadow root:

const host = document.createElement('div')
const shadowRoot = host.attachShadow({mode: 'open'})
const input = document.createElement('input')
shadowRoot.appendChild(input)
document.body.appendChild(host)

input.focus()
expect(input).toHaveFocus() // failed before this change

Fix

Add a small getActiveElement helper that walks down nested shadow roots to find the truly focused element, and use it both in the pass check and in the failure message (so the "Received element with focus" diagnostic points at the real element rather than the host). For regular elements with no shadow root it degrades to doc.activeElement, so existing behavior is unchanged.

Thanks to @julienw for the clear report and the suggested approach.

Tests

Added coverage for a focused element inside a shadow root and inside nested shadow roots. Both fail on main and pass with this change; the existing .toHaveFocus test is unaffected.

Closes testing-library#722.

toHaveFocus checked ownerDocument.activeElement === element, but when focus
is inside a shadow root document.activeElement resolves to the host, not the
truly focused element. Walk down nested shadow roots (via a getActiveElement
helper) in both the pass check and the failure message. Degrades to
doc.activeElement for regular elements, so existing behavior is unchanged.
@julienw

julienw commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Just thinking about that now: I've been using . matches(":focus") in my projects too. I wonder if that could be an easier and more elegant solution...

We could also add .toHaveFocusWithin using the css selector :focus-within.

@FrancoKaddour

Copy link
Copy Markdown
Author

Good thought — I tried :focus first too, but it doesn't hold up in jsdom, which is the environment most jest-dom users run in. Quick probe (input focused inside an open shadow root):

document.activeElement === input     → false   (the bug)
getActiveElement(document) === input → true    (this PR)
input.matches(':focus')              → false   ✗

jsdom doesn't propagate :focus across the shadow boundary, so matches(':focus') gives a false negative for exactly the case we're fixing. The explicit shadowRoot.activeElement walk sidesteps that by not relying on selector matching at all. (In a real browser :focus behaves better, but jest-dom has to work in jsdom.)

Re toHaveFocusWithin with :focus-within — I like it as a separate matcher, but I'd keep it out of this PR to keep the fix focused. Worth its own issue: :focus-within also throws in the node test project here (Failed to execute 'contains' on 'Node'), so it'd need its own jsdom-safe implementation (likely a similar walk checking containment). Happy to open that follow-up if there's interest.

@julienw

julienw commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Ah right, I'm not using that in jsdom indeed.

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.

toHaveFocus doesn't work with shadow roots

2 participants