@samitouri / QOS-React / commits / 2562e758ea

Revise useFocus/useFocusWithin (#19310)

Dominic Gannaway committed Jul 10, 2020 at 17:23 UTC 2562e758eaa023f7fbf849bc9eb295b1966b0154
4 files changed +148 -116
packages/dom-event-testing-library/domEvents.js
+8
@@ -234,6 +234,10 @@ export function blur({relatedTarget} = {}) {
234 return new FocusEvent('blur', {relatedTarget});
235 }
236
237 +export function focusOut({relatedTarget} = {}) {
238 + return new FocusEvent('focusout', {relatedTarget, bubbles: true});
239 +}
240 +
241 export function click(payload) {
242 return createMouseEvent('click', {
243 button: buttonType.primary,
@@ -259,6 +263,10 @@ export function focus({relatedTarget} = {}) {
263 return new FocusEvent('focus', {relatedTarget});
264 }
265
266 +export function focusIn({relatedTarget} = {}) {
267 + return new FocusEvent('focusin', {relatedTarget, bubbles: true});
268 +}
269 +
270 export function scroll() {
271 return createEvent('scroll');
272 }
packages/dom-event-testing-library/index.js
+2
@@ -22,12 +22,14 @@ const createEventTarget = node => ({
22 */
23 blur(payload) {
24 node.dispatchEvent(domEvents.blur(payload));
25 + node.dispatchEvent(domEvents.focusOut(payload));
26 },
27 click(payload) {
28 node.dispatchEvent(domEvents.click(payload));
29 },
30 focus(payload) {
31 node.dispatchEvent(domEvents.focus(payload));
32 + node.dispatchEvent(domEvents.focusIn(payload));
33 node.focus();
34 },
35 keydown(payload) {
packages/react-interactions/events/src/dom/create-event-handle/Focus.js
+120 -98
@@ -10,7 +10,7 @@
10 import * as React from 'react';
11 import useEvent from './useEvent';
12
13 -const {useEffect, useRef} = React;
13 +const {useCallback, useEffect, useRef} = React;
14
15 type UseFocusOptions = {|
16 disabled?: boolean,
@@ -126,7 +126,7 @@ function handleGlobalFocusVisibleEvent(
126 }
127
128 const passiveObject = {passive: true};
129 -const passiveCaptureObject = {capture: true, passive: false};
129 +const passiveObjectWithPriority = {passive: true, priority: 0};
130
131 function handleFocusVisibleTargetEvent(
132 type: string,
@@ -243,8 +243,8 @@ export function useFocus(
243 ): void {
244 // Setup controlled state for this useFocus hook
245 const stateRef = useRef({isFocused: false, isFocusVisible: false});
246 - const focusHandle = useEvent('focus', passiveCaptureObject);
247 - const blurHandle = useEvent('blur', passiveCaptureObject);
246 + const focusHandle = useEvent('focusin', passiveObjectWithPriority);
247 + const blurHandle = useEvent('focusout', passiveObjectWithPriority);
248 const focusVisibleHandles = useFocusVisibleInputHandles();
249
250 useEffect(() => {
@@ -317,7 +317,9 @@ export function useFocus(
317 }
318
319 export function useFocusWithin(
320 - focusWithinTargetRef: {current: null | Node},
320 + focusWithinTargetRef:
321 + | {current: null | Node}
322 + | ((focusWithinTarget: null | Node) => void),
323 {
324 disabled,
325 onAfterBlurWithin,
@@ -327,114 +329,134 @@ export function useFocusWithin(
329 onFocusWithinChange,
330 onFocusWithinVisibleChange,
331 }: UseFocusWithinOptions,
330 -) {
332 +): (focusWithinTarget: null | Node) => void {
333 // Setup controlled state for this useFocus hook
332 - const stateRef = useRef({isFocused: false, isFocusVisible: false});
333 - const focusHandle = useEvent('focus', passiveCaptureObject);
334 - const blurHandle = useEvent('blur', passiveCaptureObject);
334 + const stateRef = useRef<null | {isFocused: boolean, isFocusVisible: boolean}>(
335 + {isFocused: false, isFocusVisible: false},
336 + );
337 + const focusHandle = useEvent('focusin', passiveObjectWithPriority);
338 + const blurHandle = useEvent('focusout', passiveObjectWithPriority);
339 const afterBlurHandle = useEvent('afterblur', passiveObject);
340 const beforeBlurHandle = useEvent('beforeblur', passiveObject);
341 const focusVisibleHandles = useFocusVisibleInputHandles();
342
339 - useEffect(() => {
340 - const focusWithinTarget = focusWithinTargetRef.current;
341 - const state = stateRef.current;
343 + const useFocusWithinRef = useCallback(
344 + (focusWithinTarget: null | Node) => {
345 + // Handle the incoming focusTargetRef. It can be either a function ref
346 + // or an object ref.
347 + if (typeof focusWithinTargetRef === 'function') {
348 + focusWithinTargetRef(focusWithinTarget);
349 + } else {
350 + focusWithinTargetRef.current = focusWithinTarget;
351 + }
352 + const state = stateRef.current;
353 +
354 + if (focusWithinTarget !== null && state !== null) {
355 + // Handle focus visible
356 + setFocusVisibleListeners(
357 + focusVisibleHandles,
358 + focusWithinTarget,
359 + isFocusVisible => {
360 + if (state.isFocused && state.isFocusVisible !== isFocusVisible) {
361 + state.isFocusVisible = isFocusVisible;
362 + if (onFocusWithinVisibleChange) {
363 + onFocusWithinVisibleChange(isFocusVisible);
364 + }
365 + }
366 + },
367 + );
368
343 - if (focusWithinTarget !== null && state !== null) {
344 - // Handle focus visible
345 - setFocusVisibleListeners(
346 - focusVisibleHandles,
347 - focusWithinTarget,
348 - isFocusVisible => {
349 - if (state.isFocused && state.isFocusVisible !== isFocusVisible) {
350 - state.isFocusVisible = isFocusVisible;
369 + // Handle focus
370 + focusHandle.setListener(focusWithinTarget, event => {
371 + if (disabled) {
372 + return;
373 + }
374 + if (!state.isFocused) {
375 + state.isFocused = true;
376 + state.isFocusVisible = isGlobalFocusVisible;
377 + if (onFocusWithinChange) {
378 + onFocusWithinChange(true);
379 + }
380 + if (state.isFocusVisible && onFocusWithinVisibleChange) {
381 + onFocusWithinVisibleChange(true);
382 + }
383 + }
384 + if (!state.isFocusVisible && isGlobalFocusVisible) {
385 + state.isFocusVisible = isGlobalFocusVisible;
386 if (onFocusWithinVisibleChange) {
352 - onFocusWithinVisibleChange(isFocusVisible);
387 + onFocusWithinVisibleChange(true);
388 }
389 }
355 - },
356 - );
357 -
358 - // Handle focus
359 - focusHandle.setListener(focusWithinTarget, event => {
360 - if (disabled) {
361 - return;
362 - }
363 - if (!state.isFocused) {
364 - state.isFocused = true;
365 - state.isFocusVisible = isGlobalFocusVisible;
366 - if (onFocusWithinChange) {
367 - onFocusWithinChange(true);
390 + if (onFocusWithin) {
391 + onFocusWithin(event);
392 }
369 - if (state.isFocusVisible && onFocusWithinVisibleChange) {
370 - onFocusWithinVisibleChange(true);
393 + });
394 +
395 + // Handle blur
396 + blurHandle.setListener(focusWithinTarget, event => {
397 + if (disabled) {
398 + return;
399 }
372 - }
373 - if (!state.isFocusVisible && isGlobalFocusVisible) {
374 - state.isFocusVisible = isGlobalFocusVisible;
375 - if (onFocusWithinVisibleChange) {
376 - onFocusWithinVisibleChange(true);
400 + const {relatedTarget} = (event.nativeEvent: any);
401 +
402 + if (
403 + state.isFocused &&
404 + // $FlowFixMe: focusWithinTarget is never null
405 + !isRelatedTargetWithin(focusWithinTarget, relatedTarget)
406 + ) {
407 + state.isFocused = false;
408 + if (onFocusWithinChange) {
409 + onFocusWithinChange(false);
410 + }
411 + if (state.isFocusVisible && onFocusWithinVisibleChange) {
412 + onFocusWithinVisibleChange(false);
413 + }
414 + if (onBlurWithin) {
415 + onBlurWithin(event);
416 + }
417 }
378 - }
379 - if (onFocusWithin) {
380 - onFocusWithin(event);
381 - }
382 - isEmulatingMouseEvents = false;
383 - });
418 + });
419
385 - // Handle blur
386 - blurHandle.setListener(focusWithinTarget, event => {
387 - if (disabled) {
388 - return;
389 - }
390 - const {relatedTarget} = (event: any);
391 -
392 - if (
393 - state.isFocused &&
394 - !isRelatedTargetWithin(focusWithinTarget, relatedTarget)
395 - ) {
396 - state.isFocused = false;
397 - if (onFocusWithinChange) {
398 - onFocusWithinChange(false);
420 + // Handle before blur. This is a special
421 + // React provided event.
422 + beforeBlurHandle.setListener(focusWithinTarget, event => {
423 + if (disabled) {
424 + return;
425 }
400 - if (state.isFocusVisible && onFocusWithinVisibleChange) {
401 - onFocusWithinVisibleChange(false);
426 + if (onBeforeBlurWithin) {
427 + onBeforeBlurWithin(event);
428 + // Add an "afterblur" listener on document. This is a special
429 + // React provided event.
430 + afterBlurHandle.setListener(document, afterBlurEvent => {
431 + if (onAfterBlurWithin) {
432 + onAfterBlurWithin(afterBlurEvent);
433 + }
434 + // Clear listener on document
435 + afterBlurHandle.setListener(document, null);
436 + });
437 }
403 - if (onBlurWithin) {
404 - onBlurWithin(event);
405 - }
406 - }
407 - isEmulatingMouseEvents = false;
408 - });
409 -
410 - // Handle before blur. This is a special
411 - // React provided event.
412 - beforeBlurHandle.setListener(focusWithinTarget, event => {
413 - if (disabled) {
414 - return;
415 - }
416 - if (onBeforeBlurWithin) {
417 - onBeforeBlurWithin(event);
418 - // Add an "afterblur" listener on document. This is a special
419 - // React provided event.
420 - afterBlurHandle.setListener(document, afterBlurEvent => {
421 - if (onAfterBlurWithin) {
422 - onAfterBlurWithin(afterBlurEvent);
423 - }
424 - // Clear listener on document
425 - afterBlurHandle.setListener(document, null);
426 - });
427 - }
428 - });
429 - }
430 - }, [
431 - disabled,
432 - onBlurWithin,
433 - onFocusWithin,
434 - onFocusWithinChange,
435 - onFocusWithinVisibleChange,
436 - ]);
438 + });
439 + }
440 + },
441 + [
442 + afterBlurHandle,
443 + beforeBlurHandle,
444 + blurHandle,
445 + disabled,
446 + focusHandle,
447 + focusVisibleHandles,
448 + focusWithinTargetRef,
449 + onAfterBlurWithin,
450 + onBeforeBlurWithin,
451 + onBlurWithin,
452 + onFocusWithin,
453 + onFocusWithinChange,
454 + onFocusWithinVisibleChange,
455 + ],
456 + );
457
458 // Mount/Unmount logic
439 - useFocusLifecycles(stateRef);
459 + useFocusLifecycles();
460 +
461 + return useFocusWithinRef;
462 }
packages/react-interactions/events/src/dom/create-event-handle/__tests__/useFocusWithin-test.internal.js
+18 -18
@@ -69,12 +69,12 @@ describe.each(table)(`useFocus`, hasPointerEvents => {
69 onFocusWithinVisibleChange = jest.fn();
70 ref = React.createRef();
71 const Component = () => {
72 - useFocusWithin(ref, {
72 + const focusWithinRef = useFocusWithin(ref, {
73 disabled: true,
74 onFocusWithinChange,
75 onFocusWithinVisibleChange,
76 });
77 - return <div ref={ref} />;
77 + return <div ref={focusWithinRef} />;
78 };
79 ReactDOM.render(<Component />, container);
80 Scheduler.unstable_flushAll();
@@ -95,11 +95,11 @@ describe.each(table)(`useFocus`, hasPointerEvents => {
95 let onFocusWithinChange, ref, innerRef, innerRef2;
96
97 const Component = ({show}) => {
98 - useFocusWithin(ref, {
98 + const focusWithinRef = useFocusWithin(ref, {
99 onFocusWithinChange,
100 });
101 return (
102 - <div ref={ref}>
102 + <div ref={focusWithinRef}>
103 {show && <input ref={innerRef} />}
104 <div ref={innerRef2} />
105 </div>
@@ -171,11 +171,11 @@ describe.each(table)(`useFocus`, hasPointerEvents => {
171 let onFocusWithinVisibleChange, ref, innerRef, innerRef2;
172
173 const Component = ({show}) => {
174 - useFocusWithin(ref, {
174 + const focusWithinRef = useFocusWithin(ref, {
175 onFocusWithinVisibleChange,
176 });
177 return (
178 - <div ref={ref}>
178 + <div ref={focusWithinRef}>
179 {show && <input ref={innerRef} />}
180 <div ref={innerRef2} />
181 </div>
@@ -313,12 +313,12 @@ describe.each(table)(`useFocus`, hasPointerEvents => {
313 // @gate experimental
314 it('is called after a focused element is unmounted', () => {
315 const Component = ({show}) => {
316 - useFocusWithin(ref, {
316 + const focusWithinRef = useFocusWithin(ref, {
317 onBeforeBlurWithin,
318 onAfterBlurWithin,
319 });
320 return (
321 - <div ref={ref}>
321 + <div ref={focusWithinRef}>
322 {show && <input ref={innerRef} />}
323 <div ref={innerRef2} />
324 </div>
@@ -345,12 +345,12 @@ describe.each(table)(`useFocus`, hasPointerEvents => {
345 // @gate experimental
346 it('is called after a nested focused element is unmounted', () => {
347 const Component = ({show}) => {
348 - useFocusWithin(ref, {
348 + const focusWithinRef = useFocusWithin(ref, {
349 onBeforeBlurWithin,
350 onAfterBlurWithin,
351 });
352 return (
353 - <div ref={ref}>
353 + <div ref={focusWithinRef}>
354 {show && (
355 <div>
356 <input ref={innerRef} />
@@ -384,12 +384,12 @@ describe.each(table)(`useFocus`, hasPointerEvents => {
384 const inputRef = React.createRef();
385
386 const Component = ({show}) => {
387 - useFocusWithin(ref, {
387 + const focusWithinRef = useFocusWithin(ref, {
388 onBeforeBlurWithin,
389 onAfterBlurWithin,
390 });
391 return (
392 - <div ref={ref}>
392 + <div ref={focusWithinRef}>
393 {show && <button>Press me!</button>}
394 {show && <button>Press me!</button>}
395 {show && <input ref={inputRef} />}
@@ -422,7 +422,7 @@ describe.each(table)(`useFocus`, hasPointerEvents => {
422
423 const Component = ({show}) => {
424 const scopeRef = React.useRef(null);
425 - useFocusWithin(scopeRef, {
425 + const focusWithinRef = useFocusWithin(scopeRef, {
426 onBeforeBlurWithin(event) {
427 const scope = scopeRef.current;
428 targetNode = innerRef.current;
@@ -431,7 +431,7 @@ describe.each(table)(`useFocus`, hasPointerEvents => {
431 });
432
433 return (
434 - <TestScope ref={scopeRef}>
434 + <TestScope ref={focusWithinRef}>
435 {show && <input ref={innerRef} />}
436 </TestScope>
437 );
@@ -465,13 +465,13 @@ describe.each(table)(`useFocus`, hasPointerEvents => {
465 }
466
467 const Component = ({show}) => {
468 - useFocusWithin(ref, {
468 + const focusWithinRef = useFocusWithin(ref, {
469 onBeforeBlurWithin,
470 onAfterBlurWithin,
471 });
472
473 return (
474 - <div ref={ref}>
474 + <div ref={focusWithinRef}>
475 <Suspense fallback="Loading...">
476 <Child />
477 </Suspense>
@@ -524,13 +524,13 @@ describe.each(table)(`useFocus`, hasPointerEvents => {
524 }
525
526 const Component = ({show}) => {
527 - useFocusWithin(ref, {
527 + const focusWithinRef = useFocusWithin(ref, {
528 onBeforeBlurWithin,
529 onAfterBlurWithin,
530 });
531
532 return (
533 - <div ref={ref}>
533 + <div ref={focusWithinRef}>
534 <Suspense fallback={<button ref={buttonRef}>Loading...</button>}>
535 <Child />
536 </Suspense>