Skip to content

Commit 741fb7e

Browse files
iFrame focus tracking setup rename (#5754)
* iFrame focus tracking setup rename * fix ts * fix ts * fix lint --------- Co-authored-by: Daniel Lu <dl1644@gmail.com>
1 parent 6915387 commit 741fb7e

File tree

3 files changed

+106
-40
lines changed

3 files changed

+106
-40
lines changed

packages/@react-aria/interactions/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export {
1717
isFocusVisible,
1818
getInteractionModality,
1919
setInteractionModality,
20-
setupFocus,
20+
addWindowFocusTracking,
2121
useInteractionModality,
2222
useFocusVisible,
2323
useFocusVisibleListener

packages/@react-aria/interactions/src/useFocusVisible.ts

Lines changed: 49 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@ export interface FocusVisibleResult {
3737

3838
let currentModality: null | Modality = null;
3939
let changeHandlers = new Set<Handler>();
40-
export let hasSetupGlobalListeners = new Map<Window, boolean>(); // We use a map here to support setting event listeners across multiple document objects.
40+
interface GlobalListenerData {
41+
focus: () => void
42+
}
43+
export let hasSetupGlobalListeners = new Map<Window, GlobalListenerData>(); // We use a map here to support setting event listeners across multiple document objects.
4144
let hasEventBeforeFocus = false;
4245
let hasBlurredWindowRecently = false;
4346

@@ -153,45 +156,65 @@ function setupGlobalFocusEvents(element?: HTMLElement | null) {
153156

154157
// Add unmount handler
155158
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-
}
159+
tearDownWindowFocusTracking(element);
175160
}, {once: true});
176161

177-
hasSetupGlobalListeners.set(windowObject, true);
162+
hasSetupGlobalListeners.set(windowObject, {focus});
178163
}
179164

