Skip to content

Commit

Permalink
Revise useFocus/useFocusWithin (#19310)
Browse files Browse the repository at this point in the history
  • Loading branch information
trueadm committed Jul 10, 2020
1 parent 17efbf7 commit 2562e75
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 116 deletions.
8 changes: 8 additions & 0 deletions packages/dom-event-testing-library/domEvents.js
Expand Up @@ -234,6 +234,10 @@ export function blur({relatedTarget} = {}) {
return new FocusEvent('blur', {relatedTarget});
}

export function focusOut({relatedTarget} = {}) {
return new FocusEvent('focusout', {relatedTarget, bubbles: true});
}

export function click(payload) {
return createMouseEvent('click', {
button: buttonType.primary,
Expand All @@ -259,6 +263,10 @@ export function focus({relatedTarget} = {}) {
return new FocusEvent('focus', {relatedTarget});
}

export function focusIn({relatedTarget} = {}) {
return new FocusEvent('focusin', {relatedTarget, bubbles: true});
}

export function scroll() {
return createEvent('scroll');
}
Expand Down
2 changes: 2 additions & 0 deletions packages/dom-event-testing-library/index.js
Expand Up @@ -22,12 +22,14 @@ const createEventTarget = node => ({
*/
blur(payload) {
node.dispatchEvent(domEvents.blur(payload));
node.dispatchEvent(domEvents.focusOut(payload));
},
click(payload) {
node.dispatchEvent(domEvents.click(payload));
},
focus(payload) {
node.dispatchEvent(domEvents.focus(payload));
node.dispatchEvent(domEvents.focusIn(payload));
node.focus();
},
keydown(payload) {
Expand Down
218 changes: 120 additions & 98 deletions packages/react-interactions/events/src/dom/create-event-handle/Focus.js
Expand Up @@ -10,7 +10,7 @@
import * as React from 'react';
import useEvent from './useEvent';

const {useEffect, useRef} = React;
const {useCallback, useEffect, useRef} = React;

type UseFocusOptions = {|
disabled?: boolean,
Expand Down Expand Up @@ -126,7 +126,7 @@ function handleGlobalFocusVisibleEvent(
}

const passiveObject = {passive: true};
const passiveCaptureObject = {capture: true, passive: false};
const passiveObjectWithPriority = {passive: true, priority: 0};

function handleFocusVisibleTargetEvent(
type: string,
Expand Down Expand Up @@ -243,8 +243,8 @@ export function useFocus(
): void {
// Setup controlled state for this useFocus hook
const stateRef = useRef({isFocused: false, isFocusVisible: false});
const focusHandle = useEvent('focus', passiveCaptureObject);
const blurHandle = useEvent('blur', passiveCaptureObject);
const focusHandle = useEvent('focusin', passiveObjectWithPriority);
const blurHandle = useEvent('focusout', passiveObjectWithPriority);
const focusVisibleHandles = useFocusVisibleInputHandles();

useEffect(() => {
Expand Down Expand Up @@ -317,7 +317,9 @@ export function useFocus(
}

export function useFocusWithin(
focusWithinTargetRef: {current: null | Node},
focusWithinTargetRef:
| {current: null | Node}
| ((focusWithinTarget: null | Node) => void),
{
disabled,
onAfterBlurWithin,
Expand All @@ -327,114 +329,134 @@ export function useFocusWithin(
onFocusWithinChange,
onFocusWithinVisibleChange,
}: UseFocusWithinOptions,
) {
): (focusWithinTarget: null | Node) => void {
// Setup controlled state for this useFocus hook
const stateRef = useRef({isFocused: false, isFocusVisible: false});
const focusHandle = useEvent('focus', passiveCaptureObject);
const blurHandle = useEvent('blur', passiveCaptureObject);
const stateRef = useRef<null | {isFocused: boolean, isFocusVisible: boolean}>(
{isFocused: false, isFocusVisible: false},
);
const focusHandle = useEvent('focusin', passiveObjectWithPriority);
const blurHandle = useEvent('focusout', passiveObjectWithPriority);
const afterBlurHandle = useEvent('afterblur', passiveObject);
const beforeBlurHandle = useEvent('beforeblur', passiveObject);
const focusVisibleHandles = useFocusVisibleInputHandles();

useEffect(() => {
const focusWithinTarget = focusWithinTargetRef.current;
const state = stateRef.current;
const useFocusWithinRef = useCallback(
(focusWithinTarget: null | Node) => {
// Handle the incoming focusTargetRef. It can be either a function ref
// or an object ref.
if (typeof focusWithinTargetRef === 'function') {
focusWithinTargetRef(focusWithinTarget);
} else {
focusWithinTargetRef.current = focusWithinTarget;
}
const state = stateRef.current;

if (focusWithinTarget !== null && state !== null) {
// Handle focus visible
setFocusVisibleListeners(
focusVisibleHandles,
focusWithinTarget,
isFocusVisible => {
if (state.isFocused && state.isFocusVisible !== isFocusVisible) {
state.isFocusVisible = isFocusVisible;
if (onFocusWithinVisibleChange) {
onFocusWithinVisibleChange(isFocusVisible);
}
}
},
);

if (focusWithinTarget !== null && state !== null) {
// Handle focus visible
setFocusVisibleListeners(
focusVisibleHandles,
focusWithinTarget,
isFocusVisible => {
if (state.isFocused && state.isFocusVisible !== isFocusVisible) {
state.isFocusVisible = isFocusVisible;
// Handle focus
focusHandle.setListener(focusWithinTarget, event => {
if (disabled) {
return;
}
if (!state.isFocused) {
state.isFocused = true;
state.isFocusVisible = isGlobalFocusVisible;
if (onFocusWithinChange) {
onFocusWithinChange(true);
}
if (state.isFocusVisible && onFocusWithinVisibleChange) {
onFocusWithinVisibleChange(true);
}
}
if (!state.isFocusVisible && isGlobalFocusVisible) {
state.isFocusVisible = isGlobalFocusVisible;
if (onFocusWithinVisibleChange) {
onFocusWithinVisibleChange(isFocusVisible);
onFocusWithinVisibleChange(true);
}
}
},
);

// Handle focus
focusHandle.setListener(focusWithinTarget, event => {
if (disabled) {
return;
}
if (!state.isFocused) {
state.isFocused = true;
state.isFocusVisible = isGlobalFocusVisible;
if (onFocusWithinChange) {
onFocusWithinChange(true);
if (onFocusWithin) {
onFocusWithin(event);
}
if (state.isFocusVisible && onFocusWithinVisibleChange) {
onFocusWithinVisibleChange(true);
});

// Handle blur
blurHandle.setListener(focusWithinTarget, event => {
if (disabled) {
return;
}
}
if (!state.isFocusVisible && isGlobalFocusVisible) {
state.isFocusVisible = isGlobalFocusVisible;
if (onFocusWithinVisibleChange) {
onFocusWithinVisibleChange(true);
const {relatedTarget} = (event.nativeEvent: any);

if (
state.isFocused &&
// $FlowFixMe: focusWithinTarget is never null
!isRelatedTargetWithin(focusWithinTarget, relatedTarget)
) {
state.isFocused = false;
if (onFocusWithinChange) {
onFocusWithinChange(false);
}
if (state.isFocusVisible && onFocusWithinVisibleChange) {
onFocusWithinVisibleChange(false);
}
if (onBlurWithin) {
onBlurWithin(event);
}
}
}
if (onFocusWithin) {
onFocusWithin(event);
}
isEmulatingMouseEvents = false;
});
});

// Handle blur
blurHandle.setListener(focusWithinTarget, event => {
if (disabled) {
return;
}
const {relatedTarget} = (event: any);

if (
state.isFocused &&
!isRelatedTargetWithin(focusWithinTarget, relatedTarget)
) {
state.isFocused = false;
if (onFocusWithinChange) {
onFocusWithinChange(false);
// Handle before blur. This is a special
// React provided event.
beforeBlurHandle.setListener(focusWithinTarget, event => {
if (disabled) {
return;
}
if (state.isFocusVisible && onFocusWithinVisibleChange) {
onFocusWithinVisibleChange(false);
if (onBeforeBlurWithin) {
onBeforeBlurWithin(event);
// Add an "afterblur" listener on document. This is a special
// React provided event.
afterBlurHandle.setListener(document, afterBlurEvent => {
if (onAfterBlurWithin) {
onAfterBlurWithin(afterBlurEvent);
}
// Clear listener on document
afterBlurHandle.setListener(document, null);
});
}
if (onBlurWithin) {
onBlurWithin(event);
}
}
isEmulatingMouseEvents = false;
});

// Handle before blur. This is a special
// React provided event.
beforeBlurHandle.setListener(focusWithinTarget, event => {
if (disabled) {
return;
}
if (onBeforeBlurWithin) {
onBeforeBlurWithin(event);
// Add an "afterblur" listener on document. This is a special
// React provided event.
afterBlurHandle.setListener(document, afterBlurEvent => {
if (onAfterBlurWithin) {
onAfterBlurWithin(afterBlurEvent);
}
// Clear listener on document
afterBlurHandle.setListener(document, null);
});
}
});
}
}, [
disabled,
onBlurWithin,
onFocusWithin,
onFocusWithinChange,
onFocusWithinVisibleChange,
]);
});
}
},
[
afterBlurHandle,
beforeBlurHandle,
blurHandle,
disabled,
focusHandle,
focusVisibleHandles,
focusWithinTargetRef,
onAfterBlurWithin,
onBeforeBlurWithin,
onBlurWithin,
onFocusWithin,
onFocusWithinChange,
onFocusWithinVisibleChange,
],
);

// Mount/Unmount logic
useFocusLifecycles(stateRef);
useFocusLifecycles();

return useFocusWithinRef;
}

0 comments on commit 2562e75

Please sign in to comment.