Skip to content

toHaveFocus doesn't work with shadow roots #722

@julienw

Description

@julienw
  • @testing-library/jest-dom version: 6.9.1
  • node version: 24.15.0
  • jest version: 30.3.0
  • npm version: 11.12.1

Relevant code or config:

  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(); // fails

What you did:

Focused an <input> element inside a shadow root, then asserted toHaveFocus() on it.

What happened:

  expect(element).toHaveFocus()

  Expected element with focus:
    <input />
  Received element with focus:
    <div />

The <div /> reported as focused is the shadow host, not the <input>.

Reproduction:

https://github.com/julienw/jest-dom-to-have-focus-shadow-root

Run npm install && npm test. Six tests pass and one fails — the failing test is the bug.

Problem description:

When focus moves into a shadow root, document.activeElement returns the shadow host rather than the element that actually has focus. toHaveFocus checks element.ownerDocument.activeElement === element, so it can never return true for any element inside a shadow root.

The actually-focused element is reachable via shadowRoot.activeElement, which the matcher does not consult.

Suggested solution:

Traverse shadow roots when resolving the active element:

function getDeepActiveElement(doc) {
  let active = doc.activeElement;
  while (active?.shadowRoot?.activeElement) {
    active = active.shadowRoot.activeElement;
  }
  return active;                                                                                                                                                                                                                                                                        
}

Replacing element.ownerDocument.activeElement with getDeepActiveElement(element.ownerDocument) in the toHaveFocus implementation would fix the issue for arbitrarily nested shadow roots.

If the solution works for you I can do a PR with that.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions