Improve "outside click" behaviour in combination with 3rd party libraries (#2572)
* listen for both `mousedown` and `pointerdown` events This is necessary for calculating the target where the focus will eventually move to. Some other libraries will use an `event.preventDefault()` and if we are not listening for all "down" events then we might not capture the necessary target. We already tried to ensure this was always captured by using the `capture` phase of the event but that's not enough. This change won't be enough on its own, but this will improve the experience with certain 3rd party libraries already. * refactor one-liners * listen for `touchend` event to improve "outside click" on mobile devices * update changelog
This commit is contained in:
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Fixed
|
||||
|
||||
- Ensure the caret is in a consistent position when syncing the `Combobox.Input` value ([#2568](https://github.com/tailwindlabs/headlessui/pull/2568))
|
||||
- Improve "outside click" behaviour in combination with 3rd party libraries ([#2572](https://github.com/tailwindlabs/headlessui/pull/2572))
|
||||
|
||||
## [1.7.15] - 2023-06-01
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ type ContainerInput = Container | ContainerCollection
|
||||
|
||||
export function useOutsideClick(
|
||||
containers: ContainerInput | (() => ContainerInput),
|
||||
cb: (event: MouseEvent | PointerEvent | FocusEvent, target: HTMLElement) => void,
|
||||
cb: (event: MouseEvent | PointerEvent | FocusEvent | TouchEvent, target: HTMLElement) => void,
|
||||
enabled: boolean = true
|
||||
) {
|
||||
// TODO: remove this once the React bug has been fixed: https://github.com/facebook/react/issues/24657
|
||||
@@ -27,7 +27,7 @@ export function useOutsideClick(
|
||||
[enabled]
|
||||
)
|
||||
|
||||
function handleOutsideClick<E extends MouseEvent | PointerEvent | FocusEvent>(
|
||||
function handleOutsideClick<E extends MouseEvent | PointerEvent | FocusEvent | TouchEvent>(
|
||||
event: E,
|
||||
resolveTarget: (event: E) => HTMLElement | null
|
||||
) {
|
||||
@@ -102,6 +102,16 @@ export function useOutsideClick(
|
||||
|
||||
let initialClickTarget = useRef<EventTarget | null>(null)
|
||||
|
||||
useDocumentEvent(
|
||||
'pointerdown',
|
||||
(event) => {
|
||||
if (enabledRef.current) {
|
||||
initialClickTarget.current = event.composedPath?.()?.[0] || event.target
|
||||
}
|
||||
},
|
||||
true
|
||||
)
|
||||
|
||||
useDocumentEvent(
|
||||
'mousedown',
|
||||
(event) => {
|
||||
@@ -133,6 +143,24 @@ export function useOutsideClick(
|
||||
true
|
||||
)
|
||||
|
||||
useDocumentEvent(
|
||||
'touchend',
|
||||
(event) => {
|
||||
return handleOutsideClick(event, () => {
|
||||
if (event.target instanceof HTMLElement) {
|
||||
return event.target
|
||||
}
|
||||
return null
|
||||
})
|
||||
},
|
||||
|
||||
// We will use the `capture` phase so that layers in between with `event.stopPropagation()`
|
||||
// don't "cancel" this outside click check. E.g.: A `Menu` inside a `DialogPanel` if the `Menu`
|
||||
// is open, and you click outside of it in the `DialogPanel` the `Menu` should close. However,
|
||||
// the `DialogPanel` has a `onClick(e) { e.stopPropagation() }` which would cancel this.
|
||||
true
|
||||
)
|
||||
|
||||
// When content inside an iframe is clicked `window` will receive a blur event
|
||||
// This can happen when an iframe _inside_ a window is clicked
|
||||
// Or, if headless UI is *in* the iframe, when a content in a window containing that iframe is clicked
|
||||
@@ -142,12 +170,13 @@ export function useOutsideClick(
|
||||
// and we can consider it an "outside click"
|
||||
useWindowEvent(
|
||||
'blur',
|
||||
(event) =>
|
||||
handleOutsideClick(event, () =>
|
||||
window.document.activeElement instanceof HTMLIFrameElement
|
||||
(event) => {
|
||||
return handleOutsideClick(event, () => {
|
||||
return window.document.activeElement instanceof HTMLIFrameElement
|
||||
? window.document.activeElement
|
||||
: null
|
||||
),
|
||||
})
|
||||
},
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Fixed
|
||||
|
||||
- Ensure the caret is in a consistent position when syncing the `Combobox.Input` value ([#2568](https://github.com/tailwindlabs/headlessui/pull/2568))
|
||||
- Improve "outside click" behaviour in combination with 3rd party libraries ([#2572](https://github.com/tailwindlabs/headlessui/pull/2572))
|
||||
|
||||
## [1.7.14] - 2023-06-01
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@ type ContainerInput = Container | ContainerCollection
|
||||
|
||||
export function useOutsideClick(
|
||||
containers: ContainerInput | (() => ContainerInput),
|
||||
cb: (event: MouseEvent | PointerEvent | FocusEvent, target: HTMLElement) => void,
|
||||
cb: (event: MouseEvent | PointerEvent | FocusEvent | TouchEvent, target: HTMLElement) => void,
|
||||
enabled: ComputedRef<boolean> = computed(() => true)
|
||||
) {
|
||||
function handleOutsideClick<E extends MouseEvent | PointerEvent | FocusEvent>(
|
||||
function handleOutsideClick<E extends MouseEvent | PointerEvent | FocusEvent | TouchEvent>(
|
||||
event: E,
|
||||
resolveTarget: (event: E) => HTMLElement | null
|
||||
) {
|
||||
@@ -85,6 +85,16 @@ export function useOutsideClick(
|
||||
|
||||
let initialClickTarget = ref<EventTarget | null>(null)
|
||||
|
||||
useDocumentEvent(
|
||||
'pointerdown',
|
||||
(event) => {
|
||||
if (enabled.value) {
|
||||
initialClickTarget.value = event.composedPath?.()?.[0] || event.target
|
||||
}
|
||||
},
|
||||
true
|
||||
)
|
||||
|
||||
useDocumentEvent(
|
||||
'mousedown',
|
||||
(event) => {
|
||||
@@ -116,6 +126,24 @@ export function useOutsideClick(
|
||||
true
|
||||
)
|
||||
|
||||
useDocumentEvent(
|
||||
'touchend',
|
||||
(event) => {
|
||||
return handleOutsideClick(event, () => {
|
||||
if (event.target instanceof HTMLElement) {
|
||||
return event.target
|
||||
}
|
||||
return null
|
||||
})
|
||||
},
|
||||
|
||||
// We will use the `capture` phase so that layers in between with `event.stopPropagation()`
|
||||
// don't "cancel" this outside click check. E.g.: A `Menu` inside a `DialogPanel` if the `Menu`
|
||||
// is open, and you click outside of it in the `DialogPanel` the `Menu` should close. However,
|
||||
// the `DialogPanel` has a `onClick(e) { e.stopPropagation() }` which would cancel this.
|
||||
true
|
||||
)
|
||||
|
||||
// When content inside an iframe is clicked `window` will receive a blur event
|
||||
// This can happen when an iframe _inside_ a window is clicked
|
||||
// Or, if headless UI is *in* the iframe, when a content in a window containing that iframe is clicked
|
||||
@@ -125,12 +153,13 @@ export function useOutsideClick(
|
||||
// and we can consider it an "outside click"
|
||||
useWindowEvent(
|
||||
'blur',
|
||||
(event) =>
|
||||
handleOutsideClick(event, () =>
|
||||
window.document.activeElement instanceof HTMLIFrameElement
|
||||
(event) => {
|
||||
return handleOutsideClick(event, () => {
|
||||
return window.document.activeElement instanceof HTMLIFrameElement
|
||||
? window.document.activeElement
|
||||
: null
|
||||
),
|
||||
})
|
||||
},
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user