Skip to content

Commit

Permalink
further improve outside click
Browse files Browse the repository at this point in the history
We added event.preventDefault() & event.defaultPrevented checks to make
sure that we only handle 1 layer at a time.

E.g.:

```js
<Dialog>
  <Menu>
    <Menu.Button>Button</Menu.Button>
    <Menu.Items>...</Menu.Items>
  </Menu>
</Dialog>
```

If you open the Dialog, then open the Menu, pressing `Escape` will close
the Menu but not the Dialog, pressing `Escape` again will close the
Dialog.

Now this is also applied to the outside click behaviour.
If you open the Dialog, then open the Menu, clicking outside will close
the Menu but not the Dialog, outside again will close the Dialog.
  • Loading branch information
RobinMalfait committed Jun 3, 2022
1 parent a19b301 commit eb439cb
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 3 deletions.
22 changes: 22 additions & 0 deletions packages/@headlessui-react/src/hooks/use-outside-click.ts
@@ -1,4 +1,5 @@
import { MutableRefObject, useEffect, useRef } from 'react'
import { FocusableMode, isFocusableElement } from '../utils/focus-management'
import { useWindowEvent } from './use-window-event'

type Container = MutableRefObject<HTMLElement | null> | HTMLElement | null
Expand Down Expand Up @@ -30,6 +31,11 @@ export function useOutsideClick(
(event) => {
if (!enabledRef.current) return

// Check whether the event got prevented already. This can happen if you use the
// useOutsideClick hook in both a Dialog and a Menu and the inner Menu "cancels" the default
// behaviour so that only the Menu closes and not the Dialog (yet)
if (event.defaultPrevented) return

let _containers = (function resolve(containers): ContainerCollection {
if (typeof containers === 'function') {
return resolve(containers())
Expand Down Expand Up @@ -60,6 +66,22 @@ export function useOutsideClick(
}
}

// This allows us to check whether the event was defaultPrevented when you are nesting this
// inside a `<Dialog />` for example.
if (
// This check alllows us to know whether or not we clicked on a "focusable" element like a
// button or an input. This is a backwards compatibility check so that you can open a <Menu
// /> and click on another <Menu /> which should close Menu A and open Menu B. We might
// revisit that so that you will require 2 clicks instead.
!isFocusableElement(target, FocusableMode.Loose) &&
// This could be improved, but the `Combobox.Button` adds tabIndex={-1} to make it
// unfocusable via the keyboard so that tabbing to the next item from the input doesn't
// first go to the button.
target.tabIndex !== -1
) {
event.preventDefault()
}

return cb(event, target)
},
// We will use the `capture` phase so that layers in between with `event.stopPropagation()`
Expand Down
30 changes: 27 additions & 3 deletions packages/@headlessui-vue/src/hooks/use-outside-click.ts
@@ -1,19 +1,27 @@
import { useWindowEvent } from './use-window-event'
import { Ref } from 'vue'
import { computed, Ref, ComputedRef } from 'vue'
import { FocusableMode, isFocusableElement } from '../utils/focus-management'
import { dom } from '../utils/dom'
import { microTask } from '../utils/micro-task'

type Container = Ref<HTMLElement | null> | HTMLElement | null
type ContainerCollection = Container[] | Set<Container>
type ContainerInput = Container | ContainerCollection

export function useOutsideClick(
containers: ContainerInput | (() => ContainerInput),
cb: (event: MouseEvent | PointerEvent, target: HTMLElement) => void
cb: (event: MouseEvent | PointerEvent, target: HTMLElement) => void,
enabled: ComputedRef<boolean> = computed(() => true)
) {
useWindowEvent(
'click',
(event) => {
if (!enabled.value) return

// Check whether the event got prevented already. This can happen if you use the
// useOutsideClick hook in both a Dialog and a Menu and the inner Menu "cancels" the default
// behaviour so that only the Menu closes and not the Dialog (yet)
if (event.defaultPrevented) return

let target = event.target as HTMLElement

// Ignore if the target doesn't exist in the DOM anymore
Expand Down Expand Up @@ -44,6 +52,22 @@ export function useOutsideClick(
}
}

// This allows us to check whether the event was defaultPrevented when you are nesting this
// inside a `<Dialog />` for example.
if (
// This check alllows us to know whether or not we clicked on a "focusable" element like a
// button or an input. This is a backwards compatibility check so that you can open a <Menu
// /> and click on another <Menu /> which should close Menu A and open Menu B. We might
// revisit that so that you will require 2 clicks instead.
!isFocusableElement(target, FocusableMode.Loose) &&
// This could be improved, but the `Combobox.Button` adds tabIndex={-1} to make it
// unfocusable via the keyboard so that tabbing to the next item from the input doesn't
// first go to the button.
target.tabIndex !== -1
) {
event.preventDefault()
}

cb(event, target)
},
// We will use the `capture` phase so that layers in between with `event.stopPropagation()`
Expand Down

0 comments on commit eb439cb

Please sign in to comment.