Skip to content

Commit 2562e75

Browse files
authored
Revise useFocus/useFocusWithin (facebook#19310)
1 parent 17efbf7 commit 2562e75

File tree

4 files changed

+148
-116
lines changed

4 files changed

+148
-116
lines changed

packages/dom-event-testing-library/domEvents.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,10 @@ export function blur({relatedTarget} = {}) {
234234
return new FocusEvent('blur', {relatedTarget});
235235
}
236236

237+
export function focusOut({relatedTarget} = {}) {
238+
return new FocusEvent('focusout', {relatedTarget, bubbles: true});
239+
}
240+
237241
export function click(payload) {
238242
return createMouseEvent('click', {
239243
button: buttonType.primary,
@@ -259,6 +263,10 @@ export function focus({relatedTarget} = {}) {
259263
return new FocusEvent('focus', {relatedTarget});
260264
}
261265

266+
export function focusIn({relatedTarget} = {}) {
267+
return new FocusEvent('focusin', {relatedTarget, bubbles: true});
268+
}
269+
262270
export function scroll() {
263271
return createEvent('scroll');
264272
}

packages/dom-event-testing-library/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@ const createEventTarget = node => ({
2222
*/
2323
blur(payload) {
2424
node.dispatchEvent(domEvents.blur(payload));
25+
node.dispatchEvent(domEvents.focusOut(payload));
2526
},
2627
click(payload) {
2728
node.dispatchEvent(domEvents.click(payload));
2829
},
2930
focus(payload) {
3031
node.dispatchEvent(domEvents.focus(payload));
32+
node.dispatchEvent(domEvents.focusIn(payload));
3133
node.focus();
3234
},
3335
keydown(payload) {

packages/react-interactions/events/src/dom/create-event-handle/Focus.js

Lines changed: 120 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import * as React from 'react';
1111
import useEvent from './useEvent';
1212

13-
const {useEffect, useRef} = React;
13+
const {useCallback, useEffect, useRef} = React;
1414

1515
type UseFocusOptions = {|
1616
disabled?: boolean,
@@ -126,7 +126,7 @@ function handleGlobalFocusVisibleEvent(
126126
}
127127

128128
const passiveObject = {passive: true};
129-
const passiveCaptureObject = {capture: true, passive: false};
129+
const passiveObjectWithPriority = {passive: true, priority: 0};
130130

131131
function handleFocusVisibleTargetEvent(
132132
type: string,
@@ -243,8 +243,8 @@ export function useFocus(
243243
): void {
244244
// Setup controlled state for this useFocus hook
245245
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);
248248
const focusVisibleHandles = useFocusVisibleInputHandles();
249249

250250
useEffect(() => {
@@ -317,7 +317,9 @@ export function useFocus(
317317
}
318318

319319
export function useFocusWithin(
320-
focusWithinTargetRef: {current: null | Node},
320+
focusWithinTargetRef:
321+
| {current: null | Node}
322+
| ((focusWithinTarget: null | Node) => void),
321323
{
322324
disabled,
323325
onAfterBlurWithin,
@@ -327,114 +329,134 @@ export function useFocusWithin(
327329
onFocusWithinChange,
328330
onFocusWithinVisibleChange,
329331
}: UseFocusWithinOptions,
330-
) {
332+
): (focusWithinTarget: null | Node) => void {
331333
// 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);
335339
const afterBlurHandle = useEvent('afterblur', passiveObject);
336340
const beforeBlurHandle = useEvent('beforeblur', passiveObject);
337341
const focusVisibleHandles = useFocusVisibleInputHandles();
338342

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+
);
342368

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;
351386
if (onFocusWithinVisibleChange) {
352-
onFocusWithinVisibleChange(isFocusVisible);
387+
onFocusWithinVisibleChange(true);
353388
}
354389
}
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);
368392
}
369-
if (state.isFocusVisible && onFocusWithinVisibleChange) {
370-
onFocusWithinVisibleChange(true);
393+
});
394+
395+
// Handle blur
396+
blurHandle.setListener(focusWithinTarget, event => {
397+
if (disabled) {
398+
return;
371399
}
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+
}
377417
}
378-
}
379-
if (onFocusWithin) {
380-
onFocusWithin(event);
381-
}
382-
isEmulatingMouseEvents = false;
383-
});
418+
});
384419

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;
399425
}
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+
});
402437
}
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+
);
437457

438458
// Mount/Unmount logic
439-
useFocusLifecycles(stateRef);
459+
useFocusLifecycles();
460+
461+
return useFocusWithinRef;
440462
}

0 commit comments

Comments
 (0)