Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve "outside click" behaviour in combination with 3rd party libraries #2572

Merged
merged 4 commits into from
Jul 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/@headlessui-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
41 changes: 35 additions & 6 deletions packages/@headlessui-react/src/hooks/use-outside-click.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
) {
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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
Expand All @@ -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
)
}
1 change: 1 addition & 0 deletions packages/@headlessui-vue/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
41 changes: 35 additions & 6 deletions packages/@headlessui-vue/src/hooks/use-outside-click.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
) {
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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
Expand All @@ -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
)
}