mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
[react-interactions] Refine custom active element blur logic (#17354)
This commit is contained in:
@@ -465,3 +465,7 @@ export function unmountFundamentalComponent(fundamentalInstance) {
|
||||
export function getInstanceFromNode(node) {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
export function beforeRemoveInstance(instance) {
|
||||
// noop
|
||||
}
|
||||
|
||||
+49
-18
@@ -104,6 +104,12 @@ export type ChildSet = void; // Unused
|
||||
export type TimeoutHandle = TimeoutID;
|
||||
export type NoTimeout = -1;
|
||||
|
||||
type SelectionInformation = {|
|
||||
blurredActiveElement: null | HTMLElement,
|
||||
focusedElem: null | HTMLElement,
|
||||
selectionRange: mixed,
|
||||
|};
|
||||
|
||||
import {
|
||||
enableSuspenseServerRenderer,
|
||||
enableFlareAPI,
|
||||
@@ -127,7 +133,7 @@ const SUSPENSE_FALLBACK_START_DATA = '$!';
|
||||
const STYLE = 'style';
|
||||
|
||||
let eventsEnabled: ?boolean = null;
|
||||
let selectionInformation: ?mixed = null;
|
||||
let selectionInformation: null | SelectionInformation = null;
|
||||
|
||||
function shouldAutoFocusHostComponent(type: string, props: Props): boolean {
|
||||
switch (type) {
|
||||
@@ -205,6 +211,13 @@ 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);
|
||||
}
|
||||
}
|
||||
selectionInformation = null;
|
||||
ReactBrowserEventEmitterSetEnabled(eventsEnabled);
|
||||
eventsEnabled = null;
|
||||
@@ -452,30 +465,50 @@ export function insertInContainerBefore(
|
||||
}
|
||||
}
|
||||
|
||||
function dispatchFlareDetachedBlurEvent(
|
||||
elementDetached: boolean,
|
||||
targetInstance: null | Object,
|
||||
target: Element | Document,
|
||||
): void {
|
||||
// Simlulate the custom event to the React Flare responder system.
|
||||
dispatchEventForResponderEventSystem(
|
||||
'blur',
|
||||
targetInstance,
|
||||
({
|
||||
elementDetached,
|
||||
target,
|
||||
timeStamp: Date.now(),
|
||||
}: any),
|
||||
target,
|
||||
RESPONDER_EVENT_SYSTEM | IS_PASSIVE,
|
||||
);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
// This is a specific event for the React Flare
|
||||
// event system, so event responders can act
|
||||
// accordingly to a DOM node being unmounted that
|
||||
// previously had active document focus.
|
||||
function dispatchDetachedVisibleNodeEvent(
|
||||
child: Instance | TextInstance | SuspenseInstance,
|
||||
export function beforeRemoveInstance(
|
||||
instance: Instance | TextInstance | SuspenseInstance,
|
||||
): void {
|
||||
if (
|
||||
enableFlareAPI &&
|
||||
selectionInformation &&
|
||||
child === selectionInformation.focusedElem
|
||||
instance === selectionInformation.focusedElem
|
||||
) {
|
||||
const targetFiber = getClosestInstanceFromNode(child);
|
||||
// Simlulate a blur event to the React Flare responder system.
|
||||
dispatchEventForResponderEventSystem(
|
||||
'detachedvisiblenode',
|
||||
targetFiber,
|
||||
({
|
||||
target: child,
|
||||
timeStamp: Date.now(),
|
||||
}: any),
|
||||
((child: any): Document | Element),
|
||||
RESPONDER_EVENT_SYSTEM | IS_PASSIVE,
|
||||
);
|
||||
dispatchBeforeActiveElementBlur(((instance: any): HTMLElement));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,7 +516,6 @@ export function removeChild(
|
||||
parentInstance: Instance,
|
||||
child: Instance | TextInstance | SuspenseInstance,
|
||||
): void {
|
||||
dispatchDetachedVisibleNodeEvent(child);
|
||||
parentInstance.removeChild(child);
|
||||
}
|
||||
|
||||
@@ -494,7 +526,6 @@ export function removeChildFromContainer(
|
||||
if (container.nodeType === COMMENT_NODE) {
|
||||
(container.parentNode: any).removeChild(child);
|
||||
} else {
|
||||
dispatchDetachedVisibleNodeEvent(child);
|
||||
container.removeChild(child);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +100,8 @@ export function hasSelectionCapabilities(elem) {
|
||||
export function getSelectionInformation() {
|
||||
const focusedElem = getActiveElementDeep();
|
||||
return {
|
||||
// Used by Flare
|
||||
blurredActiveElement: null,
|
||||
focusedElem: focusedElem,
|
||||
selectionRange: hasSelectionCapabilities(focusedElem)
|
||||
? getSelection(focusedElem)
|
||||
|
||||
+50
-27
@@ -45,12 +45,7 @@ type FocusProps = {
|
||||
onFocusVisibleChange: boolean => void,
|
||||
};
|
||||
|
||||
type FocusEventType =
|
||||
| 'focus'
|
||||
| 'blur'
|
||||
| 'focuschange'
|
||||
| 'focusvisiblechange'
|
||||
| 'detachedvisiblenode';
|
||||
type FocusEventType = 'focus' | 'blur' | 'focuschange' | 'focusvisiblechange';
|
||||
|
||||
type FocusWithinProps = {
|
||||
disabled?: boolean,
|
||||
@@ -58,7 +53,8 @@ type FocusWithinProps = {
|
||||
onBlurWithin?: (e: FocusEvent) => void,
|
||||
onFocusWithinChange?: boolean => void,
|
||||
onFocusWithinVisibleChange?: boolean => void,
|
||||
onDetachedVisibleNode?: (e: FocusEvent) => void,
|
||||
onBeforeFocusedElementDetached?: (e: FocusEvent) => void,
|
||||
onFocusedElementDetached?: (e: FocusEvent) => void,
|
||||
};
|
||||
|
||||
type FocusWithinEventType =
|
||||
@@ -66,7 +62,8 @@ type FocusWithinEventType =
|
||||
| 'focuswithinchange'
|
||||
| 'blurwithin'
|
||||
| 'focuswithin'
|
||||
| 'detachedvisiblenode';
|
||||
| 'focusedelementdetached'
|
||||
| 'beforefocusedelementdetached';
|
||||
|
||||
/**
|
||||
* Shared between Focus and FocusWithin
|
||||
@@ -79,14 +76,22 @@ const isMac =
|
||||
? /^Mac/.test(window.navigator.platform)
|
||||
: false;
|
||||
|
||||
const targetEventTypes = ['focus', 'blur', 'detachedvisiblenode'];
|
||||
const targetEventTypes = ['focus', 'blur'];
|
||||
|
||||
const hasPointerEvents =
|
||||
typeof window !== 'undefined' && window.PointerEvent != null;
|
||||
|
||||
const rootEventTypes = hasPointerEvents
|
||||
? ['keydown', 'keyup', 'pointermove', 'pointerdown', 'pointerup']
|
||||
: ['keydown', 'keyup', 'mousedown', 'touchmove', 'touchstart', 'touchend'];
|
||||
? ['keydown', 'keyup', 'pointermove', 'pointerdown', 'pointerup', 'blur']
|
||||
: [
|
||||
'keydown',
|
||||
'keyup',
|
||||
'mousedown',
|
||||
'touchmove',
|
||||
'touchstart',
|
||||
'touchend',
|
||||
'blur',
|
||||
];
|
||||
|
||||
function isFunction(obj): boolean {
|
||||
return typeof obj === 'function';
|
||||
@@ -504,6 +509,23 @@ 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)
|
||||
@@ -514,22 +536,6 @@ const focusWithinResponderImpl = {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'detachedvisiblenode': {
|
||||
const onDetachedVisibleNode = (props.onDetachedVisibleNode: any);
|
||||
if (isFunction(onDetachedVisibleNode)) {
|
||||
const syntheticEvent = createFocusEvent(
|
||||
context,
|
||||
'detachedvisiblenode',
|
||||
event.target,
|
||||
state.pointerType,
|
||||
);
|
||||
context.dispatchEvent(
|
||||
syntheticEvent,
|
||||
onDetachedVisibleNode,
|
||||
DiscreteEvent,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onRootEvent(
|
||||
@@ -538,6 +544,23 @@ 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,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
handleRootEvent(event, context, state, isFocusVisible => {
|
||||
if (state.isFocused && state.isFocusVisible !== isFocusVisible) {
|
||||
state.isFocusVisible = isFocusVisible;
|
||||
|
||||
@@ -262,37 +262,77 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('onDetachedVisibleNode', () => {
|
||||
let onDetachedVisibleNode, ref, innerRef, innerRef2;
|
||||
|
||||
const Component = ({show}) => {
|
||||
const listener = useFocusWithin({
|
||||
onDetachedVisibleNode,
|
||||
});
|
||||
return (
|
||||
<div ref={ref} listeners={listener}>
|
||||
{show && <input ref={innerRef} />}
|
||||
<div ref={innerRef2} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
describe('onBeforeFocusedElementDetached/onFocusedElementDetached', () => {
|
||||
let onBeforeFocusedElementDetached,
|
||||
onFocusedElementDetached,
|
||||
ref,
|
||||
innerRef,
|
||||
innerRef2;
|
||||
|
||||
beforeEach(() => {
|
||||
onDetachedVisibleNode = jest.fn();
|
||||
onBeforeFocusedElementDetached = jest.fn();
|
||||
onFocusedElementDetached = jest.fn();
|
||||
ref = React.createRef();
|
||||
innerRef = React.createRef();
|
||||
innerRef2 = React.createRef();
|
||||
ReactDOM.render(<Component show={true} />, container);
|
||||
});
|
||||
|
||||
it('is called after a focused element is unmounted', () => {
|
||||
const Component = ({show}) => {
|
||||
const listener = useFocusWithin({
|
||||
onBeforeFocusedElementDetached,
|
||||
onFocusedElementDetached,
|
||||
});
|
||||
return (
|
||||
<div ref={ref} listeners={listener}>
|
||||
{show && <input ref={innerRef} />}
|
||||
<div ref={innerRef2} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ReactDOM.render(<Component show={true} />, container);
|
||||
|
||||
const inner = innerRef.current;
|
||||
const target = createEventTarget(inner);
|
||||
target.keydown({key: 'Tab'});
|
||||
target.focus();
|
||||
expect(onDetachedVisibleNode).toHaveBeenCalledTimes(0);
|
||||
expect(onBeforeFocusedElementDetached).toHaveBeenCalledTimes(0);
|
||||
expect(onFocusedElementDetached).toHaveBeenCalledTimes(0);
|
||||
ReactDOM.render(<Component show={false} />, container);
|
||||
expect(onDetachedVisibleNode).toHaveBeenCalledTimes(1);
|
||||
expect(onBeforeFocusedElementDetached).toHaveBeenCalledTimes(1);
|
||||
expect(onFocusedElementDetached).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('is called after a nested focused element is unmounted', () => {
|
||||
const Component = ({show}) => {
|
||||
const listener = useFocusWithin({
|
||||
onBeforeFocusedElementDetached,
|
||||
onFocusedElementDetached,
|
||||
});
|
||||
return (
|
||||
<div ref={ref} listeners={listener}>
|
||||
{show && (
|
||||
<div>
|
||||
<input ref={innerRef} />
|
||||
</div>
|
||||
)}
|
||||
<div ref={innerRef2} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ReactDOM.render(<Component show={true} />, container);
|
||||
|
||||
const inner = innerRef.current;
|
||||
const target = createEventTarget(inner);
|
||||
target.keydown({key: 'Tab'});
|
||||
target.focus();
|
||||
expect(onBeforeFocusedElementDetached).toHaveBeenCalledTimes(0);
|
||||
expect(onFocusedElementDetached).toHaveBeenCalledTimes(0);
|
||||
ReactDOM.render(<Component show={false} />, container);
|
||||
expect(onBeforeFocusedElementDetached).toHaveBeenCalledTimes(1);
|
||||
expect(onFocusedElementDetached).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -495,3 +495,7 @@ export function cloneFundamentalInstance(fundamentalInstance) {
|
||||
export function getInstanceFromNode(node) {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
export function beforeRemoveInstance(instance) {
|
||||
// noop
|
||||
}
|
||||
|
||||
@@ -534,3 +534,7 @@ export function unmountFundamentalComponent(fundamentalInstance) {
|
||||
export function getInstanceFromNode(node) {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
export function beforeRemoveInstance(instance) {
|
||||
// noop
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@ import {
|
||||
updateFundamentalComponent,
|
||||
commitHydratedContainer,
|
||||
commitHydratedSuspenseInstance,
|
||||
beforeRemoveInstance,
|
||||
} from './ReactFiberHostConfig';
|
||||
import {
|
||||
captureCommitPhaseError,
|
||||
@@ -808,6 +809,7 @@ function commitUnmount(
|
||||
dependencies.responders = null;
|
||||
}
|
||||
}
|
||||
beforeRemoveInstance(current.stateNode);
|
||||
}
|
||||
safelyDetachRef(current);
|
||||
return;
|
||||
|
||||
@@ -70,6 +70,7 @@ export const mountFundamentalComponent =
|
||||
export const shouldUpdateFundamentalComponent =
|
||||
$$$hostConfig.shouldUpdateFundamentalComponent;
|
||||
export const getInstanceFromNode = $$$hostConfig.getInstanceFromNode;
|
||||
export const beforeRemoveInstance = $$$hostConfig.beforeRemoveInstance;
|
||||
|
||||
// -------------------
|
||||
// Mutation
|
||||
|
||||
@@ -367,3 +367,7 @@ export function getInstanceFromNode(mockNode: Object) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function beforeRemoveInstance(instance) {
|
||||
// noop
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user