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

Unify Flare FocusWithin responder with useFocusWithin #18636

Merged
merged 2 commits into from
Apr 16, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 2 additions & 3 deletions packages/react-dom/src/client/ReactDOMHostConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -555,11 +555,10 @@ function dispatchBeforeDetachedBlur(target: HTMLElement): void {
function dispatchAfterDetachedBlur(target: HTMLElement): void {
if (enableDeprecatedFlareAPI) {
DEPRECATED_dispatchEventForResponderEventSystem(
'blur',
'afterblur',
null,
({
isTargetAttached: false,
target,
relatedTarget: target,
timeStamp: Date.now(),
}: any),
target,
Expand Down
50 changes: 36 additions & 14 deletions packages/react-interactions/events/src/dom/DeprecatedFocus.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {DiscreteEvent} from 'shared/ReactTypes';
*/

type FocusEvent = {|
isTargetAttached: boolean,
relatedTarget: null | Element | Document,
target: Element | Document,
type: FocusEventType | FocusWithinEventType,
pointerType: PointerType,
Expand Down Expand Up @@ -53,6 +53,7 @@ type FocusEventType = 'focus' | 'blur' | 'focuschange' | 'focusvisiblechange';
type FocusWithinProps = {
disabled?: boolean,
onFocusWithin?: (e: FocusEvent) => void,
onAfterBlurWithin?: (e: FocusEvent) => void,
onBeforeBlurWithin?: (e: FocusEvent) => void,
onBlurWithin?: (e: FocusEvent) => void,
onFocusWithinChange?: boolean => void,
Expand All @@ -65,7 +66,8 @@ type FocusWithinEventType =
| 'focuswithinchange'
| 'blurwithin'
| 'focuswithin'
| 'beforeblurwithin';
| 'beforeblurwithin'
| 'afterblurwithin';

/**
* Shared between Focus and FocusWithin
Expand Down Expand Up @@ -116,8 +118,7 @@ const focusVisibleEvents = hasPointerEvents

const targetEventTypes = ['focus', 'blur', 'beforeblur', ...focusVisibleEvents];

// Used only for the blur "detachedTarget" logic
const rootEventTypes = ['blur'];
const rootEventTypes = ['afterblur'];

function addWindowEventListener(types, callback, options) {
types.forEach(type => {
Expand Down Expand Up @@ -192,10 +193,10 @@ function createFocusEvent(
type: FocusEventType | FocusWithinEventType,
target: Element | Document,
pointerType: PointerType,
isTargetAttached: boolean,
relatedTarget: null | Element | Document,
): FocusEvent {
return {
isTargetAttached,
relatedTarget,
target,
type,
pointerType,
Expand Down Expand Up @@ -297,7 +298,7 @@ function dispatchFocusEvents(
'focus',
target,
pointerType,
true,
null,
);
context.dispatchEvent(syntheticEvent, onFocus, DiscreteEvent);
}
Expand All @@ -321,7 +322,7 @@ function dispatchBlurEvents(
'blur',
target,
pointerType,
true,
null,
);
context.dispatchEvent(syntheticEvent, onBlur, DiscreteEvent);
}
Expand All @@ -346,7 +347,7 @@ function dispatchFocusWithinEvents(
'focuswithin',
target,
pointerType,
true,
null,
);
context.dispatchEvent(syntheticEvent, onFocusWithin, DiscreteEvent);
}
Expand All @@ -361,19 +362,40 @@ function dispatchBlurWithinEvents(
const pointerType = state.pointerType;
const target = ((state.focusTarget: any): Element | Document) || event.target;
const onBlurWithin = (props.onBlurWithin: any);
const isTargetAttached = state.detachedTarget === null;
if (isFunction(onBlurWithin)) {
const syntheticEvent = createFocusEvent(
context,
'blurwithin',
target,
pointerType,
isTargetAttached,
null,
);
context.dispatchEvent(syntheticEvent, onBlurWithin, DiscreteEvent);
}
}

function dispatchAfterBlurWithinEvents(
context: ReactDOMResponderContext,
event: ReactDOMResponderEvent,
props: FocusWithinProps,
state: FocusState,
) {
const pointerType = state.pointerType;
const target = ((state.focusTarget: any): Element | Document) || event.target;
const onAfterBlurWithin = (props.onAfterBlurWithin: any);
const relatedTarget = state.detachedTarget;
if (isFunction(onAfterBlurWithin)) {
const syntheticEvent = createFocusEvent(
context,
'afterblurwithin',
target,
pointerType,
relatedTarget,
);
context.dispatchEvent(syntheticEvent, onAfterBlurWithin, DiscreteEvent);
}
}

function dispatchFocusChange(
context: ReactDOMResponderContext,
props: FocusProps,
Expand Down Expand Up @@ -616,7 +638,7 @@ const focusWithinResponderImpl = {
'beforeblurwithin',
event.target,
state.pointerType,
true,
null,
);
state.detachedTarget = event.target;
context.dispatchEvent(
Expand Down Expand Up @@ -660,10 +682,10 @@ const focusWithinResponderImpl = {
props: FocusWithinProps,
state: FocusState,
): void {
if (event.type === 'blur') {
if (event.type === 'afterblur') {
const detachedTarget = state.detachedTarget;
if (detachedTarget !== null && detachedTarget === event.target) {
dispatchBlurWithinEvents(context, event, props, state);
dispatchAfterBlurWithinEvents(context, event, props, state);
state.detachedTarget = null;
if (state.addedRootEvents) {
state.addedRootEvents = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,11 +290,11 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
});

describe('onBeforeBlurWithin', () => {
let onBeforeBlurWithin, onBlurWithin, ref, innerRef, innerRef2;
let onBeforeBlurWithin, onAfterBlurWithin, ref, innerRef, innerRef2;

beforeEach(() => {
onBeforeBlurWithin = jest.fn();
onBlurWithin = jest.fn();
onAfterBlurWithin = jest.fn();
ref = React.createRef();
innerRef = React.createRef();
innerRef2 = React.createRef();
Expand All @@ -305,7 +305,7 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
const Component = ({show}) => {
const listener = useFocusWithin({
onBeforeBlurWithin,
onBlurWithin,
onAfterBlurWithin,
});
return (
<div ref={ref} DEPRECATED_flareListeners={listener}>
Expand All @@ -322,12 +322,12 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
target.keydown({key: 'Tab'});
target.focus();
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
expect(onBlurWithin).toHaveBeenCalledTimes(0);
expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
ReactDOM.render(<Component show={false} />, container);
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
expect(onBlurWithin).toHaveBeenCalledTimes(1);
expect(onBlurWithin).toHaveBeenCalledWith(
expect.objectContaining({isTargetAttached: false}),
expect(onAfterBlurWithin).toHaveBeenCalledTimes(1);
expect(onAfterBlurWithin).toHaveBeenCalledWith(
expect.objectContaining({relatedTarget: inner}),
);
});

Expand All @@ -336,7 +336,7 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
const Component = ({show}) => {
const listener = useFocusWithin({
onBeforeBlurWithin,
onBlurWithin,
onAfterBlurWithin,
});
return (
<div ref={ref} DEPRECATED_flareListeners={listener}>
Expand All @@ -357,12 +357,12 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
target.keydown({key: 'Tab'});
target.focus();
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
expect(onBlurWithin).toHaveBeenCalledTimes(0);
expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
ReactDOM.render(<Component show={false} />, container);
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
expect(onBlurWithin).toHaveBeenCalledTimes(1);
expect(onBlurWithin).toHaveBeenCalledWith(
expect.objectContaining({isTargetAttached: false}),
expect(onAfterBlurWithin).toHaveBeenCalledTimes(1);
expect(onAfterBlurWithin).toHaveBeenCalledWith(
expect.objectContaining({relatedTarget: inner}),
);
});

Expand Down Expand Up @@ -400,6 +400,139 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
expect(targetNodes).toEqual([targetNode]);
});

// @gate experimental
it('is called after a nested FocusRegion is unmounted', () => {
const TestScope = React.unstable_createScope();
const testScopeQuery = (type, props) => true;
const buttonRef = React.createRef();
const button2Ref = React.createRef();
const nestedButtonRef = React.createRef();

const FocusRegion = ({autoFocus, children}) => {
const scopeRef = React.useRef(null);
const prevElementsRef = React.useRef(null);
const listener = useFocusWithin({
onBeforeBlurWithin(event) {
// We continue propagation so nested FocusRegions can correctly
// pick up changes of beforeblur occuring below.
event.continuePropagation();
const scope = scopeRef.current;
if (scope !== null) {
const detachedNode = event.target;
const scopedNodes = scope.DO_NOT_USE_queryAllNodes(
(type, props, instance) =>
detachedNode === instance ||
testScopeQuery(type, props, instance),
);
if (scopedNodes === null) {
return;
}
// Build up arrays of nodes that are before to the detached node
const prevRecoveryElements = [];
for (let i = 0; i < scopedNodes.length; i++) {
const node = scopedNodes[i];
if (node === detachedNode) {
break;
}
prevRecoveryElements.push(node);
}
// Store this in the ref to use later
prevElementsRef.current = {
recovery: prevRecoveryElements,
all: scopedNodes,
};
}
},
onAfterBlurWithin(event) {
event.continuePropagation();
const scope = scopeRef.current;
const prevElements = prevElementsRef.current;

if (scope !== null && prevElements !== null) {
const {recovery, all} = prevElements;
const scopedNodes = scope.DO_NOT_USE_queryAllNodes(
testScopeQuery,
);
if (scopedNodes !== null) {
const scopeNodesSet = new Set(scopedNodes);
const allPreviousNodesSet = new Set(all);

// Look for closest previous node
for (let i = recovery.length - 1; i >= 0; i--) {
const prevRecoveryElement = recovery[i];
// If we find a match, focus nearby and exit early
if (scopeNodesSet.has(prevRecoveryElement)) {
const prevElementIndex = scopedNodes.indexOf(
prevRecoveryElement,
);
const replaceIndex = prevElementIndex + 1;
if (replaceIndex < scopedNodes.length) {
// Focus on the element that 'replaced' the previously focused element
const possibleReplacedNode = scopedNodes[replaceIndex];
if (!allPreviousNodesSet.has(possibleReplacedNode)) {
possibleReplacedNode.focus();
return;
}
}
// If there's no new focusable element, fallback to focusing
// on the previous recovery element
prevRecoveryElement.focus();
return;
}
}
// Otherwise focus the first element
scopedNodes[0].focus();
}
}
},
});

const attemptAutofocus = React.useCallback(() => {
const scope = scopeRef.current;
const activeElement = document.activeElement;
if (
scope !== null &&
autoFocus === true &&
(!activeElement || !scope.containsNode(activeElement))
) {
const scopedNodes = scope.DO_NOT_USE_queryAllNodes(testScopeQuery);
if (scopedNodes !== null) {
scopedNodes[0].focus();
}
}
}, [autoFocus]);
React.useLayoutEffect(attemptAutofocus, [attemptAutofocus]);
React.useEffect(attemptAutofocus, [attemptAutofocus]);

return (
<TestScope ref={scopeRef} DEPRECATED_flareListeners={listener}>
{children}
</TestScope>
);
};

const Test = ({showNestedRegion}) => (
<div>
<FocusRegion autoFocus={true}>
<button ref={buttonRef}>Press me!</button>
{showNestedRegion ? (
<FocusRegion autoFocus={true}>
<button ref={nestedButtonRef}>Press me 2!</button>
</FocusRegion>
) : null}
<button ref={button2Ref}>Press me!</button>
</FocusRegion>
</div>
);

ReactDOM.render(<Test showNestedRegion={false} />, container);
expect(document.activeElement).toBe(buttonRef.current);
ReactDOM.render(<Test showNestedRegion={true} />, container);
expect(document.activeElement).toBe(nestedButtonRef.current);
ReactDOM.render(<Test showNestedRegion={false} />, container);
expect(document.activeElement).toBe(buttonRef.current);
});

// @gate experimental
it('is called after a focused suspended element is hidden', () => {
const Suspense = React.Suspense;
Expand All @@ -418,7 +551,7 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
const Component = ({show}) => {
const listener = useFocusWithin({
onBeforeBlurWithin,
onBlurWithin,
onAfterBlurWithin,
});

return (
Expand All @@ -444,7 +577,7 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
target.keydown({key: 'Tab'});
target.focus();
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
expect(onBlurWithin).toHaveBeenCalledTimes(0);
expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);

suspend = true;
root.render(<Component />);
Expand All @@ -454,7 +587,7 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
'<div><input style="display: none;">Loading...</div>',
);
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
expect(onBlurWithin).toHaveBeenCalledTimes(1);
expect(onAfterBlurWithin).toHaveBeenCalledTimes(1);
resolve();

document.body.removeChild(container2);
Expand Down