[react-interactions] Follow up active element blur logic (#17364)
Dominic Gannaway committed
Nov 14, 2019 at 11:55 UTC
fc43644eba08256464edb125adce2027cdbdca30
4 files changed
+84
-81
packages/react-dom/src/client/ReactDOMHostConfig.js
+24
-24
@@ -105,7 +105,7 @@ export type TimeoutHandle = TimeoutID;
105
export type NoTimeout = -1;
106
107
type SelectionInformation = {|
108
- blurredActiveElement: null | HTMLElement,
108
+ activeElementDetached: null | HTMLElement,
109
focusedElem: null | HTMLElement,
110
selectionRange: mixed,
111
|};
@@ -212,10 +212,10 @@ export function prepareForCommit(containerInfo: Container): void {
212
export function resetAfterCommit(containerInfo: Container): void {
213
restoreSelection(selectionInformation);
214
if (enableFlareAPI) {
215
- const blurredActiveElement = (selectionInformation: any)
216
- .blurredActiveElement;
217
- if (blurredActiveElement !== null) {
218
- dispatchActiveElementBlur(blurredActiveElement);
215
+ const activeElementDetached = (selectionInformation: any)
216
+ .activeElementDetached;
217
+ if (activeElementDetached !== null) {
218
+ dispatchDetachedBlur(activeElementDetached);
219
}
220
}
221
selectionInformation = null;
@@ -465,17 +465,14 @@ export function insertInContainerBefore(
465
}
466
}
467
468
-function dispatchFlareDetachedBlurEvent(
469
- elementDetached: boolean,
470
- targetInstance: null | Object,
471
- target: Element | Document,
472
-): void {
473
- // Simlulate the custom event to the React Flare responder system.
468
+function dispatchBeforeDetachedBlur(target: HTMLElement): void {
469
+ const targetInstance = getClosestInstanceFromNode(target);
470
+ ((selectionInformation: any): SelectionInformation).activeElementDetached = target;
471
+
472
dispatchEventForResponderEventSystem(
475
- 'blur',
473
+ 'beforeblur',
474
targetInstance,
475
({
478
- elementDetached,
476
target,
477
timeStamp: Date.now(),
478
}: any),
@@ -484,16 +481,19 @@ function dispatchFlareDetachedBlurEvent(
481
);
482
}
483
487
-function dispatchBeforeActiveElementBlur(element: HTMLElement): void {
488
- const targtInstance = getClosestInstanceFromNode(element);
489
- ((selectionInformation: any): SelectionInformation).blurredActiveElement = element;
490
- dispatchFlareDetachedBlurEvent(false, targtInstance, element);
491
-}
492
-
493
-function dispatchActiveElementBlur(
494
- node: Instance | TextInstance | SuspenseInstance,
495
-): void {
496
- dispatchFlareDetachedBlurEvent(true, null, ((node: any): HTMLElement));
484
+function dispatchDetachedBlur(target: HTMLElement): void {
485
+ const targetInstance = getClosestInstanceFromNode(target);
486
+ dispatchEventForResponderEventSystem(
487
+ 'blur',
488
+ targetInstance,
489
+ ({
490
+ isTargetAttached: false,
491
+ target,
492
+ timeStamp: Date.now(),
493
+ }: any),
494
+ target,
495
+ RESPONDER_EVENT_SYSTEM | IS_PASSIVE,
496
+ );
497
}
498
499
// This is a specific event for the React Flare
@@ -508,7 +508,7 @@ export function beforeRemoveInstance(
508
selectionInformation &&
509
instance === selectionInformation.focusedElem
510
) {
511
- dispatchBeforeActiveElementBlur(((instance: any): HTMLElement));
511
+ dispatchBeforeDetachedBlur(((instance: any): HTMLElement));
512
}
513
}
514
packages/react-dom/src/client/ReactInputSelection.js
+1
-1
@@ -101,7 +101,7 @@ export function getSelectionInformation() {
101
const focusedElem = getActiveElementDeep();
102
return {
103
// Used by Flare
104
- blurredActiveElement: null,
104
+ activeElementDetached: null,
105
focusedElem: focusedElem,
106
selectionRange: hasSelectionCapabilities(focusedElem)
107
? getSelection(focusedElem)
packages/react-interactions/events/src/dom/Focus.js
+37
-36
@@ -22,6 +22,7 @@ import {DiscreteEvent} from 'shared/ReactTypes';
22
*/
23
24
type FocusEvent = {|
25
+ isTargetAttached: boolean,
26
target: Element | Document,
27
type: FocusEventType | FocusWithinEventType,
28
pointerType: PointerType,
@@ -30,6 +31,7 @@ type FocusEvent = {|
31
|};
32
33
type FocusState = {
34
+ detachedTarget: null | Element | Document,
35
focusTarget: null | Element | Document,
36
isFocused: boolean,
37
isFocusVisible: boolean,
@@ -50,11 +52,10 @@ type FocusEventType = 'focus' | 'blur' | 'focuschange' | 'focusvisiblechange';
52
type FocusWithinProps = {
53
disabled?: boolean,
54
onFocusWithin?: (e: FocusEvent) => void,
55
+ onBeforeBlurWithin?: (e: FocusEvent) => void,
56
onBlurWithin?: (e: FocusEvent) => void,
57
onFocusWithinChange?: boolean => void,
58
onFocusWithinVisibleChange?: boolean => void,
56
- onBeforeFocusedElementDetached?: (e: FocusEvent) => void,
57
- onFocusedElementDetached?: (e: FocusEvent) => void,
59
};
60
61
type FocusWithinEventType =
@@ -62,8 +63,7 @@ type FocusWithinEventType =
63
| 'focuswithinchange'
64
| 'blurwithin'
65
| 'focuswithin'
65
- | 'focusedelementdetached'
66
- | 'beforefocusedelementdetached';
66
+ | 'beforeblurwithin';
67
68
/**
69
* Shared between Focus and FocusWithin
@@ -76,7 +76,7 @@ const isMac =
76
? /^Mac/.test(window.navigator.platform)
77
: false;
78
79
-const targetEventTypes = ['focus', 'blur'];
79
+const targetEventTypes = ['focus', 'blur', 'beforeblur'];
80
81
const hasPointerEvents =
82
typeof window !== 'undefined' && window.PointerEvent != null;
@@ -102,8 +102,10 @@ function createFocusEvent(
102
type: FocusEventType | FocusWithinEventType,
103
target: Element | Document,
104
pointerType: PointerType,
105
+ isTargetAttached: boolean,
106
): FocusEvent {
107
return {
108
+ isTargetAttached,
109
target,
110
type,
111
pointerType,
@@ -218,6 +220,7 @@ function dispatchFocusEvents(
220
'focus',
221
target,
222
pointerType,
223
+ true,
224
);
225
context.dispatchEvent(syntheticEvent, onFocus, DiscreteEvent);
226
}
@@ -241,6 +244,7 @@ function dispatchBlurEvents(
244
'blur',
245
target,
246
pointerType,
247
+ true,
248
);
249
context.dispatchEvent(syntheticEvent, onBlur, DiscreteEvent);
250
}
@@ -265,6 +269,7 @@ function dispatchFocusWithinEvents(
269
'focuswithin',
270
target,
271
pointerType,
272
+ true,
273
);
274
context.dispatchEvent(syntheticEvent, onFocusWithin, DiscreteEvent);
275
}
@@ -279,12 +284,14 @@ function dispatchBlurWithinEvents(
284
const pointerType = state.pointerType;
285
const target = ((state.focusTarget: any): Element | Document) || event.target;
286
const onBlurWithin = (props.onBlurWithin: any);
287
+ const isTargetAttached = state.detachedTarget === null;
288
if (isFunction(onBlurWithin)) {
289
const syntheticEvent = createFocusEvent(
290
context,
291
'blurwithin',
292
target,
293
pointerType,
294
+ isTargetAttached,
295
);
296
context.dispatchEvent(syntheticEvent, onBlurWithin, DiscreteEvent);
297
}
@@ -328,6 +335,7 @@ const focusResponderImpl = {
335
rootEventTypes,
336
getInitialState(): FocusState {
337
return {
338
+ detachedTarget: null,
339
focusTarget: null,
340
isEmulatingMouseEvents: false,
341
isFocused: false,
@@ -466,6 +474,7 @@ const focusWithinResponderImpl = {
474
rootEventTypes,
475
getInitialState(): FocusState {
476
return {
477
+ detachedTarget: null,
478
focusTarget: null,
479
isEmulatingMouseEvents: false,
480
isFocused: false,
@@ -509,23 +518,6 @@ const focusWithinResponderImpl = {
518
break;
519
}
520
case 'blur': {
512
- if ((nativeEvent: any).elementDetached === false) {
513
- const onBeforeFocusedElementDetached = (props.onBeforeFocusedElementDetached: any);
514
- if (isFunction(onBeforeFocusedElementDetached)) {
515
- const syntheticEvent = createFocusEvent(
516
- context,
517
- 'beforefocusedelementdetached',
518
- event.target,
519
- state.pointerType,
520
- );
521
- context.dispatchEvent(
522
- syntheticEvent,
523
- onBeforeFocusedElementDetached,
524
- DiscreteEvent,
525
- );
526
- }
527
- return;
528
- }
521
if (
522
state.isFocused &&
523
!context.isTargetWithinResponder(relatedTarget)
@@ -536,6 +528,24 @@ const focusWithinResponderImpl = {
528
}
529
break;
530
}
531
+ case 'beforeblur': {
532
+ const onBeforeBlurWithin = (props.onBeforeBlurWithin: any);
533
+ if (isFunction(onBeforeBlurWithin)) {
534
+ const syntheticEvent = createFocusEvent(
535
+ context,
536
+ 'beforeblurwithin',
537
+ event.target,
538
+ state.pointerType,
539
+ true,
540
+ );
541
+ state.detachedTarget = event.target;
542
+ context.dispatchEvent(
543
+ syntheticEvent,
544
+ onBeforeBlurWithin,
545
+ DiscreteEvent,
546
+ );
547
+ }
548
+ }
549
}
550
},
551
onRootEvent(
@@ -544,20 +554,11 @@ const focusWithinResponderImpl = {
554
props: FocusWithinProps,
555
state: FocusState,
556
): void {
547
- if ((event.nativeEvent: any).elementDetached === true) {
548
- const onFocusedElementDetached = (props.onFocusedElementDetached: any);
549
- if (isFunction(onFocusedElementDetached)) {
550
- const syntheticEvent = createFocusEvent(
551
- context,
552
- 'focusedelementdetached',
553
- event.target,
554
- state.pointerType,
555
- );
556
- context.dispatchEvent(
557
- syntheticEvent,
558
- onFocusedElementDetached,
559
- DiscreteEvent,
560
- );
557
+ if (event.type === 'blur') {
558
+ const detachedTarget = state.detachedTarget;
559
+ if (detachedTarget !== null && detachedTarget === event.target) {
560
+ dispatchBlurWithinEvents(context, event, props, state);
561
+ state.detachedTarget = null;
562
}
563
return;
564
}
packages/react-interactions/events/src/dom/__tests__/FocusWithin-test.internal.js
+22
-20
@@ -262,16 +262,12 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
262
});
263
});
264
265
- describe('onBeforeFocusedElementDetached/onFocusedElementDetached', () => {
266
- let onBeforeFocusedElementDetached,
267
- onFocusedElementDetached,
268
- ref,
269
- innerRef,
270
- innerRef2;
265
+ describe('onBeforeBlurWithin', () => {
266
+ let onBeforeBlurWithin, onBlurWithin, ref, innerRef, innerRef2;
267
268
beforeEach(() => {
273
- onBeforeFocusedElementDetached = jest.fn();
274
- onFocusedElementDetached = jest.fn();
269
+ onBeforeBlurWithin = jest.fn();
270
+ onBlurWithin = jest.fn();
271
ref = React.createRef();
272
innerRef = React.createRef();
273
innerRef2 = React.createRef();
@@ -280,8 +276,8 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
276
it('is called after a focused element is unmounted', () => {
277
const Component = ({show}) => {
278
const listener = useFocusWithin({
283
- onBeforeFocusedElementDetached,
284
- onFocusedElementDetached,
279
+ onBeforeBlurWithin,
280
+ onBlurWithin,
281
});
282
return (
283
<div ref={ref} listeners={listener}>
@@ -297,18 +293,21 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
293
const target = createEventTarget(inner);
294
target.keydown({key: 'Tab'});
295
target.focus();
300
- expect(onBeforeFocusedElementDetached).toHaveBeenCalledTimes(0);
301
- expect(onFocusedElementDetached).toHaveBeenCalledTimes(0);
296
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
297
+ expect(onBlurWithin).toHaveBeenCalledTimes(0);
298
ReactDOM.render(<Component show={false} />, container);
303
- expect(onBeforeFocusedElementDetached).toHaveBeenCalledTimes(1);
304
- expect(onFocusedElementDetached).toHaveBeenCalledTimes(1);
299
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
300
+ expect(onBlurWithin).toHaveBeenCalledTimes(1);
301
+ expect(onBlurWithin).toHaveBeenCalledWith(
302
+ expect.objectContaining({isTargetAttached: false}),
303
+ );
304
});
305
306
it('is called after a nested focused element is unmounted', () => {
307
const Component = ({show}) => {
308
const listener = useFocusWithin({
310
- onBeforeFocusedElementDetached,
311
- onFocusedElementDetached,
309
+ onBeforeBlurWithin,
310
+ onBlurWithin,
311
});
312
return (
313
<div ref={ref} listeners={listener}>
@@ -328,11 +327,14 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
327
const target = createEventTarget(inner);
328
target.keydown({key: 'Tab'});
329
target.focus();
331
- expect(onBeforeFocusedElementDetached).toHaveBeenCalledTimes(0);
332
- expect(onFocusedElementDetached).toHaveBeenCalledTimes(0);
330
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
331
+ expect(onBlurWithin).toHaveBeenCalledTimes(0);
332
ReactDOM.render(<Component show={false} />, container);
334
- expect(onBeforeFocusedElementDetached).toHaveBeenCalledTimes(1);
335
- expect(onFocusedElementDetached).toHaveBeenCalledTimes(1);
333
+ expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
334
+ expect(onBlurWithin).toHaveBeenCalledTimes(1);
335
+ expect(onBlurWithin).toHaveBeenCalledWith(
336
+ expect.objectContaining({isTargetAttached: false}),
337
+ );
338
});
339
});
340