Skip to content

Commit a152827

Browse files
authored
Refine the heuristics around beforeblur/afterblur (facebook#18668)
* Refine the heuristics around beforeblur/afterblur
1 parent 707478e commit a152827

File tree

14 files changed

+381
-52
lines changed

14 files changed

+381
-52
lines changed

packages/react-art/src/ReactARTHostConfig.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,7 @@ export function getPublicInstance(instance) {
306306

307307
export function prepareForCommit() {
308308
// Noop
309+
return null;
309310
}
310311

311312
export function prepareUpdate(domElement, type, oldProps, newProps) {
@@ -507,3 +508,11 @@ export function unmountEventListener(listener: any) {
507508
export function validateEventListenerTarget(target: any, listener: any) {
508509
throw new Error('Not yet implemented.');
509510
}
511+
512+
export function beforeActiveInstanceBlur() {
513+
// noop
514+
}
515+
516+
export function afterActiveInstanceBlur() {
517+
// noop
518+
}

packages/react-dom/src/client/ReactDOMHostConfig.js

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,6 @@ import {
9191
import {getListenerMapForElement} from '../events/DOMEventListenerMap';
9292
import {TOP_BEFORE_BLUR, TOP_AFTER_BLUR} from '../events/DOMTopLevelEventTypes';
9393

94-
// TODO: This is an exposed internal, we should move this around
95-
// so this isn't the case.
96-
import {isFiberInsideHiddenOrRemovedTree} from 'react-reconciler/src/ReactFiberTreeReflection';
97-
9894
export type ReactListenerEvent = ReactDOMListenerEvent;
9995
export type ReactListenerMap = ReactDOMListenerMap;
10096
export type ReactListener = ReactDOMListener;
@@ -159,7 +155,6 @@ export opaque type OpaqueIDType =
159155
};
160156

161157
type SelectionInformation = {|
162-
activeElementDetached: null | HTMLElement,
163158
focusedElem: null | HTMLElement,
164159
selectionRange: mixed,
165160
|};
@@ -247,32 +242,40 @@ export function getPublicInstance(instance: Instance): * {
247242
return instance;
248243
}
249244

250-
export function prepareForCommit(containerInfo: Container): void {
245+
export function prepareForCommit(containerInfo: Container): Object | null {
251246
eventsEnabled = ReactBrowserEventEmitterIsEnabled();
252247
selectionInformation = getSelectionInformation();
248+
let activeInstance = null;
253249
if (enableDeprecatedFlareAPI || enableUseEventAPI) {
254250
const focusedElem = selectionInformation.focusedElem;
255251
if (focusedElem !== null) {
256-
const instance = getClosestInstanceFromNode(focusedElem);
257-
if (instance !== null && isFiberInsideHiddenOrRemovedTree(instance)) {
258-
dispatchBeforeDetachedBlur(focusedElem);
259-
}
252+
activeInstance = getClosestInstanceFromNode(focusedElem);
260253
}
261254
}
262255
ReactBrowserEventEmitterSetEnabled(false);
256+
return activeInstance;
257+
}
258+
259+
export function beforeActiveInstanceBlur(): void {
260+
if (enableDeprecatedFlareAPI || enableUseEventAPI) {
261+
ReactBrowserEventEmitterSetEnabled(true);
262+
dispatchBeforeDetachedBlur((selectionInformation: any).focusedElem);
263+
ReactBrowserEventEmitterSetEnabled(false);
264+
}
265+
}
266+
267+
export function afterActiveInstanceBlur(): void {
268+
if (enableDeprecatedFlareAPI || enableUseEventAPI) {
269+
ReactBrowserEventEmitterSetEnabled(true);
270+
dispatchAfterDetachedBlur((selectionInformation: any).focusedElem);
271+
ReactBrowserEventEmitterSetEnabled(false);
272+
}
263273
}
264274

265275
export function resetAfterCommit(containerInfo: Container): void {
266276
restoreSelection(selectionInformation);
267277
ReactBrowserEventEmitterSetEnabled(eventsEnabled);
268278
eventsEnabled = null;
269-
if (enableDeprecatedFlareAPI || enableUseEventAPI) {
270-
const activeElementDetached = (selectionInformation: any)
271-
.activeElementDetached;
272-
if (activeElementDetached !== null) {
273-
dispatchAfterDetachedBlur(activeElementDetached);
274-
}
275-
}
276279
selectionInformation = null;
277280
}
278281

@@ -525,8 +528,6 @@ function createEvent(type: TopLevelType): Event {
525528
}
526529

527530
function dispatchBeforeDetachedBlur(target: HTMLElement): void {
528-
((selectionInformation: any): SelectionInformation).activeElementDetached = target;
529-
530531
if (enableDeprecatedFlareAPI || enableUseEventAPI) {
531532
const event = createEvent(TOP_BEFORE_BLUR);
532533
// Dispatch "beforeblur" directly on the target,

packages/react-dom/src/client/ReactInputSelection.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,6 @@ export function hasSelectionCapabilities(elem) {
100100
export function getSelectionInformation() {
101101
const focusedElem = getActiveElementDeep();
102102
return {
103-
// Used by Flare
104-
activeElementDetached: null,
105103
focusedElem: focusedElem,
106104
selectionRange: hasSelectionCapabilities(focusedElem)
107105
? getSelection(focusedElem)

packages/react-interactions/events/src/dom/__tests__/FocusWithin-test.internal.js

Lines changed: 116 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ let ReactFeatureFlags;
1616
let ReactDOM;
1717
let FocusWithinResponder;
1818
let useFocusWithin;
19-
let Scheduler;
19+
let ReactTestRenderer;
20+
let act;
2021

2122
const initializeModules = hasPointerEvents => {
2223
setPointerEvent(hasPointerEvents);
@@ -26,7 +27,8 @@ const initializeModules = hasPointerEvents => {
2627
ReactFeatureFlags.enableScopeAPI = true;
2728
React = require('react');
2829
ReactDOM = require('react-dom');
29-
Scheduler = require('scheduler');
30+
ReactTestRenderer = require('react-test-renderer');
31+
act = ReactTestRenderer.act;
3032

3133
// TODO: This import throws outside of experimental mode. Figure out better
3234
// strategy for gated imports.
@@ -43,17 +45,22 @@ const table = [[forcePointerEvents], [!forcePointerEvents]];
4345

4446
describe.each(table)('FocusWithin responder', hasPointerEvents => {
4547
let container;
48+
let container2;
4649

4750
beforeEach(() => {
4851
initializeModules();
4952
container = document.createElement('div');
5053
document.body.appendChild(container);
54+
container2 = document.createElement('div');
55+
document.body.appendChild(container2);
5156
});
5257

5358
afterEach(() => {
5459
ReactDOM.render(null, container);
5560
document.body.removeChild(container);
61+
document.body.removeChild(container2);
5662
container = null;
63+
container2 = null;
5764
});
5865

5966
describe('disabled', () => {
@@ -366,6 +373,40 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
366373
);
367374
});
368375

376+
// @gate experimental
377+
it('is called after many elements are unmounted', () => {
378+
const buttonRef = React.createRef();
379+
const inputRef = React.createRef();
380+
381+
const Component = ({show}) => {
382+
const listener = useFocusWithin({
383+
onBeforeBlurWithin,
384+
onAfterBlurWithin,
385+
});
386+
return (
387+
<div ref={ref} DEPRECATED_flareListeners={listener}>
388+
{show && <button>Press me!</button>}
389+
{show && <button>Press me!</button>}
390+
{show && <input ref={inputRef} />}
391+
{show && <button>Press me!</button>}
392+
{!show && <button ref={buttonRef}>Press me!</button>}
393+
{show && <button>Press me!</button>}
394+
<button>Press me!</button>
395+
<button>Press me!</button>
396+
</div>
397+
);
398+
};
399+
400+
ReactDOM.render(<Component show={true} />, container);
401+
402+
inputRef.current.focus();
403+
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
404+
expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
405+
ReactDOM.render(<Component show={false} />, container);
406+
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
407+
expect(onAfterBlurWithin).toHaveBeenCalledTimes(1);
408+
});
409+
369410
// @gate experimental
370411
it('is called after a nested focused element is unmounted (with scope query)', () => {
371412
const TestScope = React.unstable_createScope();
@@ -430,12 +471,10 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
430471
);
431472
};
432473

433-
const container2 = document.createElement('div');
434-
document.body.appendChild(container2);
435-
436474
const root = ReactDOM.createRoot(container2);
437-
root.render(<Component />);
438-
Scheduler.unstable_flushAll();
475+
act(() => {
476+
root.render(<Component />);
477+
});
439478
jest.runAllTimers();
440479
expect(container2.innerHTML).toBe('<div><input></div>');
441480

@@ -447,17 +486,84 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
447486
expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
448487

449488
suspend = true;
450-
root.render(<Component />);
451-
Scheduler.unstable_flushAll();
489+
act(() => {
490+
root.render(<Component />);
491+
});
452492
jest.runAllTimers();
453493
expect(container2.innerHTML).toBe(
454494
'<div><input style="display: none;">Loading...</div>',
455495
);
456496
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
457497
expect(onAfterBlurWithin).toHaveBeenCalledTimes(1);
458498
resolve();
499+
});
500+
501+
// @gate experimental
502+
it('is called after a focused suspended element is hidden then shown', () => {
503+
const Suspense = React.Suspense;
504+
let suspend = false;
505+
let resolve;
506+
const promise = new Promise(resolvePromise => (resolve = resolvePromise));
507+
const buttonRef = React.createRef();
508+
509+
function Child() {
510+
if (suspend) {
511+
throw promise;
512+
} else {
513+
return <input ref={innerRef} />;
514+
}
515+
}
459516

460-
document.body.removeChild(container2);
517+
const Component = ({show}) => {
518+
const listener = useFocusWithin({
519+
onBeforeBlurWithin,
520+
onAfterBlurWithin,
521+
});
522+
523+
return (
524+
<div ref={ref} DEPRECATED_flareListeners={listener}>
525+
<Suspense fallback={<button ref={buttonRef}>Loading...</button>}>
526+
<Child />
527+
</Suspense>
528+
</div>
529+
);
530+
};
531+
532+
const root = ReactDOM.createRoot(container2);
533+
534+
act(() => {
535+
root.render(<Component />);
536+
});
537+
jest.runAllTimers();
538+
539+
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
540+
expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
541+
542+
suspend = true;
543+
act(() => {
544+
root.render(<Component />);
545+
});
546+
jest.runAllTimers();
547+
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
548+
expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
549+
550+
act(() => {
551+
root.render(<Component />);
552+
});
553+
jest.runAllTimers();
554+
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
555+
expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
556+
557+
buttonRef.current.focus();
558+
suspend = false;
559+
act(() => {
560+
root.render(<Component />);
561+
});
562+
jest.runAllTimers();
563+
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
564+
expect(onAfterBlurWithin).toHaveBeenCalledTimes(1);
565+
566+
resolve();
461567
});
462568
});
463569

0 commit comments

Comments
 (0)