1
1
/* eslint-env node, browser */
2
- /* eslint no-process-env: 0 */
3
2
4
3
import React from 'react'
5
4
import PropTypes from 'prop-types'
@@ -13,8 +12,21 @@ class FocusWithin extends React.Component {
13
12
focused : false
14
13
}
15
14
15
+ lastBlurEvent = null
16
+
16
17
ref = React . createRef ( )
17
18
19
+ componentDidMount ( ) {
20
+ /**
21
+ * In order for document.body to receive focus events
22
+ * it needs to be focusable. Adding `tabindex="-1"` makes it focusable
23
+ * but prevents it from receiving the focus on user interaction.
24
+ */
25
+ if ( document != null ) {
26
+ document . querySelector ( 'body' ) . setAttribute ( 'tabindex' , '-1' )
27
+ }
28
+ }
29
+
18
30
/**
19
31
* Calls `focus` method on the container node
20
32
*
@@ -28,86 +40,119 @@ class FocusWithin extends React.Component {
28
40
}
29
41
}
30
42
31
- onFocus = evt => {
32
- const { onFocus } = this . props
33
- const { focused } = this . state
34
-
35
- // TODO: Figure out if this check is "safe" or we should rely on SCU instead
36
- if ( ! focused ) {
43
+ /**
44
+ * Event handler that fires if the FocusEvent bubbled up to the document.
45
+ *
46
+ * @private
47
+ * @method _onFocusIn
48
+ *
49
+ * We check if 3 conditions are met:
50
+ * 1. Current state is focused
51
+ * 2. Blur occured inside the container
52
+ * 3. Focus occured outside of the container
53
+ *
54
+ * In this case we fire `onBlur` callback.
55
+ */
56
+ _onFocusIn = ( ) => {
57
+ if (
58
+ this . lastBlurEvent &&
59
+ this . isInsideNode ( this . ref . current , this . lastBlurEvent . target ) &&
60
+ ! this . isInsideNode ( this . ref . current , document . activeElement )
61
+ ) {
37
62
this . setState (
38
63
{
39
- focused : true
64
+ focused : false
40
65
} ,
41
66
( ) => {
42
- onFocus ( evt )
67
+ document . removeEventListener ( 'focusin' , this . _onFocusIn )
68
+ this . props . onBlur ( this . lastBlurEvent )
43
69
}
44
70
)
45
71
}
46
72
}
47
73
48
- onBlur = evt => {
49
- const { onBlur } = this . props
74
+ /**
75
+ * @private
76
+ * @method onFocus
77
+ */
78
+ onFocus = evt => {
79
+ const { onFocus } = this . props
50
80
const { focused } = this . state
51
81
52
- // Do not blur if focus within the container or we're editing
53
- if ( this . isFocusWithin ( this . ref . current ) ) {
54
- evt . preventDefault ( )
55
- evt . stopPropagation ( )
56
- return
57
- }
58
-
59
- // Persist event object
60
- evt . persist ( )
61
-
62
- if ( focused ) {
82
+ /**
83
+ * If it's not focused yet we'll set the state to `focused: true`
84
+ */
85
+ if ( ! focused ) {
63
86
this . setState (
64
87
{
65
- focused : false
88
+ focused : true
66
89
} ,
67
90
( ) => {
68
- onBlur ( evt )
91
+ /**
92
+ * Attach a native event listener to the document. We have to use `focusin` since
93
+ * native `focus` event doesn't bubble. See
94
+ * https://developer.mozilla.org/en-US/docs/Web/Events/focusin and
95
+ * https://developer.mozilla.org/en-US/docs/Web/Events/focus
96
+ */
97
+ document . addEventListener ( 'focusin' , this . _onFocusIn )
98
+ onFocus ( evt )
69
99
}
70
100
)
71
101
}
72
102
}
73
103
74
- isFocusWithin = node => {
75
- // We need to check `:focus-within` on the the parent element in order to work
104
+ /**
105
+ * @private
106
+ * @method onBlur
107
+ */
108
+ onBlur = evt => {
109
+ evt . persist ( ) // Persist the original event since it will be fired later
110
+ this . lastBlurEvent = evt
111
+ }
112
+
113
+ /**
114
+ * Checks if the parentNode contains the node
115
+ *
116
+ * @private
117
+ * @method isInsideNode
118
+ * @param parentNode
119
+ * @param node
120
+ * @returns {boolean }
121
+ */
122
+ isInsideNode = ( parentNode , node ) => {
76
123
if ( process . env . NODE_ENV === 'development' ) {
77
- if (
78
- node == null ||
79
- node . parentNode == null ||
80
- typeof node . parentNode . querySelector !== 'function'
81
- ) {
124
+ if ( parentNode == null || Object ( parentNode ) . nodeType !== 1 ) {
82
125
throw new Error (
83
- 'A ref to a DOM Node with a valid parent Node must be supplied to' +
126
+ 'A ref to a valid DOM Node must be supplied to' +
84
127
' FocusWithin.\n' +
85
128
' You have probably provided a ref to a React Element.\n See https://reactjs.org/docs/react-api.html#refs'
86
129
)
87
130
}
88
131
}
89
- return ! ! node . parentNode . querySelector ( ':focus-within' )
132
+ return parentNode . contains ( node )
90
133
}
91
134
92
135
render ( ) {
93
136
const { children } = this . props
94
137
const { focused } = this . state
95
138
139
+ const events = {
140
+ onFocus : this . onFocus ,
141
+ onBlur : this . onBlur
142
+ }
143
+
96
144
if ( typeof children === 'function' ) {
97
145
return React . cloneElement (
98
146
children ( {
99
147
focused,
100
148
getRef : this . ref
101
149
} ) ,
102
- {
103
- onFocus : this . onFocus ,
104
- onBlur : this . onBlur
105
- }
150
+ events
106
151
)
107
152
}
108
153
109
154
return (
110
- < div ref = { this . ref } onFocus = { this . onFocus } onBlur = { this . onBlur } style = { noOutlineStyles } >
155
+ < div ref = { this . ref } style = { noOutlineStyles } { ... events } >
111
156
{ children }
112
157
</ div >
113
158
)
0 commit comments