-
Notifications
You must be signed in to change notification settings - Fork 0
/
usePopoverClickListener.ts
70 lines (55 loc) · 2.29 KB
/
usePopoverClickListener.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import { defer } from 'lodash'
import useEventListener from '@use-it/event-listener'
import { useEffect, useState } from 'react'
/** @internal */
export function hasAncestor(target: HTMLElement, selector: string): boolean {
const parent = target.parentElement
if (!parent) return false
if (parent.matches(selector)) return true
return hasAncestor(parent, selector)
}
export interface UsePopoverClickListenerOptions {
visible: boolean
onOutsideClick: () => void
popoverSelector?: string
}
/**
* A hook that detects when the user has clicked outside of a popover,
* indicating that the popover should be closed.
*/
export function usePopoverClickListener({
visible,
onOutsideClick,
popoverSelector = '.popover-with-click-listener',
}: UsePopoverClickListenerOptions): void {
const [visibleSince, setVisibleSince] = useState<Date>()
useEffect(() => {
setVisibleSince(visible ? new Date() : undefined)
}, [visible])
function onClick(e: MouseEvent): void {
if (!visible) return
if (!e.target) return
// The next 3 lines prevent onOutsideClick from firing due to the click
// event that showed the popover
if (!visibleSince) return
const msSinceShown = new Date().valueOf() - visibleSince.valueOf()
if (msSinceShown < 50) return
const target = e.target as HTMLElement
// Hack to prevent clicking a react select option from closing the popover
if (hasAncestor(target, '[class$="-menu"]')) return
const inPopover =
target.matches(popoverSelector) || hasAncestor(target, popoverSelector)
if (!inPopover) {
// To understand why we use defer here, consider what would happen if we
// did NOT user defer. When the reference element is clicked, onOutsideClick
// would be called immediately and the popover would be hidden. Then the
// onClick handler of the reference element would fire, immediately showing
// the popover again.
// We used to prevent this using e.stopPropagation(), but stopping
// propagation causes LinkButton act like a link to # instead of
// a button.
defer(onOutsideClick)
}
}
useEventListener('click', onClick)
}