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>