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

[react-interactions] Refine custom active element blur logic (#17354)

Dominic Gannaway committed Nov 13, 2019 at 20:46 UTC a61886b16b5183d63b85dbd92e5808f82be93208
10 files changed +178 -63
packages/react-art/src/ReactARTHostConfig.js
+4
@@ -465,3 +465,7 @@ export function unmountFundamentalComponent(fundamentalInstance) {
465 export function getInstanceFromNode(node) {
466 throw new Error('Not yet implemented.');
467 }
468 +
469 +export function beforeRemoveInstance(instance) {
470 + // noop
471 +}
packages/react-dom/src/client/ReactDOMHostConfig.js
+49 -18
@@ -104,6 +104,12 @@ export type ChildSet = void; // Unused
104 export type TimeoutHandle = TimeoutID;
105 export type NoTimeout = -1;
106
107 +type SelectionInformation = {|
108 + blurredActiveElement: null | HTMLElement,
109 + focusedElem: null | HTMLElement,
110 + selectionRange: mixed,
111 +|};
112 +
113 import {
114 enableSuspenseServerRenderer,
115 enableFlareAPI,
@@ -127,7 +133,7 @@ const SUSPENSE_FALLBACK_START_DATA = '$!';
133 const STYLE = 'style';
134
135 let eventsEnabled: ?boolean = null;
130 -let selectionInformation: ?mixed = null;
136 +let selectionInformation: null | SelectionInformation = null;
137
138 function shouldAutoFocusHostComponent(type: string, props: Props): boolean {
139 switch (type) {
@@ -205,6 +211,13 @@ export function prepareForCommit(containerInfo: Container): void {
211
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);
219 + }
220 + }
221 selectionInformation = null;
222 ReactBrowserEventEmitterSetEnabled(eventsEnabled);
223 eventsEnabled = null;
@@ -452,30 +465,50 @@ 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.
474 + dispatchEventForResponderEventSystem(
475 + 'blur',
476 + targetInstance,
477 + ({
478 + elementDetached,
479 + target,
480 + timeStamp: Date.now(),
481 + }: any),
482 + target,
483 + RESPONDER_EVENT_SYSTEM | IS_PASSIVE,
484 + );
485 +}
486 +
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));
497 +}
498 +
499 // This is a specific event for the React Flare
500 // event system, so event responders can act
501 // accordingly to a DOM node being unmounted that
502 // previously had active document focus.
459 -function dispatchDetachedVisibleNodeEvent(
460 - child: Instance | TextInstance | SuspenseInstance,
503 +export function beforeRemoveInstance(
504 + instance: Instance | TextInstance | SuspenseInstance,
505 ): void {
506 if (
507 enableFlareAPI &&
508 selectionInformation &&
465 - child === selectionInformation.focusedElem
509 + instance === selectionInformation.focusedElem
510 ) {
467 - const targetFiber = getClosestInstanceFromNode(child);
468 - // Simlulate a blur event to the React Flare responder system.
469 - dispatchEventForResponderEventSystem(
470 - 'detachedvisiblenode',
471 - targetFiber,
472 - ({
473 - target: child,
474 - timeStamp: Date.now(),
475 - }: any),
476 - ((child: any): Document | Element),
477 - RESPONDER_EVENT_SYSTEM | IS_PASSIVE,
478 - );
511 + dispatchBeforeActiveElementBlur(((instance: any): HTMLElement));
512 }
513 }
514
@@ -483,7 +516,6 @@ export function removeChild(
516 parentInstance: Instance,
517 child: Instance | TextInstance | SuspenseInstance,
518 ): void {
486 - dispatchDetachedVisibleNodeEvent(child);
519 parentInstance.removeChild(child);
520 }
521
@@ -494,7 +526,6 @@ export function removeChildFromContainer(
526 if (container.nodeType === COMMENT_NODE) {
527 (container.parentNode: any).removeChild(child);
528 } else {
497 - dispatchDetachedVisibleNodeEvent(child);
529 container.removeChild(child);
530 }
531 }
packages/react-dom/src/client/ReactInputSelection.js
+2
@@ -100,6 +100,8 @@ export function hasSelectionCapabilities(elem) {
100 export function getSelectionInformation() {
101 const focusedElem = getActiveElementDeep();
102 return {
103 + // Used by Flare
104 + blurredActiveElement: null,
105 focusedElem: focusedElem,
106 selectionRange: hasSelectionCapabilities(focusedElem)
107 ? getSelection(focusedElem)
packages/react-interactions/events/src/dom/Focus.js
+50 -27
@@ -45,12 +45,7 @@ type FocusProps = {
45 onFocusVisibleChange: boolean => void,
46 };
47
48 -type FocusEventType =
49 - | 'focus'
50 - | 'blur'
51 - | 'focuschange'
52 - | 'focusvisiblechange'
53 - | 'detachedvisiblenode';
48 +type FocusEventType = 'focus' | 'blur' | 'focuschange' | 'focusvisiblechange';
49
50 type FocusWithinProps = {
51 disabled?: boolean,
@@ -58,7 +53,8 @@ type FocusWithinProps = {
53 onBlurWithin?: (e: FocusEvent) => void,
54 onFocusWithinChange?: boolean => void,
55 onFocusWithinVisibleChange?: boolean => void,
61 - onDetachedVisibleNode?: (e: FocusEvent) => void,
56 + onBeforeFocusedElementDetached?: (e: FocusEvent) => void,
57 + onFocusedElementDetached?: (e: FocusEvent) => void,
58 };
59
60 type FocusWithinEventType =
@@ -66,7 +62,8 @@ type FocusWithinEventType =
62 | 'focuswithinchange'
63 | 'blurwithin'
64 | 'focuswithin'
69 - | 'detachedvisiblenode';
65 + | 'focusedelementdetached'
66 + | 'beforefocusedelementdetached';
67
68 /**
69 * Shared between Focus and FocusWithin
@@ -79,14 +76,22 @@ const isMac =
76 ? /^Mac/.test(window.navigator.platform)
77 : false;
78
82 -const targetEventTypes = ['focus', 'blur', 'detachedvisiblenode'];
79 +const targetEventTypes = ['focus', 'blur'];
80
81 const hasPointerEvents =
82 typeof window !== 'undefined' && window.PointerEvent != null;
83
84 const rootEventTypes = hasPointerEvents
88 - ? ['keydown', 'keyup', 'pointermove', 'pointerdown', 'pointerup']
89 - : ['keydown', 'keyup', 'mousedown', 'touchmove', 'touchstart', 'touchend'];
85 + ? ['keydown', 'keyup', 'pointermove', 'pointerdown', 'pointerup', 'blur']
86 + : [
87 + 'keydown',
88 + 'keyup',
89 + 'mousedown',
90 + 'touchmove',
91 + 'touchstart',
92 + 'touchend',
93 + 'blur',
94 + ];
95
96 function isFunction(obj): boolean {
97 return typeof obj === 'function';
@@ -504,6 +509,23 @@ const focusWithinResponderImpl = {
509 break;
510 }
511 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 + }
529 if (
530 state.isFocused &&
531 !context.isTargetWithinResponder(relatedTarget)
@@ -514,22 +536,6 @@ const focusWithinResponderImpl = {
536 }
537 break;
538 }
517 - case 'detachedvisiblenode': {
518 - const onDetachedVisibleNode = (props.onDetachedVisibleNode: any);
519 - if (isFunction(onDetachedVisibleNode)) {
520 - const syntheticEvent = createFocusEvent(
521 - context,
522 - 'detachedvisiblenode',
523 - event.target,
524 - state.pointerType,
525 - );
526 - context.dispatchEvent(
527 - syntheticEvent,
528 - onDetachedVisibleNode,
529 - DiscreteEvent,
530 - );
531 - }
532 - }
539 }
540 },
541 onRootEvent(
@@ -538,6 +544,23 @@ const focusWithinResponderImpl = {
544 props: FocusWithinProps,
545 state: FocusState,
546 ): 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 + );
561 + }
562 + return;
563 + }
564 handleRootEvent(event, context, state, isFocusVisible => {
565 if (state.isFocused && state.isFocusVisible !== isFocusVisible) {
566 state.isFocusVisible = isFocusVisible;
packages/react-interactions/events/src/dom/__tests__/FocusWithin-test.internal.js
+58 -18
@@ -262,37 +262,77 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
262 });
263 });
264
265 - describe('onDetachedVisibleNode', () => {
266 - let onDetachedVisibleNode, ref, innerRef, innerRef2;
267 -
268 - const Component = ({show}) => {
269 - const listener = useFocusWithin({
270 - onDetachedVisibleNode,
271 - });
272 - return (
273 - <div ref={ref} listeners={listener}>
274 - {show && <input ref={innerRef} />}
275 - <div ref={innerRef2} />
276 - </div>
277 - );
278 - };
265 + describe('onBeforeFocusedElementDetached/onFocusedElementDetached', () => {
266 + let onBeforeFocusedElementDetached,
267 + onFocusedElementDetached,
268 + ref,
269 + innerRef,
270 + innerRef2;
271
272 beforeEach(() => {
281 - onDetachedVisibleNode = jest.fn();
273 + onBeforeFocusedElementDetached = jest.fn();
274 + onFocusedElementDetached = jest.fn();
275 ref = React.createRef();
276 innerRef = React.createRef();
277 innerRef2 = React.createRef();
285 - ReactDOM.render(<Component show={true} />, container);
278 });
279
280 it('is called after a focused element is unmounted', () => {
281 + const Component = ({show}) => {
282 + const listener = useFocusWithin({
283 + onBeforeFocusedElementDetached,
284 + onFocusedElementDetached,
285 + });
286 + return (
287 + <div ref={ref} listeners={listener}>
288 + {show && <input ref={innerRef} />}
289 + <div ref={innerRef2} />
290 + </div>
291 + );
292 + };
293 +
294 + ReactDOM.render(<Component show={true} />, container);
295 +
296 + const inner = innerRef.current;
297 + const target = createEventTarget(inner);
298 + target.keydown({key: 'Tab'});
299 + target.focus();
300 + expect(onBeforeFocusedElementDetached).toHaveBeenCalledTimes(0);
301 + expect(onFocusedElementDetached).toHaveBeenCalledTimes(0);
302 + ReactDOM.render(<Component show={false} />, container);
303 + expect(onBeforeFocusedElementDetached).toHaveBeenCalledTimes(1);
304 + expect(onFocusedElementDetached).toHaveBeenCalledTimes(1);
305 + });
306 +
307 + it('is called after a nested focused element is unmounted', () => {
308 + const Component = ({show}) => {
309 + const listener = useFocusWithin({
310 + onBeforeFocusedElementDetached,
311 + onFocusedElementDetached,
312 + });
313 + return (
314 + <div ref={ref} listeners={listener}>
315 + {show && (
316 + <div>
317 + <input ref={innerRef} />
318 + </div>
319 + )}
320 + <div ref={innerRef2} />
321 + </div>
322 + );
323 + };
324 +
325 + ReactDOM.render(<Component show={true} />, container);
326 +
327 const inner = innerRef.current;
328 const target = createEventTarget(inner);
329 target.keydown({key: 'Tab'});
330 target.focus();
293 - expect(onDetachedVisibleNode).toHaveBeenCalledTimes(0);
331 + expect(onBeforeFocusedElementDetached).toHaveBeenCalledTimes(0);
332 + expect(onFocusedElementDetached).toHaveBeenCalledTimes(0);
333 ReactDOM.render(<Component show={false} />, container);
295 - expect(onDetachedVisibleNode).toHaveBeenCalledTimes(1);
334 + expect(onBeforeFocusedElementDetached).toHaveBeenCalledTimes(1);
335 + expect(onFocusedElementDetached).toHaveBeenCalledTimes(1);
336 });
337 });
338
packages/react-native-renderer/src/ReactFabricHostConfig.js
+4
@@ -495,3 +495,7 @@ export function cloneFundamentalInstance(fundamentalInstance) {
495 export function getInstanceFromNode(node) {
496 throw new Error('Not yet implemented.');
497 }
498 +
499 +export function beforeRemoveInstance(instance) {
500 + // noop
501 +}
packages/react-native-renderer/src/ReactNativeHostConfig.js
+4
@@ -534,3 +534,7 @@ export function unmountFundamentalComponent(fundamentalInstance) {
534 export function getInstanceFromNode(node) {
535 throw new Error('Not yet implemented.');
536 }
537 +
538 +export function beforeRemoveInstance(instance) {
539 + // noop
540 +}
packages/react-reconciler/src/ReactFiberCommitWork.js
+2
@@ -105,6 +105,7 @@ import {
105 updateFundamentalComponent,
106 commitHydratedContainer,
107 commitHydratedSuspenseInstance,
108 + beforeRemoveInstance,
109 } from './ReactFiberHostConfig';
110 import {
111 captureCommitPhaseError,
@@ -808,6 +809,7 @@ function commitUnmount(
809 dependencies.responders = null;
810 }
811 }
812 + beforeRemoveInstance(current.stateNode);
813 }
814 safelyDetachRef(current);
815 return;
packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
+1
@@ -70,6 +70,7 @@ export const mountFundamentalComponent =
70 export const shouldUpdateFundamentalComponent =
71 $$$hostConfig.shouldUpdateFundamentalComponent;
72 export const getInstanceFromNode = $$$hostConfig.getInstanceFromNode;
73 +export const beforeRemoveInstance = $$$hostConfig.beforeRemoveInstance;
74
75 // -------------------
76 // Mutation
packages/react-test-renderer/src/ReactTestHostConfig.js
+4
@@ -367,3 +367,7 @@ export function getInstanceFromNode(mockNode: Object) {
367 }
368 return null;
369 }
370 +
371 +export function beforeRemoveInstance(instance) {
372 + // noop
373 +}