Skip to content

Commit 8afe84d

Browse files
fix(vue): handle outsideClick touchend event on SVG elements (#3777)
Closes: #3752 Reuses code from: #3704 Right now `touchend` only checks if target is HTMLElement, but this check will fail for some other elements (like SVG elements). This change brings dom utils from #3704 to Vue to properly resolve target. Before: https://github.com/user-attachments/assets/668b79df-b4d7-4287-acac-1c4a8777d6dd After: https://github.com/user-attachments/assets/92e328a8-0994-40df-b4ca-0ca610b81d72 --------- Co-authored-by: Robin Malfait <[email protected]>
1 parent 87b3ffc commit 8afe84d

File tree

4 files changed

+46
-7
lines changed

4 files changed

+46
-7
lines changed

packages/@headlessui-vue/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
- Cancel outside click behavior on touch devices when scrolling ([#3266](https://github.com/tailwindlabs/headlessui/pull/3266))
1818
- Fix restoring focus to correct element when closing `Dialog` component ([#3365](https://github.com/tailwindlabs/headlessui/pull/3365))
1919
- Cleanup `process` in Combobox component when using virtualization ([#3495](https://github.com/tailwindlabs/headlessui/pull/3495))
20+
- Ensure outside click properly works when clicking SVG elements ([#3777](https://github.com/tailwindlabs/headlessui/pull/3777))
2021

2122
## [1.7.22] - 2024-05-08
2223

packages/@headlessui-vue/src/hooks/use-outside-click.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { computed, ref, type ComputedRef, type Ref } from 'vue'
2-
import { dom } from '../utils/dom'
2+
import { dom, isHTMLIframeElement, isHTMLorSVGElement } from '../utils/dom'
33
import { FocusableMode, isFocusableElement } from '../utils/focus-management'
44
import { isMobile } from '../utils/platform'
55
import { useDocumentEvent } from './use-document-event'
@@ -19,12 +19,15 @@ const MOVE_THRESHOLD_PX = 30
1919

2020
export function useOutsideClick(
2121
containers: ContainerInput | (() => ContainerInput),
22-
cb: (event: MouseEvent | PointerEvent | FocusEvent | TouchEvent, target: HTMLElement) => void,
22+
cb: (
23+
event: MouseEvent | PointerEvent | FocusEvent | TouchEvent,
24+
target: HTMLOrSVGElement & Element
25+
) => void,
2326
enabled: ComputedRef<boolean> = computed(() => true)
2427
) {
2528
function handleOutsideClick<E extends MouseEvent | PointerEvent | FocusEvent | TouchEvent>(
2629
event: E,
27-
resolveTarget: (event: E) => HTMLElement | null
30+
resolveTarget: (event: E) => (HTMLOrSVGElement & Element) | null
2831
) {
2932
// Check whether the event got prevented already. This can happen if you use the
3033
// useOutsideClick hook in both a Dialog and a Menu and the inner Menu "cancels" the default
@@ -162,7 +165,7 @@ export function useOutsideClick(
162165
}
163166

164167
return handleOutsideClick(event, () => {
165-
if (event.target instanceof HTMLElement) {
168+
if (isHTMLorSVGElement(event.target)) {
166169
return event.target
167170
}
168171
return null
@@ -188,7 +191,7 @@ export function useOutsideClick(
188191
'blur',
189192
(event) => {
190193
return handleOutsideClick(event, () => {
191-
return window.document.activeElement instanceof HTMLIFrameElement
194+
return isHTMLIframeElement(window.document.activeElement)
192195
? window.document.activeElement
193196
: null
194197
})

packages/@headlessui-vue/src/utils/dom.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,38 @@ export function dom<T extends HTMLElement | ComponentPublicInstance>(
2121

2222
return null
2323
}
24+
25+
// Source: https://github.com/tailwindlabs/headlessui/blob/2de2779a1e3a5a02c1684e611daf5a68e8002143/packages/%40headlessui-react/src/utils/dom.ts
26+
// Normally you can use `element instanceof HTMLElement`, but if you are in
27+
// different JS Context (e.g.: inside an iframe) then the `HTMLElement` will be
28+
// a different class and the check will fail.
29+
//
30+
// Instead, we will check for certain properties to determine if the element
31+
// is of a specific type.
32+
33+
export function isNode(element: unknown): element is Node {
34+
if (typeof element !== 'object') return false
35+
if (element === null) return false
36+
return 'nodeType' in element
37+
}
38+
39+
export function isElement(element: unknown): element is Element {
40+
return isNode(element) && 'tagName' in element
41+
}
42+
43+
export function isHTMLElement(element: unknown): element is HTMLElement {
44+
return isElement(element) && 'accessKey' in element
45+
}
46+
47+
// HTMLOrSVGElement doesn't inherit from HTMLElement or from Element. But this
48+
// is the type that contains the `tabIndex` property.
49+
//
50+
// Once we know that this is an `HTMLOrSVGElement` we also know that it is an
51+
// `Element` (that contains more information)
52+
export function isHTMLorSVGElement(element: unknown): element is HTMLOrSVGElement & Element {
53+
return isElement(element) && 'tabIndex' in element
54+
}
55+
56+
export function isHTMLIframeElement(element: unknown): element is HTMLIFrameElement {
57+
return isHTMLElement(element) && element.nodeName === 'IFRAME'
58+
}

packages/@headlessui-vue/src/utils/focus-management.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export enum FocusableMode {
7575
}
7676

7777
export function isFocusableElement(
78-
element: HTMLElement,
78+
element: HTMLOrSVGElement & Element,
7979
mode: FocusableMode = FocusableMode.Strict
8080
) {
8181
if (element === getOwnerDocument(element)?.body) return false
@@ -85,7 +85,7 @@ export function isFocusableElement(
8585
return element.matches(focusableSelector)
8686
},
8787
[FocusableMode.Loose]() {
88-
let next: HTMLElement | null = element
88+
let next: Element | null = element
8989

9090
while (next !== null) {
9191
if (next.matches(focusableSelector)) return true

0 commit comments

Comments
 (0)