@samitouri / QOS-React-2 / commits / fc43644eba

[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