Skip to content

toHaveFocus doesn't work with shadow roots #722

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions