[react-interactions] Follow up active element blur logic (#17364)

This commit is contained in:
Dominic Gannaway
2019-11-14 11:55:50 +00:00
committed by GitHub
parent a61886b16b
commit fc43644eba
4 changed files with 84 additions and 81 deletions
+24 -24
View File
@@ -105,7 +105,7 @@ export type TimeoutHandle = TimeoutID;
export type NoTimeout = -1;
type SelectionInformation = {|
blurredActiveElement: null | HTMLElement,
activeElementDetached: null | HTMLElement,
focusedElem: null | HTMLElement,
selectionRange: mixed,
|};
@@ -212,10 +212,10 @@ export function prepareForCommit(containerInfo: Container): void {
export function resetAfterCommit(containerInfo: Container): void {
restoreSelection(selectionInformation);
if (enableFlareAPI) {
const blurredActiveElement = (selectionInformation: any)
.blurredActiveElement;
if (blurredActiveElement !== null) {
dispatchActiveElementBlur(blurredActiveElement);
const activeElementDetached = (selectionInformation: any)
.activeElementDetached;
if (activeElementDetached !== null) {
dispatchDetachedBlur(activeElementDetached);
}
}
selectionInformation = null;
@@ -465,17 +465,14 @@ export function insertInContainerBefore(
}
}
function dispatchFlareDetachedBlurEvent(
elementDetached: boolean,
targetInstance: null | Object,
target: Element | Document,
): void {
// Simlulate the custom event to the React Flare responder system.
function dispatchBeforeDetachedBlur(target: HTMLElement): void {
const targetInstance = getClosestInstanceFromNode(target);
((selectionInformation: any): SelectionInformation).activeElementDetached = target;
dispatchEventForResponderEventSystem(
'blur',
'beforeblur',
targetInstance,
({
elementDetached,
target,
timeStamp: Date.now(),
}: any),
@@ -484,16 +481,19 @@ function dispatchFlareDetachedBlurEvent(
);
}
function dispatchBeforeActiveElementBlur(element: HTMLElement): void {
const targtInstance = getClosestInstanceFromNode(element);
((selectionInformation: any): SelectionInformation).blurredActiveElement = element;
dispatchFlareDetachedBlurEvent(false, targtInstance, element);
}
function dispatchActiveElementBlur(
node: Instance | TextInstance | SuspenseInstance,
): void {
dispatchFlareDetachedBlurEvent(true, null, ((node: any): HTMLElement));
function dispatchDetachedBlur(target: HTMLElement): void {
const targetInstance = getClosestInstanceFromNode(target);
dispatchEventForResponderEventSystem(
'blur',
targetInstance,
({
isTargetAttached: false,
target,
timeStamp: Date.now(),
}: any),
target,
RESPONDER_EVENT_SYSTEM | IS_PASSIVE,
);
}
// This is a specific event for the React Flare
@@ -508,7 +508,7 @@ export function beforeRemoveInstance(
selectionInformation &&
instance === selectionInformation.focusedElem
) {
dispatchBeforeActiveElementBlur(((instance: any): HTMLElement));
dispatchBeforeDetachedBlur(((instance: any): HTMLElement));
}
}
+1 -1
View File
@@ -101,7 +101,7 @@ export function getSelectionInformation() {
const focusedElem = getActiveElementDeep();
return {
// Used by Flare
blurredActiveElement: null,
activeElementDetached: null,
focusedElem: focusedElem,
selectionRange: hasSelectionCapabilities(focusedElem)
? getSelection(focusedElem)
+37 -36
View File
@@ -22,6 +22,7 @@ import {DiscreteEvent} from 'shared/ReactTypes';
*/
type FocusEvent = {|
isTargetAttached: boolean,
target: Element | Document,
type: FocusEventType | FocusWithinEventType,
pointerType: PointerType,
@@ -30,6 +31,7 @@ type FocusEvent = {|
|};
type FocusState = {
detachedTarget: null | Element | Document,
focusTarget: null | Element | Document,
isFocused: boolean,
isFocusVisible: boolean,
@@ -50,11 +52,10 @@ type FocusEventType = 'focus' | 'blur' | 'focuschange' | 'focusvisiblechange';
type FocusWithinProps = {
disabled?: boolean,
onFocusWithin?: (e: FocusEvent) => void,
onBeforeBlurWithin?: (e: FocusEvent) => void,
onBlurWithin?: (e: FocusEvent) => void,
onFocusWithinChange?: boolean => void,
onFocusWithinVisibleChange?: boolean => void,
onBeforeFocusedElementDetached?: (e: FocusEvent) => void,
onFocusedElementDetached?: (e: FocusEvent) => void,
};
type FocusWithinEventType =
@@ -62,8 +63,7 @@ type FocusWithinEventType =
| 'focuswithinchange'
| 'blurwithin'
| 'focuswithin'
| 'focusedelementdetached'
| 'beforefocusedelementdetached';
| 'beforeblurwithin';
/**
* Shared between Focus and FocusWithin
@@ -76,7 +76,7 @@ const isMac =
? /^Mac/.test(window.navigator.platform)
: false;
const targetEventTypes = ['focus', 'blur'];
const targetEventTypes = ['focus', 'blur', 'beforeblur'];
const hasPointerEvents =
typeof window !== 'undefined' && window.PointerEvent != null;
@@ -102,8 +102,10 @@ function createFocusEvent(
type: FocusEventType | FocusWithinEventType,
target: Element | Document,
pointerType: PointerType,
isTargetAttached: boolean,
): FocusEvent {
return {
isTargetAttached,
target,
type,
pointerType,
@@ -218,6 +220,7 @@ function dispatchFocusEvents(
'focus',
target,
pointerType,
true,
);
context.dispatchEvent(syntheticEvent, onFocus, DiscreteEvent);
}
@@ -241,6 +244,7 @@ function dispatchBlurEvents(
'blur',
target,
pointerType,
true,
);
context.dispatchEvent(syntheticEvent, onBlur, DiscreteEvent);
}
@@ -265,6 +269,7 @@ function dispatchFocusWithinEvents(
'focuswithin',
target,
pointerType,
true,
);
context.dispatchEvent(syntheticEvent, onFocusWithin, DiscreteEvent);
}
@@ -279,12 +284,14 @@ 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,
);
context.dispatchEvent(syntheticEvent, onBlurWithin, DiscreteEvent);
}
@@ -328,6 +335,7 @@ const focusResponderImpl = {
rootEventTypes,
getInitialState(): FocusState {
return {
detachedTarget: null,
focusTarget: null,
isEmulatingMouseEvents: false,
isFocused: false,
@@ -466,6 +474,7 @@ const focusWithinResponderImpl = {
rootEventTypes,
getInitialState(): FocusState {
return {
detachedTarget: null,
focusTarget: null,
isEmulatingMouseEvents: false,
isFocused: false,
@@ -509,23 +518,6 @@ const focusWithinResponderImpl = {
break;
}
case 'blur': {
if ((nativeEvent: any).elementDetached === false) {
const onBeforeFocusedElementDetached = (props.onBeforeFocusedElementDetached: any);
if (isFunction(onBeforeFocusedElementDetached)) {
const syntheticEvent = createFocusEvent(
context,
'beforefocusedelementdetached',
event.target,
state.pointerType,
);
context.dispatchEvent(
syntheticEvent,
onBeforeFocusedElementDetached,
DiscreteEvent,
);
}
return;
}
if (
state.isFocused &&
!context.isTargetWithinResponder(relatedTarget)
@@ -536,6 +528,24 @@ const focusWithinResponderImpl = {
}
break;
}
case 'beforeblur': {
const onBeforeBlurWithin = (props.onBeforeBlurWithin: any);
if (isFunction(onBeforeBlurWithin)) {
const syntheticEvent = createFocusEvent(
context,
'beforeblurwithin',
event.target,
state.pointerType,
true,
);
state.detachedTarget = event.target;
context.dispatchEvent(
syntheticEvent,
onBeforeBlurWithin,
DiscreteEvent,
);
}
}
}
},
onRootEvent(
@@ -544,20 +554,11 @@ const focusWithinResponderImpl = {
props: FocusWithinProps,
state: FocusState,
): void {
if ((event.nativeEvent: any).elementDetached === true) {
const onFocusedElementDetached = (props.onFocusedElementDetached: any);
if (isFunction(onFocusedElementDetached)) {
const syntheticEvent = createFocusEvent(
context,
'focusedelementdetached',
event.target,
state.pointerType,
);
context.dispatchEvent(
syntheticEvent,
onFocusedElementDetached,
DiscreteEvent,
);
if (event.type === 'blur') {
const detachedTarget = state.detachedTarget;
if (detachedTarget !== null && detachedTarget === event.target) {
dispatchBlurWithinEvents(context, event, props, state);
state.detachedTarget = null;
}
return;
}
@@ -262,16 +262,12 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
});
});
describe('onBeforeFocusedElementDetached/onFocusedElementDetached', () => {
let onBeforeFocusedElementDetached,
onFocusedElementDetached,
ref,
innerRef,
innerRef2;
describe('onBeforeBlurWithin', () => {
let onBeforeBlurWithin, onBlurWithin, ref, innerRef, innerRef2;
beforeEach(() => {
onBeforeFocusedElementDetached = jest.fn();
onFocusedElementDetached = jest.fn();
onBeforeBlurWithin = jest.fn();
onBlurWithin = jest.fn();
ref = React.createRef();
innerRef = React.createRef();
innerRef2 = React.createRef();
@@ -280,8 +276,8 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
it('is called after a focused element is unmounted', () => {
const Component = ({show}) => {
const listener = useFocusWithin({
onBeforeFocusedElementDetached,
onFocusedElementDetached,
onBeforeBlurWithin,
onBlurWithin,
});
return (
<div ref={ref} listeners={listener}>
@@ -297,18 +293,21 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
const target = createEventTarget(inner);
target.keydown({key: 'Tab'});
target.focus();
expect(onBeforeFocusedElementDetached).toHaveBeenCalledTimes(0);
expect(onFocusedElementDetached).toHaveBeenCalledTimes(0);
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
expect(onBlurWithin).toHaveBeenCalledTimes(0);
ReactDOM.render(<Component show={false} />, container);
expect(onBeforeFocusedElementDetached).toHaveBeenCalledTimes(1);
expect(onFocusedElementDetached).toHaveBeenCalledTimes(1);
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
expect(onBlurWithin).toHaveBeenCalledTimes(1);
expect(onBlurWithin).toHaveBeenCalledWith(
expect.objectContaining({isTargetAttached: false}),
);
});
it('is called after a nested focused element is unmounted', () => {
const Component = ({show}) => {
const listener = useFocusWithin({
onBeforeFocusedElementDetached,
onFocusedElementDetached,
onBeforeBlurWithin,
onBlurWithin,
});
return (
<div ref={ref} listeners={listener}>
@@ -328,11 +327,14 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
const target = createEventTarget(inner);
target.keydown({key: 'Tab'});
target.focus();
expect(onBeforeFocusedElementDetached).toHaveBeenCalledTimes(0);
expect(onFocusedElementDetached).toHaveBeenCalledTimes(0);
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
expect(onBlurWithin).toHaveBeenCalledTimes(0);
ReactDOM.render(<Component show={false} />, container);
expect(onBeforeFocusedElementDetached).toHaveBeenCalledTimes(1);
expect(onFocusedElementDetached).toHaveBeenCalledTimes(1);
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
expect(onBlurWithin).toHaveBeenCalledTimes(1);
expect(onBlurWithin).toHaveBeenCalledWith(
expect.objectContaining({isTargetAttached: false}),
);
});
});