Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revise useFocus/useFocusWithin #19310

Merged
merged 1 commit into from Jul 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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;
}