Files
headlessui/packages/@headlessui-react/src/utils/active-element-history.ts
T
Robin Malfait 30a6d51665 Fix focus not returned to SVG Element (#3704)
This PR fixes an issue where the focus is not returned to an `SVG`
element with a `tabIndex` correctly.

There are a few issues going on here:

1. We assume that the element to focus (`e.target`) is an instanceof
`HTMLElement`, but the `SVGElement` is not an instanceof `HTMLElement`.
2. By using `instanceof` we are checking against concrete classes, so if
this happen to cross certain contexts (Shadow DOM, Iframes, ...) then
the instances would be different.

To solve this, we will now:

1. Relax the types and only care about the actual attributes and methods
we are interested in. In most cases this means changing internal types
from `HTMLElement` to `Element` for example.
2. We will check whether certain properties are available in the object
to deduce the correct type from the object.

Fixes: #3660

## Test plan

Added an SVG to open a Dialog component and made sure that pressing
`escape` or clicking outside of the Dialog does restore the focus to the
SVG itself.
```tsx
<svg
  tabIndex={0}
  onKeyDown={(e) => {
    if (e.key === 'Enter' || e.key === ' ') {
      setIsOpen((v) => !v)
    }
  }}
  onClick={() => setIsOpen((v) => !v)}
  className="h-6 w-6 text-gray-500"
>
  <BookOpenIcon />
</svg>
```


Here is a video of that behavior:


https://github.com/user-attachments/assets/1805ca67-8bc7-4315-98a7-2490cba9230c
2025-04-25 14:52:32 +02:00

41 lines
1.4 KiB
TypeScript

import { onDocumentReady } from './document-ready'
import * as DOM from './dom'
import { focusableSelector } from './focus-management'
export let history: (HTMLOrSVGElement & Element)[] = []
onDocumentReady(() => {
function handle(e: Event) {
if (!DOM.isHTMLorSVGElement(e.target)) return
if (e.target === document.body) return
if (history[0] === e.target) return
let focusableElement = e.target
// Figure out the closest focusable element, this is needed in a situation
// where you click on a non-focusable element inside a focusable element.
//
// E.g.:
//
// ```html
// <button>
// <span>Click me</span>
// </button>
// ```
focusableElement = focusableElement.closest(focusableSelector) as HTMLOrSVGElement & Element
history.unshift(focusableElement ?? e.target)
// Filter out DOM Nodes that don't exist anymore
history = history.filter((x) => x != null && x.isConnected)
history.splice(10) // Only keep the 10 most recent items
}
window.addEventListener('click', handle, { capture: true })
window.addEventListener('mousedown', handle, { capture: true })
window.addEventListener('focus', handle, { capture: true })
document.body.addEventListener('click', handle, { capture: true })
document.body.addEventListener('mousedown', handle, { capture: true })
document.body.addEventListener('focus', handle, { capture: true })
})