From 423822925ccfc2333bf6f798a1b211c5d2568539 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 3 Jul 2023 15:26:31 +0200 Subject: [PATCH 1/4] 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. --- .../@headlessui-react/src/hooks/use-outside-click.ts | 10 ++++++++++ .../@headlessui-vue/src/hooks/use-outside-click.ts | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/packages/@headlessui-react/src/hooks/use-outside-click.ts b/packages/@headlessui-react/src/hooks/use-outside-click.ts index 12965d146e..3617152c88 100644 --- a/packages/@headlessui-react/src/hooks/use-outside-click.ts +++ b/packages/@headlessui-react/src/hooks/use-outside-click.ts @@ -102,6 +102,16 @@ export function useOutsideClick( let initialClickTarget = useRef(null) + useDocumentEvent( + 'pointerdown', + (event) => { + if (enabledRef.current) { + initialClickTarget.current = event.composedPath?.()?.[0] || event.target + } + }, + true + ) + useDocumentEvent( 'mousedown', (event) => { diff --git a/packages/@headlessui-vue/src/hooks/use-outside-click.ts b/packages/@headlessui-vue/src/hooks/use-outside-click.ts index 084eedba51..69946f4281 100644 --- a/packages/@headlessui-vue/src/hooks/use-outside-click.ts +++ b/packages/@headlessui-vue/src/hooks/use-outside-click.ts @@ -85,6 +85,16 @@ export function useOutsideClick( let initialClickTarget = ref(null) + useDocumentEvent( + 'pointerdown', + (event) => { + if (enabled.value) { + initialClickTarget.value = event.composedPath?.()?.[0] || event.target + } + }, + true + ) + useDocumentEvent( 'mousedown', (event) => { From bbe103c6f48007e3038bcfdf5386a3d3c438548e Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 3 Jul 2023 15:32:12 +0200 Subject: [PATCH 2/4] refactor one-liners --- .../@headlessui-react/src/hooks/use-outside-click.ts | 9 +++++---- packages/@headlessui-vue/src/hooks/use-outside-click.ts | 9 +++++---- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/@headlessui-react/src/hooks/use-outside-click.ts b/packages/@headlessui-react/src/hooks/use-outside-click.ts index 3617152c88..f208d9c0ca 100644 --- a/packages/@headlessui-react/src/hooks/use-outside-click.ts +++ b/packages/@headlessui-react/src/hooks/use-outside-click.ts @@ -152,12 +152,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 ) } diff --git a/packages/@headlessui-vue/src/hooks/use-outside-click.ts b/packages/@headlessui-vue/src/hooks/use-outside-click.ts index 69946f4281..181613c0ae 100644 --- a/packages/@headlessui-vue/src/hooks/use-outside-click.ts +++ b/packages/@headlessui-vue/src/hooks/use-outside-click.ts @@ -135,12 +135,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 ) } From 4701a3bdc26173e7521ab2d42f3ac5f1ac13de83 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 3 Jul 2023 15:56:16 +0200 Subject: [PATCH 3/4] listen for `touchend` event to improve "outside click" on mobile devices --- .../src/hooks/use-outside-click.ts | 22 +++++++++++++++++-- .../src/hooks/use-outside-click.ts | 22 +++++++++++++++++-- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/packages/@headlessui-react/src/hooks/use-outside-click.ts b/packages/@headlessui-react/src/hooks/use-outside-click.ts index f208d9c0ca..82c26d8bec 100644 --- a/packages/@headlessui-react/src/hooks/use-outside-click.ts +++ b/packages/@headlessui-react/src/hooks/use-outside-click.ts @@ -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( + function handleOutsideClick( event: E, resolveTarget: (event: E) => HTMLElement | null ) { @@ -143,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 diff --git a/packages/@headlessui-vue/src/hooks/use-outside-click.ts b/packages/@headlessui-vue/src/hooks/use-outside-click.ts index 181613c0ae..eeda763f85 100644 --- a/packages/@headlessui-vue/src/hooks/use-outside-click.ts +++ b/packages/@headlessui-vue/src/hooks/use-outside-click.ts @@ -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 = computed(() => true) ) { - function handleOutsideClick( + function handleOutsideClick( event: E, resolveTarget: (event: E) => HTMLElement | null ) { @@ -126,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 From 5b5f6ec3b770fd7db0f9b60cacc261812ad85b45 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 3 Jul 2023 16:16:15 +0200 Subject: [PATCH 4/4] update changelog --- packages/@headlessui-react/CHANGELOG.md | 1 + packages/@headlessui-vue/CHANGELOG.md | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/@headlessui-react/CHANGELOG.md b/packages/@headlessui-react/CHANGELOG.md index 3a00b99538..5c034bb566 100644 --- a/packages/@headlessui-react/CHANGELOG.md +++ b/packages/@headlessui-react/CHANGELOG.md @@ -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 diff --git a/packages/@headlessui-vue/CHANGELOG.md b/packages/@headlessui-vue/CHANGELOG.md index cb0a52fb6b..9736078a60 100644 --- a/packages/@headlessui-vue/CHANGELOG.md +++ b/packages/@headlessui-vue/CHANGELOG.md @@ -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