30a6d51665
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
41 lines
1.4 KiB
TypeScript
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 })
|
|
})
|