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
* resolve focusable element when recording elements
Right now, we have to record when a click / mousedown / focus event happens on an element. But when you click on a non-focusable element inside of a focusable element then we record the inner element instead of the outer one.
This happens in this scenario:
```html
<button>
<span>click me</span>
</button>
```
This solves it by resolving the closest focusable element (and we fallback to the e.target as a last resort)
* update changelog
* Allow to open combobox on input focus
* Close focused combobox with openOnFocus prop when clicking the button
* ensure tabbing through a few fields, doesn't result in an incorrectly selected item
When you have a fwe inputs such as:
```html
<form>
<input />
<input />
<input />
<Combobox>
<Combobox.Input />
</Combobox>
<input />
<input />
<input />
</form>
```
Tabbing through this list will open the combobox once you are on the
input field. When you continue tabbing, the first item would be
selected. However, if the combobox is not marked as nullable, it means
that just going through the form means that we set a value we can't
unset anymore.
We still want to open the combobox, we just don't want to select
anything in this case.
* only `openOnFocus` if the `<Combobox.Input />` is focused from the
outside
If the focus is coming from the `<Combobox.Button />` or as a side
effect of selecting an `<Combobox.Option />` then we don't want to
re-open the `<Combobox />`
* update tests to ensure that the `Combobox.Input` is the active element
* order `handleBlur` and `handleFocus` the same way in Vue & React
* only select the active option when the Combobox wasn't opened by focusing the input field
* convert to `immediate` prop on the `Combobox` itself
* update changelog
* ensure we see the "relatedTarget" in Safari
Safari doesn't fire a `focus` event when clicking a button, therefore it
does not become the `document.activeElement`, and events like `blur` or
`focus` doesn't set the button as the `event.relatedTarget`.
Keeping track of a history like this solves that problem. We already had
the code for the `FocusTrap` component.
---------
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>