@@ -8,7 +8,7 @@ type ContainerInput = Container | ContainerCollection
8
8
9
9
export function useOutsideClick (
10
10
containers : ContainerInput | ( ( ) => ContainerInput ) ,
11
- cb : ( event : MouseEvent | PointerEvent , target : HTMLElement ) => void ,
11
+ cb : ( event : MouseEvent | PointerEvent | FocusEvent , target : HTMLElement ) => void ,
12
12
enabled : boolean = true
13
13
) {
14
14
// TODO: remove this once the React bug has been fixed: https://github.com/facebook/react/issues/24657
@@ -26,68 +26,96 @@ export function useOutsideClick(
26
26
[ enabled ]
27
27
)
28
28
29
- useWindowEvent (
30
- 'click' ,
31
- ( event ) => {
32
- if ( ! enabledRef . current ) return
29
+ function handleOutsideClick < E extends MouseEvent | PointerEvent | FocusEvent > (
30
+ event : E ,
31
+ resolveTarget : ( event : E ) => HTMLElement | null
32
+ ) {
33
+ if ( ! enabledRef . current ) return
33
34
34
- // Check whether the event got prevented already. This can happen if you use the
35
- // useOutsideClick hook in both a Dialog and a Menu and the inner Menu "cancels" the default
36
- // behaviour so that only the Menu closes and not the Dialog (yet)
37
- if ( event . defaultPrevented ) return
35
+ // Check whether the event got prevented already. This can happen if you use the
36
+ // useOutsideClick hook in both a Dialog and a Menu and the inner Menu "cancels" the default
37
+ // behaviour so that only the Menu closes and not the Dialog (yet)
38
+ if ( event . defaultPrevented ) return
38
39
39
- let _containers = ( function resolve ( containers ) : ContainerCollection {
40
- if ( typeof containers === 'function' ) {
41
- return resolve ( containers ( ) )
42
- }
40
+ let _containers = ( function resolve ( containers ) : ContainerCollection {
41
+ if ( typeof containers === 'function' ) {
42
+ return resolve ( containers ( ) )
43
+ }
43
44
44
- if ( Array . isArray ( containers ) ) {
45
- return containers
46
- }
45
+ if ( Array . isArray ( containers ) ) {
46
+ return containers
47
+ }
47
48
48
- if ( containers instanceof Set ) {
49
- return containers
50
- }
49
+ if ( containers instanceof Set ) {
50
+ return containers
51
+ }
51
52
52
- return [ containers ]
53
- } ) ( containers )
53
+ return [ containers ]
54
+ } ) ( containers )
54
55
55
- let target = event . target as HTMLElement
56
+ let target = resolveTarget ( event )
56
57
57
- // Ignore if the target doesn't exist in the DOM anymore
58
- if ( ! target . ownerDocument . documentElement . contains ( target ) ) return
58
+ if ( target === null ) {
59
+ return
60
+ }
59
61
60
- // Ignore if the target exists in one of the containers
61
- for ( let container of _containers ) {
62
- if ( container === null ) continue
63
- let domNode = container instanceof HTMLElement ? container : container . current
64
- if ( domNode ?. contains ( target ) ) {
65
- return
66
- }
67
- }
62
+ // Ignore if the target doesn't exist in the DOM anymore
63
+ if ( ! target . ownerDocument . documentElement . contains ( target ) ) return
68
64
69
- // This allows us to check whether the event was defaultPrevented when you are nesting this
70
- // inside a `<Dialog />` for example.
71
- if (
72
- // This check alllows us to know whether or not we clicked on a "focusable" element like a
73
- // button or an input. This is a backwards compatibility check so that you can open a <Menu
74
- // /> and click on another <Menu /> which should close Menu A and open Menu B. We might
75
- // revisit that so that you will require 2 clicks instead.
76
- ! isFocusableElement ( target , FocusableMode . Loose ) &&
77
- // This could be improved, but the `Combobox.Button` adds tabIndex={-1} to make it
78
- // unfocusable via the keyboard so that tabbing to the next item from the input doesn't
79
- // first go to the button.
80
- target . tabIndex !== - 1
81
- ) {
82
- event . preventDefault ( )
65
+ // Ignore if the target exists in one of the containers
66
+ for ( let container of _containers ) {
67
+ if ( container === null ) continue
68
+ let domNode = container instanceof HTMLElement ? container : container . current
69
+ if ( domNode ?. contains ( target ) ) {
70
+ return
83
71
}
72
+ }
73
+
74
+ // This allows us to check whether the event was defaultPrevented when you are nesting this
75
+ // inside a `<Dialog />` for example.
76
+ if (
77
+ // This check alllows us to know whether or not we clicked on a "focusable" element like a
78
+ // button or an input. This is a backwards compatibility check so that you can open a <Menu
79
+ // /> and click on another <Menu /> which should close Menu A and open Menu B. We might
80
+ // revisit that so that you will require 2 clicks instead.
81
+ ! isFocusableElement ( target , FocusableMode . Loose ) &&
82
+ // This could be improved, but the `Combobox.Button` adds tabIndex={-1} to make it
83
+ // unfocusable via the keyboard so that tabbing to the next item from the input doesn't
84
+ // first go to the button.
85
+ target . tabIndex !== - 1
86
+ ) {
87
+ event . preventDefault ( )
88
+ }
89
+
90
+ return cb ( event , target )
91
+ }
92
+
93
+ useWindowEvent (
94
+ 'click' ,
95
+ ( event ) => handleOutsideClick ( event , ( event ) => event . target as HTMLElement ) ,
84
96
85
- return cb ( event , target )
86
- } ,
87
97
// We will use the `capture` phase so that layers in between with `event.stopPropagation()`
88
98
// don't "cancel" this outside click check. E.g.: A `Menu` inside a `DialogPanel` if the `Menu`
89
99
// is open, and you click outside of it in the `DialogPanel` the `Menu` should close. However,
90
100
// the `DialogPanel` has a `onClick(e) { e.stopPropagation() }` which would cancel this.
91
101
true
92
102
)
103
+
104
+ // When content inside an iframe is clicked `window` will receive a blur event
105
+ // This can happen when an iframe _inside_ a window is clicked
106
+ // Or, if headless UI is *in* the iframe, when a content in a window containing that iframe is clicked
107
+
108
+ // In this case we care only about the first case so we check to see if the active element is the iframe
109
+ // If so this was because of a click, focus, or other interaction with the child iframe
110
+ // and we can consider it an "outside click"
111
+ useWindowEvent (
112
+ 'blur' ,
113
+ ( event ) =>
114
+ handleOutsideClick ( event , ( ) =>
115
+ window . document . activeElement instanceof HTMLIFrameElement
116
+ ? window . document . activeElement
117
+ : null
118
+ ) ,
119
+ true
120
+ )
93
121
}
0 commit comments