Skip to content

Commit 0516001

Browse files
Andrey Okonetchnikovokonet
authored andcommitted
fix(Safari): Rewrite using a different techniqe
Checking with querySelector(':focus-within') only works if the element has focus. Unfortunately, according to the spec `onBlur` event occurs after the blur already happened so the selector always returns `null` in Safari. This commit rewrites the logic to not rely on the CSS selector anymore. This also makes it work in browsers that doesn't support `:focus-within` yet without the need of polyfill. Fixes #4
1 parent 24fc6f8 commit 0516001

File tree

1 file changed

+83
-38
lines changed

1 file changed

+83
-38
lines changed

src/FocusWithin.js

Lines changed: 83 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
/* eslint-env node, browser */
2-
/* eslint no-process-env: 0 */
32

43
import React from 'react'
54
import PropTypes from 'prop-types'
@@ -13,8 +12,21 @@ class FocusWithin extends React.Component {
1312
focused: false
1413
}
1514

15+
lastBlurEvent = null
16+
1617
ref = React.createRef()
1718

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+
1830
/**
1931
* Calls `focus` method on the container node
2032
*
@@ -28,86 +40,119 @@ class FocusWithin extends React.Component {
2840
}
2941
}
3042

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+
) {
3762
this.setState(
3863
{
39-
focused: true
64+
focused: false
4065
},
4166
() => {
42-
onFocus(evt)
67+
document.removeEventListener('focusin', this._onFocusIn)
68+
this.props.onBlur(this.lastBlurEvent)
4369
}
4470
)
4571
}
4672
}
4773

48-
onBlur = evt => {
49-
const { onBlur } = this.props
74+
/**
75+
* @private
76+
* @method onFocus
77+
*/
78+
onFocus = evt => {
79+
const { onFocus } = this.props
5080
const { focused } = this.state
5181

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) {
6386
this.setState(
6487
{
65-
focused: false
88+
focused: true
6689
},
6790
() => {
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)
6999
}
70100
)
71101
}
72102
}
73103

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) => {
76123
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) {
82125
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' +
84127
' FocusWithin.\n' +
85128
' You have probably provided a ref to a React Element.\n See https://reactjs.org/docs/react-api.html#refs'
86129
)
87130
}
88131
}
89-
return !!node.parentNode.querySelector(':focus-within')
132+
return parentNode.contains(node)
90133
}
91134

92135
render() {
93136
const { children } = this.props
94137
const { focused } = this.state
95138

139+
const events = {
140+
onFocus: this.onFocus,
141+
onBlur: this.onBlur
142+
}
143+
96144
if (typeof children === 'function') {
97145
return React.cloneElement(
98146
children({
99147
focused,
100148
getRef: this.ref
101149
}),
102-
{
103-
onFocus: this.onFocus,
104-
onBlur: this.onBlur
105-
}
150+
events
106151
)
107152
}
108153

109154
return (
110-
<div ref={this.ref} onFocus={this.onFocus} onBlur={this.onBlur} style={noOutlineStyles}>
155+
<div ref={this.ref} style={noOutlineStyles} {...events}>
111156
{children}
112157
</div>
113158
)

0 commit comments

Comments
 (0)