Skip to content

Commit

Permalink
Improve outside click on Safari iOS (#1712)
Browse files Browse the repository at this point in the history
* ensure outside click works on Safari in iOS

When tapping on an element that is not clickable (like a div), then the
`click` and `mousedown` events will not reach the
`window.addEventListener('click')` listeners.

The only event that does that could be interesting for us is the
`pointerdown` event. The issue with this one is that we then run into
the big issue we ran in a few months ago where clicks on a scrollbar
*also* fired while a click doesn't.

This issue was not an issue in React land, the
`window.addEventListener('click')` was fired even when tapping on a
`div`. This was very very confusing, but we think this is because of the
syntethic event system, where the event listener is added to the root of
your application (E.g.: #app) and React manually bubbles the events.
Because this is done manually, it *does* reach the window as well.

The confusing part is, how does React convert a `pointerdown` event to a
`mousedown` and `click`. There is no code for that in their codebase?

Turns out they don't, and turns out the events **do** bubble, but up
until the `document`, not the `window`. But since they are manually
bubbling events it all makes sense.

So the solution? Let's switch from `window` to `document`...

* update Dialog example to use DialogPanel

* update changelog
  • Loading branch information
RobinMalfait committed Jul 26, 2022
1 parent 5af3bd4 commit b2c4023
Show file tree
Hide file tree
Showing 7 changed files with 50 additions and 11 deletions.
1 change: 1 addition & 0 deletions packages/@headlessui-react/CHANGELOG.md
Expand Up @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Resync input when display value changes ([#1679](https://github.com/tailwindlabs/headlessui/pull/1679))
- Ensure controlled `Tabs` don't change automagically ([#1680](https://github.com/tailwindlabs/headlessui/pull/1680))
- Don't scroll lock when a Transition + Dialog is mounted but hidden ([#1681](https://github.com/tailwindlabs/headlessui/pull/1681))
- Improve outside click on Safari iOS ([#1712](https://github.com/tailwindlabs/headlessui/pull/1712))

## [1.6.6] - 2022-07-07

Expand Down
20 changes: 20 additions & 0 deletions packages/@headlessui-react/src/hooks/use-document-event.ts
@@ -0,0 +1,20 @@
import { useEffect } from 'react'

import { useLatestValue } from './use-latest-value'

export function useDocumentEvent<TType extends keyof DocumentEventMap>(
type: TType,
listener: (ev: DocumentEventMap[TType]) => any,
options?: boolean | AddEventListenerOptions
) {
let listenerRef = useLatestValue(listener)

useEffect(() => {
function handler(event: DocumentEventMap[TType]) {
listenerRef.current(event)
}

document.addEventListener(type, handler, options)
return () => document.removeEventListener(type, handler, options)
}, [type, options])
}
8 changes: 4 additions & 4 deletions packages/@headlessui-react/src/hooks/use-outside-click.ts
@@ -1,6 +1,6 @@
import { MutableRefObject, useEffect, useRef } from 'react'
import { FocusableMode, isFocusableElement } from '../utils/focus-management'
import { useWindowEvent } from './use-window-event'
import { useDocumentEvent } from './use-document-event'

type Container = MutableRefObject<HTMLElement | null> | HTMLElement | null
type ContainerCollection = Container[] | Set<Container>
Expand Down Expand Up @@ -92,7 +92,7 @@ export function useOutsideClick(

let initialClickTarget = useRef<EventTarget | null>(null)

useWindowEvent(
useDocumentEvent(
'mousedown',
(event) => {
if (enabledRef.current) {
Expand All @@ -102,7 +102,7 @@ export function useOutsideClick(
true
)

useWindowEvent(
useDocumentEvent(
'click',
(event) => {
if (!initialClickTarget.current) {
Expand Down Expand Up @@ -130,7 +130,7 @@ export function useOutsideClick(
// In this case we care only about the first case so we check to see if the active element is the iframe
// If so this was because of a click, focus, or other interaction with the child iframe
// and we can consider it an "outside click"
useWindowEvent(
useDocumentEvent(
'blur',
(event) =>
handleOutsideClick(event, () =>
Expand Down
1 change: 1 addition & 0 deletions packages/@headlessui-vue/CHANGELOG.md
Expand Up @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Close `Menu` component when using `tab` key ([#1673](https://github.com/tailwindlabs/headlessui/pull/1673))
- Resync input when display value changes ([#1679](https://github.com/tailwindlabs/headlessui/pull/1679))
- Ensure controlled `Tabs` don't change automagically ([#1680](https://github.com/tailwindlabs/headlessui/pull/1680))
- Improve outside click on Safari iOS ([#1712](https://github.com/tailwindlabs/headlessui/pull/1712))

## [1.6.7] - 2022-07-12

Expand Down
15 changes: 15 additions & 0 deletions packages/@headlessui-vue/src/hooks/use-document-event.ts
@@ -0,0 +1,15 @@
import { watchEffect } from 'vue'
import { isServer } from '../utils/ssr'

export function useDocumentEvent<TType extends keyof DocumentEventMap>(
type: TType,
listener: (this: Document, ev: DocumentEventMap[TType]) => any,
options?: boolean | AddEventListenerOptions
) {
if (isServer) return

watchEffect((onInvalidate) => {
document.addEventListener(type, listener, options)
onInvalidate(() => document.removeEventListener(type, listener, options))
})
}
8 changes: 4 additions & 4 deletions packages/@headlessui-vue/src/hooks/use-outside-click.ts
@@ -1,7 +1,7 @@
import { useWindowEvent } from './use-window-event'
import { computed, Ref, ComputedRef, ref } from 'vue'
import { FocusableMode, isFocusableElement } from '../utils/focus-management'
import { dom } from '../utils/dom'
import { useDocumentEvent } from './use-document-event'

type Container = Ref<HTMLElement | null> | HTMLElement | null
type ContainerCollection = Container[] | Set<Container>
Expand Down Expand Up @@ -78,7 +78,7 @@ export function useOutsideClick(

let initialClickTarget = ref<EventTarget | null>(null)

useWindowEvent(
useDocumentEvent(
'mousedown',
(event) => {
if (enabled.value) {
Expand All @@ -88,7 +88,7 @@ export function useOutsideClick(
true
)

useWindowEvent(
useDocumentEvent(
'click',
(event) => {
if (!initialClickTarget.value) {
Expand Down Expand Up @@ -116,7 +116,7 @@ export function useOutsideClick(
// In this case we care only about the first case so we check to see if the active element is the iframe
// If so this was because of a click, focus, or other interaction with the child iframe
// and we can consider it an "outside click"
useWindowEvent(
useDocumentEvent(
'blur',
(event) =>
handleOutsideClick(event, () =>
Expand Down
8 changes: 5 additions & 3 deletions packages/playground-vue/src/components/dialog/dialog.vue
Expand Up @@ -26,7 +26,7 @@
leaveTo="opacity-0"
entered="opacity-75"
>
<DialogOverlay className="fixed inset-0 bg-gray-500 transition-opacity" />
<div className="fixed inset-0 bg-gray-500 transition-opacity" />
</TransitionChild>

<TransitionChild
Expand All @@ -41,7 +41,7 @@
<span class="hidden sm:inline-block sm:h-screen sm:align-middle" aria-hidden="true">
&#8203;
</span>
<div
<DialogPanel
class="inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle"
>
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
Expand Down Expand Up @@ -172,7 +172,7 @@
Cancel
</button>
</div>
</div>
</DialogPanel>
</TransitionChild>
</div>
</div>
Expand All @@ -186,6 +186,7 @@ import {
Dialog,
DialogTitle,
DialogOverlay,
DialogPanel,
Menu,
MenuButton,
MenuItems,
Expand Down Expand Up @@ -268,6 +269,7 @@ export default {
Dialog,
DialogTitle,
DialogOverlay,
DialogPanel,
Menu,
MenuButton,
MenuItems,
Expand Down

0 comments on commit b2c4023

Please sign in to comment.