15
15
// NOTICE file in the root directory of this source tree.
16
16
// See https://github.com/facebook/react/tree/cc7c1aece46a6b69b41958d731e0fd27c94bfc6c/packages/react-interactions
17
17
18
- import { isMac , isVirtualClick } from '@react-aria/utils' ;
18
+ import { getOwnerDocument , getOwnerWindow , isMac , isVirtualClick } from '@react-aria/utils' ;
19
19
import { useEffect , useState } from 'react' ;
20
20
import { useIsSSR } from '@react-aria/ssr' ;
21
21
@@ -37,7 +37,7 @@ export interface FocusVisibleResult {
37
37
38
38
let currentModality : null | Modality = null ;
39
39
let changeHandlers = new Set < Handler > ( ) ;
40
- let hasSetupGlobalListeners = false ;
40
+ export let hasSetupGlobalListeners = new Map < Window , boolean > ( ) ; // We use a map here to support setting event listeners across multiple document objects.
41
41
let hasEventBeforeFocus = false ;
42
42
let hasBlurredWindowRecently = false ;
43
43
@@ -114,49 +114,84 @@ function handleWindowBlur() {
114
114
/**
115
115
* Setup global event listeners to control when keyboard focus style should be visible.
116
116
*/
117
- function setupGlobalFocusEvents ( ) {
118
- if ( typeof window === 'undefined' || hasSetupGlobalListeners ) {
117
+ function setupGlobalFocusEvents ( element ?: HTMLElement | null ) {
118
+ if ( typeof window === 'undefined' || hasSetupGlobalListeners . get ( getOwnerWindow ( element ) ) ) {
119
119
return ;
120
120
}
121
121
122
+ const windowObject = getOwnerWindow ( element ) ;
123
+ const documentObject = getOwnerDocument ( element ) ;
124
+
122
125
// Programmatic focus() calls shouldn't affect the current input modality.
123
126
// However, we need to detect other cases when a focus event occurs without
124
127
// a preceding user event (e.g. screen reader focus). Overriding the focus
125
128
// method on HTMLElement.prototype is a bit hacky, but works.
126
- let focus = HTMLElement . prototype . focus ;
127
- HTMLElement . prototype . focus = function ( ) {
129
+ let focus = windowObject . HTMLElement . prototype . focus ;
130
+ windowObject . HTMLElement . prototype . focus = function ( ) {
128
131
hasEventBeforeFocus = true ;
129
132
focus . apply ( this , arguments as unknown as [ options ?: FocusOptions | undefined ] ) ;
130
133
} ;
131
134
132
- document . addEventListener ( 'keydown' , handleKeyboardEvent , true ) ;
133
- document . addEventListener ( 'keyup' , handleKeyboardEvent , true ) ;
134
- document . addEventListener ( 'click' , handleClickEvent , true ) ;
135
+ documentObject . addEventListener ( 'keydown' , handleKeyboardEvent , true ) ;
136
+ documentObject . addEventListener ( 'keyup' , handleKeyboardEvent , true ) ;
137
+ documentObject . addEventListener ( 'click' , handleClickEvent , true ) ;
135
138
136
139
// Register focus events on the window so they are sure to happen
137
140
// before React's event listeners (registered on the document).
138
- window . addEventListener ( 'focus' , handleFocusEvent , true ) ;
139
- window . addEventListener ( 'blur' , handleWindowBlur , false ) ;
141
+ windowObject . addEventListener ( 'focus' , handleFocusEvent , true ) ;
142
+ windowObject . addEventListener ( 'blur' , handleWindowBlur , false ) ;
140
143
141
144
if ( typeof PointerEvent !== 'undefined' ) {
142
- document . addEventListener ( 'pointerdown' , handlePointerEvent , true ) ;
143
- document . addEventListener ( 'pointermove' , handlePointerEvent , true ) ;
144
- document . addEventListener ( 'pointerup' , handlePointerEvent , true ) ;
145
+ documentObject . addEventListener ( 'pointerdown' , handlePointerEvent , true ) ;
146
+ documentObject . addEventListener ( 'pointermove' , handlePointerEvent , true ) ;
147
+ documentObject . addEventListener ( 'pointerup' , handlePointerEvent , true ) ;
145
148
} else {
146
- document . addEventListener ( 'mousedown' , handlePointerEvent , true ) ;
147
- document . addEventListener ( 'mousemove' , handlePointerEvent , true ) ;
148
- document . addEventListener ( 'mouseup' , handlePointerEvent , true ) ;
149
+ documentObject . addEventListener ( 'mousedown' , handlePointerEvent , true ) ;
150
+ documentObject . addEventListener ( 'mousemove' , handlePointerEvent , true ) ;
151
+ documentObject . addEventListener ( 'mouseup' , handlePointerEvent , true ) ;
149
152
}
150
153
151
- hasSetupGlobalListeners = true ;
154
+ // Add unmount handler
155
+ windowObject . addEventListener ( 'beforeunload' , ( ) => {
156
+ documentObject . removeEventListener ( 'keydown' , handleKeyboardEvent , true ) ;
157
+ documentObject . removeEventListener ( 'keyup' , handleKeyboardEvent , true ) ;
158
+ documentObject . removeEventListener ( 'click' , handleClickEvent , true ) ;
159
+ windowObject . removeEventListener ( 'focus' , handleFocusEvent , true ) ;
160
+ windowObject . removeEventListener ( 'blur' , handleWindowBlur , false ) ;
161
+
162
+ if ( typeof PointerEvent !== 'undefined' ) {
163
+ documentObject . removeEventListener ( 'pointerdown' , handlePointerEvent , true ) ;
164
+ documentObject . removeEventListener ( 'pointermove' , handlePointerEvent , true ) ;
165
+ documentObject . removeEventListener ( 'pointerup' , handlePointerEvent , true ) ;
166
+ } else {
167
+ documentObject . removeEventListener ( 'mousedown' , handlePointerEvent , true ) ;
168
+ documentObject . removeEventListener ( 'mousemove' , handlePointerEvent , true ) ;
169
+ documentObject . removeEventListener ( 'mouseup' , handlePointerEvent , true ) ;
170
+ }
171
+
172
+ if ( hasSetupGlobalListeners . has ( windowObject ) ) {
173
+ hasSetupGlobalListeners . delete ( windowObject ) ;
174
+ }
175
+ } , { once : true } ) ;
176
+
177
+ hasSetupGlobalListeners . set ( windowObject , true ) ;
152
178
}
153
179
154
- if ( typeof document !== 'undefined' ) {
155
- if ( document . readyState !== 'loading' ) {
156
- setupGlobalFocusEvents ( ) ;
180
+ export const setupFocus = ( element ?: HTMLElement | null ) => {
181
+ const documentObject = getOwnerDocument ( element ) ;
182
+ if ( documentObject . readyState !== 'loading' ) {
183
+ setupGlobalFocusEvents ( element ) ;
157
184
} else {
158
- document . addEventListener ( 'DOMContentLoaded' , setupGlobalFocusEvents ) ;
185
+ documentObject . addEventListener ( 'DOMContentLoaded' , ( ) =>
186
+ setupGlobalFocusEvents ( element )
187
+ ) ;
159
188
}
189
+ } ;
190
+
191
+ // Server-side rendering does not have the document object defined
192
+ // eslint-disable-next-line no-restricted-globals
193
+ if ( typeof document !== 'undefined' ) {
194
+ setupFocus ( ) ;
160
195
}
161
196
162
197
/**
@@ -213,11 +248,16 @@ const nonTextInputTypes = new Set([
213
248
* focus visible style can be properly set.
214
249
*/
215
250
function isKeyboardFocusEvent ( isTextInput : boolean , modality : Modality , e : HandlerEvent ) {
216
- isTextInput = isTextInput ||
217
- ( e ?. target instanceof HTMLInputElement && ! nonTextInputTypes . has ( e ?. target ?. type ) ) ||
218
- e ?. target instanceof HTMLTextAreaElement ||
219
- ( e ?. target instanceof HTMLElement && e ?. target . isContentEditable ) ;
220
- return ! ( isTextInput && modality === 'keyboard' && e instanceof KeyboardEvent && ! FOCUS_VISIBLE_INPUT_KEYS [ e . key ] ) ;
251
+ const IHTMLInputElement = typeof window !== 'undefined' ? getOwnerWindow ( e ?. target as Element ) . HTMLInputElement : HTMLInputElement ;
252
+ const IHTMLTextAreaElement = typeof window !== 'undefined' ? getOwnerWindow ( e ?. target as Element ) . HTMLTextAreaElement : HTMLTextAreaElement ;
253
+ const IHTMLElement = typeof window !== 'undefined' ? getOwnerWindow ( e ?. target as Element ) . HTMLElement : HTMLElement ;
254
+ const IKeyboardEvent = typeof window !== 'undefined' ? getOwnerWindow ( e ?. target as Element ) . KeyboardEvent : KeyboardEvent ;
255
+
256
+ isTextInput = isTextInput ||
257
+ ( e ?. target instanceof IHTMLInputElement && ! nonTextInputTypes . has ( e ?. target ?. type ) ) ||
258
+ e ?. target instanceof IHTMLTextAreaElement ||
259
+ ( e ?. target instanceof IHTMLElement && e ?. target . isContentEditable ) ;
260
+ return ! ( isTextInput && modality === 'keyboard' && e instanceof IKeyboardEvent && ! FOCUS_VISIBLE_INPUT_KEYS [ e . key ] ) ;
221
261
}
222
262
223
263
/**
0 commit comments