180-
export const setupFocus = (element?: HTMLElement | null) => {
165+
const tearDownWindowFocusTracking = (element, loadListener?: () => void) => {
166+
const windowObject = getOwnerWindow(element);
167+
const documentObject = getOwnerDocument(element);
168+
if (loadListener) {
169+
documentObject.removeEventListener('DOMContentLoaded', loadListener);
170+
}
171+
if (!hasSetupGlobalListeners.has(windowObject)) {
172+
return;
173+
}
174+
windowObject.HTMLElement.prototype.focus = hasSetupGlobalListeners.get(windowObject)!.focus;
175+
176+
documentObject.removeEventListener('keydown', handleKeyboardEvent, true);
177+
documentObject.removeEventListener('keyup', handleKeyboardEvent, true);
178+
documentObject.removeEventListener('click', handleClickEvent, true);
179+
windowObject.removeEventListener('focus', handleFocusEvent, true);
180+
windowObject.removeEventListener('blur', handleWindowBlur, false);
181+
182+
if (typeof PointerEvent !== 'undefined') {
183+
documentObject.removeEventListener('pointerdown', handlePointerEvent, true);
184+
documentObject.removeEventListener('pointermove', handlePointerEvent, true);
185+
documentObject.removeEventListener('pointerup', handlePointerEvent, true);
186+
} else {
187+
documentObject.removeEventListener('mousedown', handlePointerEvent, true);
188+
documentObject.removeEventListener('mousemove', handlePointerEvent, true);
189+
documentObject.removeEventListener('mouseup', handlePointerEvent, true);
190+
}
191+
192+
hasSetupGlobalListeners.delete(windowObject);
193+
};
194+
195+
/**
196+
* Adds a window (ie iframe) to the list of windows that are being tracked for focus visible.
197+
* @param element @default document.body - The element provided will be used to get the window to add.
198+
*/
199+
export const addWindowFocusTracking = (element?: HTMLElement | null) => {
181200
const documentObject = getOwnerDocument(element);
201+
let loadListener;
182202
if (documentObject.readyState !== 'loading') {
183203
setupGlobalFocusEvents(element);
184204
} else {
185-
documentObject.addEventListener('DOMContentLoaded', () =>
186-
setupGlobalFocusEvents(element)
187-
);
205+
loadListener = () => {
206+
setupGlobalFocusEvents(element);
207+
};
208+
documentObject.addEventListener('DOMContentLoaded', loadListener);
188209
}
210+
211+
return () => tearDownWindowFocusTracking(element, loadListener);
189212
};
190213

191214
// Server-side rendering does not have the document object defined
192215
// eslint-disable-next-line no-restricted-globals
193216
if (typeof document !== 'undefined') {
194-
setupFocus();
217+
addWindowFocusTracking();
195218
}
196219

197220
/**

packages/@react-aria/interactions/test/useFocusVisible.test.js

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212
import {act, fireEvent, render, renderHook, screen, waitFor} from '@react-spectrum/test-utils';
13+
import {addWindowFocusTracking, useFocusVisible, useFocusVisibleListener} from '../';
1314
import {hasSetupGlobalListeners} from '../src/useFocusVisible';
1415
import React from 'react';
1516
import {render as ReactDOMRender} from 'react-dom';
16-
import {setupFocus, useFocusVisible, useFocusVisibleListener} from '../';
1717

1818
function Example(props) {
1919
const {isFocusVisible} = useFocusVisible();
20-
return <div id={props.id}>example{isFocusVisible && '-focusVisible'}</div>;
20+
return <div {...props}>example{isFocusVisible && '-focusVisible'}</div>;
2121
}
2222

2323
function toggleBrowserTabs() {
@@ -103,7 +103,7 @@ describe('useFocusVisible', function () {
103103
iframe.contentWindow.document.body.appendChild(iframeRoot);
104104
});
105105

106-
afterEach(() => {
106+
afterEach(async () => {
107107
fireEvent(iframe.contentWindow, new Event('beforeunload'));
108108
iframe.remove();
109109
});
@@ -120,7 +120,7 @@ describe('useFocusVisible', function () {
120120
expect(el.textContent).toBe('example');
121121

122122
// Setup focus in iframe
123-
setupFocus(iframeRoot);
123+
addWindowFocusTracking(iframeRoot);
124124
expect(el.textContent).toBe('example');
125125

126126
// Focus in iframe after setupFocus
@@ -129,16 +129,19 @@ describe('useFocusVisible', function () {
129129
});
130130

131131
it('removes event listeners on beforeunload', async function () {
132-
ReactDOMRender(<Example id="iframe-example" />, iframeRoot);
133-
setupFocus(iframeRoot);
132+
let tree = render(<Example data-testid="iframe-example" />, iframeRoot);
134133

135134
await waitFor(() => {
136-
expect(document.querySelector('iframe').contentWindow.document.body.querySelector('div[id="iframe-example"]')).toBeTruthy();
135+
expect(tree.getByTestId('iframe-example')).toBeTruthy();
137136
});
138-
const el = document.querySelector('iframe').contentWindow.document.body.querySelector('div[id="iframe-example"]');
137+
const el = tree.getByTestId('iframe-example');
138+
// trigger keyboard focus
139+
fireEvent.keyDown(el, {key: 'a'});
140+
fireEvent.keyUp(el, {key: 'a'});
139141
expect(el.textContent).toBe('example-focusVisible');
140142

141143
fireEvent.mouseDown(el);
144+
fireEvent.mouseUp(el);
142145
expect(el.textContent).toBe('example');
143146

144147
// Focus events after beforeunload no longer work
@@ -147,14 +150,36 @@ describe('useFocusVisible', function () {
147150
expect(el.textContent).toBe('example');
148151
});
149152

153+
it('removes event listeners using teardown function', async function () {
154+
let tree = render(<Example data-testid="iframe-example" />, iframeRoot);
155+
let tearDown = addWindowFocusTracking(iframeRoot);
156+
157+
await waitFor(() => {
158+
expect(tree.getByTestId('iframe-example')).toBeTruthy();
159+
});
160+
const el = tree.getByTestId('iframe-example');
161+
// trigger keyboard focus
162+
fireEvent.keyDown(el, {key: 'a'});
163+
fireEvent.keyUp(el, {key: 'a'});
164+
expect(el.textContent).toBe('example-focusVisible');
165+
166+
fireEvent.mouseDown(el);
167+
fireEvent.mouseUp(el);
168+
expect(el.textContent).toBe('example');
169+
170+
tearDown();
171+
fireEvent.focus(iframe.contentWindow.document.body);
172+
expect(el.textContent).toBe('example');
173+
});
174+
150175
it('removes the window object from the hasSetupGlobalListeners object on beforeunload', async function () {
151176
ReactDOMRender(<Example id="iframe-example" />, iframeRoot);
152177
expect(hasSetupGlobalListeners.size).toBe(1);
153178
expect(hasSetupGlobalListeners.get(window)).toBeTruthy();
154179
expect(hasSetupGlobalListeners.get(iframe.contentWindow)).toBeFalsy();
155180

156181
// After setup focus
157-
setupFocus(iframeRoot);
182+
addWindowFocusTracking(iframeRoot);
158183
expect(hasSetupGlobalListeners.size).toBe(2);
159184
expect(hasSetupGlobalListeners.get(window)).toBeTruthy();
160185
expect(hasSetupGlobalListeners.get(iframe.contentWindow)).toBeTruthy();
@@ -166,9 +191,27 @@ describe('useFocusVisible', function () {
166191
expect(hasSetupGlobalListeners.get(iframe.contentWindow)).toBeFalsy();
167192
});
168193

194+
it('removes the window object from the hasSetupGlobalListeners object if we preemptively tear down', async function () {
195+
ReactDOMRender(<Example id="iframe-example" />, iframeRoot);
196+
expect(hasSetupGlobalListeners.size).toBe(1);
197+
expect(hasSetupGlobalListeners.get(window)).toBeTruthy();
198+
expect(hasSetupGlobalListeners.get(iframe.contentWindow)).toBeFalsy();
199+
200+
// After setup focus
201+
let tearDown = addWindowFocusTracking(iframeRoot);
202+
expect(hasSetupGlobalListeners.size).toBe(2);
203+
expect(hasSetupGlobalListeners.get(window)).toBeTruthy();
204+
expect(hasSetupGlobalListeners.get(iframe.contentWindow)).toBeTruthy();
205+
206+
tearDown();
207+
expect(hasSetupGlobalListeners.size).toBe(1);
208+
expect(hasSetupGlobalListeners.get(window)).toBeTruthy();
209+
expect(hasSetupGlobalListeners.get(iframe.contentWindow)).toBeFalsy();
210+
});
211+
169212
it('returns positive isFocusVisible result after toggling browser tabs after keyboard navigation', async function () {
170213
ReactDOMRender(<Example id="iframe-example" />, iframeRoot);
171-
setupFocus(iframeRoot);
214+
addWindowFocusTracking(iframeRoot);
172215

173216
// Fire focus in iframe
174217
await waitFor(() => {
@@ -188,7 +231,7 @@ describe('useFocusVisible', function () {
188231

189232
it('returns negative isFocusVisible result after toggling browser tabs without prior keyboard navigation', async function () {
190233
ReactDOMRender(<Example id="iframe-example" />, iframeRoot);
191-
setupFocus(iframeRoot);
234+
addWindowFocusTracking(iframeRoot);
192235

193236
// Fire focus in iframe
194237
await waitFor(() => {
@@ -206,7 +249,7 @@ describe('useFocusVisible', function () {
206249

207250
it('returns positive isFocusVisible result after toggling browser window after keyboard navigation', async function () {
208251
ReactDOMRender(<Example id="iframe-example" />, iframeRoot);
209-
setupFocus(iframeRoot);
252+
addWindowFocusTracking(iframeRoot);
210253

211254
// Fire focus in iframe
212255
await waitFor(() => {
@@ -225,7 +268,7 @@ describe('useFocusVisible', function () {
225268

226269
it('returns negative isFocusVisible result after toggling browser window without prior keyboard navigation', async function () {
227270
ReactDOMRender(<Example id="iframe-example" />, iframeRoot);
228-
setupFocus(iframeRoot);
271+
addWindowFocusTracking(iframeRoot);
229272

230273
// Fire focus in iframe
231274
await waitFor(() => {

0 commit comments

Comments
 (0)