mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Remove ReactDOM.useEvent and associated types+tests (#18689)
This commit is contained in:
-16
@@ -493,22 +493,6 @@ export function makeServerId(): OpaqueIDType {
|
||||
throw new Error('Not yet implemented');
|
||||
}
|
||||
|
||||
export function registerEvent(event: any, rootContainerInstance: any) {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
export function mountEventListener(listener: any) {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
export function unmountEventListener(listener: any) {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
export function validateEventListenerTarget(target: any, listener: any) {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
export function beforeActiveInstanceBlur() {
|
||||
// noop
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import type {
|
||||
ReactProviderType,
|
||||
ReactEventResponder,
|
||||
ReactEventResponderListener,
|
||||
ReactScopeMethods,
|
||||
} from 'shared/ReactTypes';
|
||||
import type {
|
||||
Fiber,
|
||||
@@ -48,14 +47,6 @@ type HookLogEntry = {
|
||||
...
|
||||
};
|
||||
|
||||
type ReactDebugListenerMap = {|
|
||||
clear: () => void,
|
||||
setListener: (
|
||||
target: EventTarget | ReactScopeMethods,
|
||||
callback: ?(SyntheticEvent<EventTarget>) => void,
|
||||
) => void,
|
||||
|};
|
||||
|
||||
let hookLog: Array<HookLogEntry> = [];
|
||||
|
||||
// Primitives
|
||||
@@ -311,16 +302,6 @@ function useTransition(
|
||||
return [callback => {}, false];
|
||||
}
|
||||
|
||||
const noOp = () => {};
|
||||
|
||||
function useEvent(event: any): ReactDebugListenerMap {
|
||||
hookLog.push({primitive: 'Event', stackError: new Error(), value: event});
|
||||
return {
|
||||
clear: noOp,
|
||||
setListener: noOp,
|
||||
};
|
||||
}
|
||||
|
||||
function useDeferredValue<T>(value: T, config: TimeoutConfig | null | void): T {
|
||||
// useDeferredValue() composes multiple hooks internally.
|
||||
// Advance the current hook index the same number of times
|
||||
@@ -368,7 +349,6 @@ const Dispatcher: DispatcherType = {
|
||||
useTransition,
|
||||
useMutableSource,
|
||||
useDeferredValue,
|
||||
useEvent,
|
||||
useOpaqueIdentifier,
|
||||
};
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
'use strict';
|
||||
|
||||
let React;
|
||||
let ReactDOM;
|
||||
let ReactDebugTools;
|
||||
|
||||
describe('ReactHooksInspection', () => {
|
||||
@@ -19,9 +18,7 @@ describe('ReactHooksInspection', () => {
|
||||
jest.resetModules();
|
||||
const ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.enableDeprecatedFlareAPI = true;
|
||||
ReactFeatureFlags.enableUseEventAPI = true;
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
ReactDebugTools = require('react-debug-tools');
|
||||
});
|
||||
|
||||
@@ -46,45 +43,4 @@ describe('ReactHooksInspection', () => {
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
it('should inspect a simple ReactDOM.useEvent hook', () => {
|
||||
let clickHandle;
|
||||
let ref;
|
||||
|
||||
const effect = () => {
|
||||
clickHandle.setListener(ref.current, () => {});
|
||||
};
|
||||
|
||||
function Foo(props) {
|
||||
ref = React.useRef(null);
|
||||
clickHandle = ReactDOM.unstable_useEvent('click');
|
||||
React.useEffect(effect);
|
||||
return <div ref={ref}>Hello world</div>;
|
||||
}
|
||||
const tree = ReactDebugTools.inspectHooks(Foo, {});
|
||||
expect(tree).toEqual([
|
||||
{
|
||||
isStateEditable: false,
|
||||
id: 0,
|
||||
name: 'Ref',
|
||||
subHooks: [],
|
||||
value: null,
|
||||
},
|
||||
{
|
||||
isStateEditable: false,
|
||||
id: 1,
|
||||
name: 'Event',
|
||||
value: {capture: false, passive: undefined, priority: 0, type: 'click'},
|
||||
subHooks: [],
|
||||
},
|
||||
{
|
||||
isStateEditable: false,
|
||||
id: 2,
|
||||
name: 'Effect',
|
||||
value: effect,
|
||||
subHooks: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,5 +36,4 @@ export {
|
||||
unstable_scheduleHydration,
|
||||
unstable_renderSubtreeIntoContainer,
|
||||
unstable_createPortal,
|
||||
unstable_useEvent,
|
||||
} from './src/client/ReactDOM';
|
||||
|
||||
@@ -19,5 +19,4 @@ export {
|
||||
unstable_flushDiscreteUpdates,
|
||||
unstable_flushControlled,
|
||||
unstable_scheduleHydration,
|
||||
unstable_useEvent,
|
||||
} from './src/client/ReactDOM';
|
||||
|
||||
-3
@@ -20,7 +20,6 @@ import {
|
||||
unmountComponentAtNode,
|
||||
} from './ReactDOMLegacy';
|
||||
import {createRoot, createBlockingRoot, isValidContainer} from './ReactDOMRoot';
|
||||
import {useEvent} from './ReactDOMUseEvent';
|
||||
|
||||
import {
|
||||
batchedEventUpdates,
|
||||
@@ -211,8 +210,6 @@ export {
|
||||
// Temporary alias since we already shipped React 16 RC with it.
|
||||
// TODO: remove in React 17.
|
||||
unstable_createPortal,
|
||||
// enableUseEventAPI
|
||||
useEvent as unstable_useEvent,
|
||||
};
|
||||
|
||||
const foundDevTools = injectIntoDevTools({
|
||||
|
||||
@@ -15,7 +15,6 @@ import type {
|
||||
SuspenseInstance,
|
||||
Props,
|
||||
} from './ReactDOMHostConfig';
|
||||
import type {ReactDOMListener} from '../shared/ReactDOMTypes';
|
||||
|
||||
import {
|
||||
HostComponent,
|
||||
@@ -33,7 +32,6 @@ const randomKey = Math.random()
|
||||
const internalInstanceKey = '__reactFiber$' + randomKey;
|
||||
const internalEventHandlersKey = '__reactEvents$' + randomKey;
|
||||
const internalContainerInstanceKey = '__reactContainer$' + randomKey;
|
||||
const internalEventListenersKey = '__reactListeners$' + randomKey;
|
||||
|
||||
export function precacheFiberNode(
|
||||
hostInst: Fiber,
|
||||
@@ -187,18 +185,3 @@ export function updateFiberProps(
|
||||
): void {
|
||||
(node: any)[internalEventHandlersKey] = props;
|
||||
}
|
||||
|
||||
// This is used for useEvent listeners
|
||||
export function getListenersFromTarget(
|
||||
target: EventTarget,
|
||||
): null | Set<ReactDOMListener> {
|
||||
return (target: any)[internalEventListenersKey] || null;
|
||||
}
|
||||
|
||||
// This is used for useEvent listeners
|
||||
export function initListenersSet(
|
||||
target: EventTarget,
|
||||
value: Set<ReactDOMListener>,
|
||||
): void {
|
||||
(target: any)[internalEventListenersKey] = value;
|
||||
}
|
||||
|
||||
+7
-125
@@ -13,17 +13,12 @@ import type {
|
||||
ReactDOMEventResponder,
|
||||
ReactDOMEventResponderInstance,
|
||||
ReactDOMFundamentalComponentInstance,
|
||||
ReactDOMListener,
|
||||
ReactDOMListenerEvent,
|
||||
ReactDOMListenerMap,
|
||||
} from '../shared/ReactDOMTypes';
|
||||
import type {ReactScopeMethods} from 'shared/ReactTypes';
|
||||
|
||||
import {
|
||||
precacheFiberNode,
|
||||
updateFiberProps,
|
||||
getClosestInstanceFromNode,
|
||||
getListenersFromTarget,
|
||||
} from './ReactDOMComponentTree';
|
||||
import {
|
||||
createElement,
|
||||
@@ -69,32 +64,9 @@ import {
|
||||
enableSuspenseServerRenderer,
|
||||
enableDeprecatedFlareAPI,
|
||||
enableFundamentalAPI,
|
||||
enableUseEventAPI,
|
||||
enableScopeAPI,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
PLUGIN_EVENT_SYSTEM,
|
||||
USE_EVENT_SYSTEM,
|
||||
} from '../events/EventSystemFlags';
|
||||
import {
|
||||
isManagedDOMElement,
|
||||
isValidEventTarget,
|
||||
listenToTopLevelEvent,
|
||||
attachListenerToManagedDOMElement,
|
||||
detachListenerFromManagedDOMElement,
|
||||
attachTargetEventListener,
|
||||
detachTargetEventListener,
|
||||
isReactScope,
|
||||
attachListenerToReactScope,
|
||||
detachListenerFromReactScope,
|
||||
} from '../events/DOMModernPluginEventSystem';
|
||||
import {getListenerMapForElement} from '../events/DOMEventListenerMap';
|
||||
import {TOP_BEFORE_BLUR, TOP_AFTER_BLUR} from '../events/DOMTopLevelEventTypes';
|
||||
|
||||
export type ReactListenerEvent = ReactDOMListenerEvent;
|
||||
export type ReactListenerMap = ReactDOMListenerMap;
|
||||
export type ReactListener = ReactDOMListener;
|
||||
|
||||
export type Type = string;
|
||||
export type Props = {
|
||||
autoFocus?: boolean,
|
||||
@@ -246,7 +218,7 @@ export function prepareForCommit(containerInfo: Container): Object | null {
|
||||
eventsEnabled = ReactBrowserEventEmitterIsEnabled();
|
||||
selectionInformation = getSelectionInformation();
|
||||
let activeInstance = null;
|
||||
if (enableDeprecatedFlareAPI || enableUseEventAPI) {
|
||||
if (enableDeprecatedFlareAPI) {
|
||||
const focusedElem = selectionInformation.focusedElem;
|
||||
if (focusedElem !== null) {
|
||||
activeInstance = getClosestInstanceFromNode(focusedElem);
|
||||
@@ -257,7 +229,7 @@ export function prepareForCommit(containerInfo: Container): Object | null {
|
||||
}
|
||||
|
||||
export function beforeActiveInstanceBlur(): void {
|
||||
if (enableDeprecatedFlareAPI || enableUseEventAPI) {
|
||||
if (enableDeprecatedFlareAPI) {
|
||||
ReactBrowserEventEmitterSetEnabled(true);
|
||||
dispatchBeforeDetachedBlur((selectionInformation: any).focusedElem);
|
||||
ReactBrowserEventEmitterSetEnabled(false);
|
||||
@@ -265,7 +237,7 @@ export function beforeActiveInstanceBlur(): void {
|
||||
}
|
||||
|
||||
export function afterActiveInstanceBlur(): void {
|
||||
if (enableDeprecatedFlareAPI || enableUseEventAPI) {
|
||||
if (enableDeprecatedFlareAPI) {
|
||||
ReactBrowserEventEmitterSetEnabled(true);
|
||||
dispatchAfterDetachedBlur((selectionInformation: any).focusedElem);
|
||||
ReactBrowserEventEmitterSetEnabled(false);
|
||||
@@ -528,7 +500,7 @@ function createEvent(type: TopLevelType): Event {
|
||||
}
|
||||
|
||||
function dispatchBeforeDetachedBlur(target: HTMLElement): void {
|
||||
if (enableDeprecatedFlareAPI || enableUseEventAPI) {
|
||||
if (enableDeprecatedFlareAPI) {
|
||||
const event = createEvent(TOP_BEFORE_BLUR);
|
||||
// Dispatch "beforeblur" directly on the target,
|
||||
// so it gets picked up by the event system and
|
||||
@@ -538,7 +510,7 @@ function dispatchBeforeDetachedBlur(target: HTMLElement): void {
|
||||
}
|
||||
|
||||
function dispatchAfterDetachedBlur(target: HTMLElement): void {
|
||||
if (enableDeprecatedFlareAPI || enableUseEventAPI) {
|
||||
if (enableDeprecatedFlareAPI) {
|
||||
const event = createEvent(TOP_AFTER_BLUR);
|
||||
// So we know what was detached, make the relatedTarget the
|
||||
// detached target on the "afterblur" event.
|
||||
@@ -550,23 +522,8 @@ function dispatchAfterDetachedBlur(target: HTMLElement): void {
|
||||
|
||||
export function beforeRemoveInstance(
|
||||
instance: Instance | TextInstance | SuspenseInstance,
|
||||
): void {
|
||||
if (enableUseEventAPI) {
|
||||
// It's unfortunate that we have to do this cleanup, but
|
||||
// it's necessary otherwise we will leak the host instances
|
||||
// from the useEvent hook instances Map. We call destroy
|
||||
// on each listener to ensure we properly remove the instance
|
||||
// from the instances Map. Note: we have this Map so that we
|
||||
// can properly unmount instances when the function component
|
||||
// that the hook is attached to gets unmounted.
|
||||
const listenersSet = getListenersFromTarget(instance);
|
||||
if (listenersSet !== null) {
|
||||
const listeners = Array.from(listenersSet);
|
||||
for (let i = 0; i < listeners.length; i++) {
|
||||
listeners[i].destroy(instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
// TODO for ReactDOM.createEventInstance
|
||||
}
|
||||
|
||||
export function removeChild(
|
||||
@@ -1141,78 +1098,3 @@ export function makeOpaqueHydratingObject(
|
||||
valueOf: attemptToReadValue,
|
||||
};
|
||||
}
|
||||
|
||||
export function registerEvent(
|
||||
event: ReactDOMListenerEvent,
|
||||
rootContainerInstance: Container,
|
||||
): void {
|
||||
const {passive, priority, type} = event;
|
||||
const listenerMap = getListenerMapForElement(rootContainerInstance);
|
||||
// Add the event listener to the target container (falling back to
|
||||
// the target if we didn't find one).
|
||||
listenToTopLevelEvent(
|
||||
type,
|
||||
rootContainerInstance,
|
||||
listenerMap,
|
||||
PLUGIN_EVENT_SYSTEM | USE_EVENT_SYSTEM,
|
||||
passive,
|
||||
priority,
|
||||
);
|
||||
}
|
||||
|
||||
export function mountEventListener(listener: ReactDOMListener): void {
|
||||
if (enableUseEventAPI) {
|
||||
const {target} = listener;
|
||||
if (isManagedDOMElement(target)) {
|
||||
attachListenerToManagedDOMElement(listener);
|
||||
} else if (enableScopeAPI && isReactScope(target)) {
|
||||
attachListenerToReactScope(listener);
|
||||
} else {
|
||||
attachTargetEventListener(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function unmountEventListener(listener: ReactDOMListener): void {
|
||||
if (enableUseEventAPI) {
|
||||
const {target} = listener;
|
||||
if (isManagedDOMElement(target)) {
|
||||
detachListenerFromManagedDOMElement(listener);
|
||||
} else if (enableScopeAPI && isReactScope(target)) {
|
||||
detachListenerFromReactScope(listener);
|
||||
} else {
|
||||
detachTargetEventListener(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function validateEventListenerTarget(
|
||||
target: EventTarget | ReactScopeMethods,
|
||||
listener: ?(SyntheticEvent<EventTarget>) => void,
|
||||
): boolean {
|
||||
if (enableUseEventAPI) {
|
||||
if (
|
||||
target != null &&
|
||||
(isManagedDOMElement(target) ||
|
||||
isValidEventTarget(target) ||
|
||||
isReactScope(target))
|
||||
) {
|
||||
if (listener == null || typeof listener === 'function') {
|
||||
return true;
|
||||
}
|
||||
if (__DEV__) {
|
||||
console.warn(
|
||||
'Event listener method setListener() from useEvent() hook requires the second argument' +
|
||||
' to be either a valid function callback or null/undefined.',
|
||||
);
|
||||
}
|
||||
}
|
||||
if (__DEV__) {
|
||||
console.warn(
|
||||
'Event listener method setListener() from useEvent() hook requires the first argument to be ' +
|
||||
'a valid DOM EventTarget. If using a ref, ensure the current value is not null.',
|
||||
);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';
|
||||
import type {EventPriority} from 'shared/ReactTypes';
|
||||
import type {
|
||||
ReactDOMListenerEvent,
|
||||
ReactDOMListenerMap,
|
||||
} from '../shared/ReactDOMTypes';
|
||||
|
||||
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
||||
import invariant from 'shared/invariant';
|
||||
|
||||
import {getEventPriorityForListenerSystem} from '../events/DOMEventProperties';
|
||||
|
||||
type EventOptions = {|
|
||||
capture?: boolean,
|
||||
passive?: boolean,
|
||||
priority?: EventPriority,
|
||||
|};
|
||||
|
||||
const {ReactCurrentDispatcher} = ReactSharedInternals;
|
||||
|
||||
function resolveDispatcher() {
|
||||
const dispatcher = ReactCurrentDispatcher.current;
|
||||
invariant(
|
||||
dispatcher !== null,
|
||||
'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
|
||||
' one of the following reasons:\n' +
|
||||
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
|
||||
'2. You might be breaking the Rules of Hooks\n' +
|
||||
'3. You might have more than one copy of React in the same app\n' +
|
||||
'See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.',
|
||||
);
|
||||
return dispatcher;
|
||||
}
|
||||
|
||||
export function useEvent(
|
||||
type: string,
|
||||
options?: EventOptions,
|
||||
): ReactDOMListenerMap {
|
||||
const topLevelType = ((type: any): DOMTopLevelEventType);
|
||||
const dispatcher = resolveDispatcher();
|
||||
let capture = false;
|
||||
let passive = undefined; // Undefined means to use the browser default
|
||||
let priority;
|
||||
|
||||
if (options != null) {
|
||||
const optionsCapture = options.capture;
|
||||
const optionsPassive = options.passive;
|
||||
const optionsPriority = options.priority;
|
||||
|
||||
if (typeof optionsCapture === 'boolean') {
|
||||
capture = optionsCapture;
|
||||
}
|
||||
if (typeof optionsPassive === 'boolean') {
|
||||
passive = optionsPassive;
|
||||
}
|
||||
if (typeof optionsPriority === 'number') {
|
||||
priority = optionsPriority;
|
||||
}
|
||||
}
|
||||
if (priority === undefined) {
|
||||
priority = getEventPriorityForListenerSystem(topLevelType);
|
||||
}
|
||||
const event: ReactDOMListenerEvent = {
|
||||
capture,
|
||||
passive,
|
||||
priority,
|
||||
type: topLevelType,
|
||||
};
|
||||
return dispatcher.useEvent(event);
|
||||
}
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
UserBlockingEvent,
|
||||
ContinuousEvent,
|
||||
} from 'shared/ReactTypes';
|
||||
import {enableUseEventAPI} from 'shared/ReactFeatureFlags';
|
||||
|
||||
// Needed for SimpleEventPlugin, rather than
|
||||
// do it in two places, which duplicates logic
|
||||
@@ -96,13 +95,6 @@ const otherDiscreteEvents = [
|
||||
DOMTopLevelEventTypes.TOP_COMPOSITION_UPDATE,
|
||||
];
|
||||
|
||||
if (enableUseEventAPI) {
|
||||
otherDiscreteEvents.push(
|
||||
DOMTopLevelEventTypes.TOP_BEFORE_BLUR,
|
||||
DOMTopLevelEventTypes.TOP_AFTER_BLUR,
|
||||
);
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
const userBlockingPairsForSimpleEventPlugin = [
|
||||
DOMTopLevelEventTypes.TOP_DRAG, 'drag',
|
||||
|
||||
+4
-347
@@ -17,11 +17,7 @@ import type {EventSystemFlags} from './EventSystemFlags';
|
||||
import type {EventPriority, ReactScopeMethods} from 'shared/ReactTypes';
|
||||
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
|
||||
import type {PluginModule} from 'legacy-events/PluginModuleType';
|
||||
import type {
|
||||
ReactSyntheticEvent,
|
||||
CustomDispatchConfig,
|
||||
} from 'legacy-events/ReactSyntheticEventType';
|
||||
import type {ReactDOMListener} from '../shared/ReactDOMTypes';
|
||||
import type {ReactSyntheticEvent} from 'legacy-events/ReactSyntheticEventType';
|
||||
|
||||
import {registrationNameDependencies} from 'legacy-events/EventPluginRegistry';
|
||||
import {plugins} from 'legacy-events/EventPluginRegistry';
|
||||
@@ -36,7 +32,6 @@ import {
|
||||
import {
|
||||
HostRoot,
|
||||
HostPortal,
|
||||
ScopeComponent,
|
||||
HostComponent,
|
||||
} from 'react-reconciler/src/ReactWorkTags';
|
||||
|
||||
@@ -80,25 +75,14 @@ import {
|
||||
TOP_PROGRESS,
|
||||
TOP_PLAYING,
|
||||
TOP_CLICK,
|
||||
TOP_BEFORE_BLUR,
|
||||
TOP_AFTER_BLUR,
|
||||
TOP_SELECTION_CHANGE,
|
||||
} from './DOMTopLevelEventTypes';
|
||||
import {
|
||||
getClosestInstanceFromNode,
|
||||
getListenersFromTarget,
|
||||
initListenersSet,
|
||||
} from '../client/ReactDOMComponentTree';
|
||||
import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree';
|
||||
import {COMMENT_NODE} from '../shared/HTMLNodeType';
|
||||
import {topLevelEventsToDispatchConfig} from './DOMEventProperties';
|
||||
import {batchedEventUpdates} from './ReactDOMUpdateBatching';
|
||||
import getListener from './getListener';
|
||||
|
||||
import {
|
||||
enableLegacyFBSupport,
|
||||
enableUseEventAPI,
|
||||
enableScopeAPI,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {enableLegacyFBSupport} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
invokeGuardedCallbackAndCatchFirstError,
|
||||
rethrowCaughtError,
|
||||
@@ -140,44 +124,8 @@ const capturePhaseEvents = new Set([
|
||||
TOP_WAITING,
|
||||
]);
|
||||
|
||||
if (enableUseEventAPI) {
|
||||
capturePhaseEvents.add(TOP_BEFORE_BLUR);
|
||||
capturePhaseEvents.add(TOP_AFTER_BLUR);
|
||||
}
|
||||
|
||||
const emptyDispatchConfigForCustomEvents: CustomDispatchConfig = {
|
||||
customEvent: true,
|
||||
phasedRegistrationNames: {
|
||||
bubbled: null,
|
||||
captured: null,
|
||||
},
|
||||
};
|
||||
|
||||
const isArray = Array.isArray;
|
||||
|
||||
// TODO: we should remove the FlowFixMes and the casting to figure out how to make
|
||||
// these patterns work properly.
|
||||
// $FlowFixMe: Flow struggles with this pattern, so we also have to cast it.
|
||||
const PossiblyWeakMap = ((typeof WeakMap === 'function' ? WeakMap : Map): any);
|
||||
|
||||
// $FlowFixMe: Flow cannot handle polymorphic WeakMaps
|
||||
const eventTargetEventListenerStore: WeakMap<
|
||||
EventTarget,
|
||||
Map<
|
||||
DOMTopLevelEventType,
|
||||
{bubbled: Set<ReactDOMListener>, captured: Set<ReactDOMListener>},
|
||||
>,
|
||||
> = new PossiblyWeakMap();
|
||||
|
||||
// $FlowFixMe: Flow cannot handle polymorphic WeakMaps
|
||||
const reactScopeListenerStore: WeakMap<
|
||||
ReactScopeMethods,
|
||||
Map<
|
||||
DOMTopLevelEventType,
|
||||
{bubbled: Set<ReactDOMListener>, captured: Set<ReactDOMListener>},
|
||||
>,
|
||||
> = new PossiblyWeakMap();
|
||||
|
||||
function executeDispatch(
|
||||
event: ReactSyntheticEvent,
|
||||
listener: Function,
|
||||
@@ -514,183 +462,7 @@ export function dispatchEventForPluginEventSystem(
|
||||
);
|
||||
}
|
||||
|
||||
function getNearestRootOrPortalContainer(instance: Element): Element {
|
||||
let node = getClosestInstanceFromNode(instance);
|
||||
while (node !== null) {
|
||||
const tag = node.tag;
|
||||
// Once we encounter a host container or root container
|
||||
// we can return their DOM instance.
|
||||
if (tag === HostRoot || tag === HostPortal) {
|
||||
return node.stateNode.containerInfo;
|
||||
}
|
||||
node = node.return;
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
function addEventTypeToDispatchConfig(type: DOMTopLevelEventType): void {
|
||||
const dispatchConfig = topLevelEventsToDispatchConfig.get(type);
|
||||
// If we don't have a dispatchConfig, then we're dealing with
|
||||
// an event type that React does not know about (i.e. a custom event).
|
||||
// We need to register an event config for this or the SimpleEventPlugin
|
||||
// will not appropriately provide a SyntheticEvent, so we use out empty
|
||||
// dispatch config for custom events.
|
||||
if (dispatchConfig === undefined) {
|
||||
topLevelEventsToDispatchConfig.set(
|
||||
type,
|
||||
emptyDispatchConfigForCustomEvents,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function attachListenerToManagedDOMElement(
|
||||
listener: ReactDOMListener,
|
||||
): void {
|
||||
const {event, target} = listener;
|
||||
const {passive, priority, type} = event;
|
||||
|
||||
const managedTargetElement = ((target: any): Element);
|
||||
const containerEventTarget = getNearestRootOrPortalContainer(
|
||||
managedTargetElement,
|
||||
);
|
||||
const listenerMap = getListenerMapForElement(containerEventTarget);
|
||||
// Add the event listener to the target container (falling back to
|
||||
// the target if we didn't find one).
|
||||
listenToTopLevelEvent(
|
||||
type,
|
||||
containerEventTarget,
|
||||
listenerMap,
|
||||
PLUGIN_EVENT_SYSTEM | USE_EVENT_SYSTEM,
|
||||
passive,
|
||||
priority,
|
||||
);
|
||||
// Get the internal listeners Set from the target instance.
|
||||
let listeners = getListenersFromTarget(managedTargetElement);
|
||||
// If we don't have any listeners, then we need to init them.
|
||||
if (listeners === null) {
|
||||
listeners = new Set();
|
||||
initListenersSet(managedTargetElement, listeners);
|
||||
}
|
||||
// Add our listener to the listeners Set.
|
||||
listeners.add(listener);
|
||||
// Finally, add the event to our known event types list.
|
||||
addEventTypeToDispatchConfig(type);
|
||||
}
|
||||
|
||||
export function detachListenerFromManagedDOMElement(
|
||||
listener: ReactDOMListener,
|
||||
): void {
|
||||
const {target} = listener;
|
||||
const managedTargetElement = ((target: any): Element);
|
||||
// Get the internal listeners Set from the target instance.
|
||||
const listeners = getListenersFromTarget(managedTargetElement);
|
||||
if (listeners !== null) {
|
||||
// Remove out listener from the listeners Set.
|
||||
listeners.delete(listener);
|
||||
}
|
||||
}
|
||||
|
||||
export function attachTargetEventListener(listener: ReactDOMListener): void {
|
||||
const {event, target} = listener;
|
||||
const {capture, passive, priority, type} = event;
|
||||
const eventTarget = ((target: any): EventTarget);
|
||||
const listenerMap = getListenerMapForElement(eventTarget);
|
||||
// Add the event listener to the TargetEvent object.
|
||||
listenToTopLevelEvent(
|
||||
type,
|
||||
eventTarget,
|
||||
listenerMap,
|
||||
PLUGIN_EVENT_SYSTEM | USE_EVENT_SYSTEM | IS_TARGET_PHASE_ONLY,
|
||||
passive,
|
||||
priority,
|
||||
capture,
|
||||
);
|
||||
let eventTypeMap = eventTargetEventListenerStore.get(eventTarget);
|
||||
if (eventTypeMap === undefined) {
|
||||
eventTypeMap = new Map();
|
||||
eventTargetEventListenerStore.set(eventTarget, eventTypeMap);
|
||||
}
|
||||
// Get the listeners by the event type
|
||||
let listeners = eventTypeMap.get(type);
|
||||
if (listeners === undefined) {
|
||||
listeners = {captured: new Set(), bubbled: new Set()};
|
||||
eventTypeMap.set(type, listeners);
|
||||
}
|
||||
// Add our listener to the listeners Set.
|
||||
if (capture) {
|
||||
listeners.captured.add(listener);
|
||||
} else {
|
||||
listeners.bubbled.add(listener);
|
||||
}
|
||||
// Finally, add the event to our known event types list.
|
||||
addEventTypeToDispatchConfig(type);
|
||||
}
|
||||
|
||||
export function detachTargetEventListener(listener: ReactDOMListener): void {
|
||||
const {event, target} = listener;
|
||||
const {capture, type} = event;
|
||||
const validEventTarget = ((target: any): EventTarget);
|
||||
const eventTypeMap = eventTargetEventListenerStore.get(validEventTarget);
|
||||
if (eventTypeMap !== undefined) {
|
||||
const listeners = eventTypeMap.get(type);
|
||||
if (listeners !== undefined) {
|
||||
// Remove out listener from the listeners Set.
|
||||
if (capture) {
|
||||
listeners.captured.delete(listener);
|
||||
} else {
|
||||
listeners.bubbled.delete(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function attachListenerToReactScope(listener: ReactDOMListener): void {
|
||||
const {event, target} = listener;
|
||||
const {capture, type} = event;
|
||||
const reactScope = ((target: any): ReactScopeMethods);
|
||||
let eventTypeMap = reactScopeListenerStore.get(reactScope);
|
||||
if (eventTypeMap === undefined) {
|
||||
eventTypeMap = new Map();
|
||||
reactScopeListenerStore.set(reactScope, eventTypeMap);
|
||||
}
|
||||
// Get the listeners by the event type
|
||||
let listeners = eventTypeMap.get(type);
|
||||
if (listeners === undefined) {
|
||||
listeners = {captured: new Set(), bubbled: new Set()};
|
||||
eventTypeMap.set(type, listeners);
|
||||
}
|
||||
// Add our listener to the listeners Set.
|
||||
if (capture) {
|
||||
listeners.captured.add(listener);
|
||||
} else {
|
||||
listeners.bubbled.add(listener);
|
||||
}
|
||||
// Finally, add the event to our known event types list.
|
||||
addEventTypeToDispatchConfig(type);
|
||||
}
|
||||
|
||||
export function detachListenerFromReactScope(listener: ReactDOMListener): void {
|
||||
const {event, target} = listener;
|
||||
const {capture, type} = event;
|
||||
const reactScope = ((target: any): ReactScopeMethods);
|
||||
const eventTypeMap = reactScopeListenerStore.get(reactScope);
|
||||
if (eventTypeMap !== undefined) {
|
||||
const listeners = eventTypeMap.get(type);
|
||||
if (listeners !== undefined) {
|
||||
// Remove out listener from the listeners Set.
|
||||
if (capture) {
|
||||
listeners.captured.delete(listener);
|
||||
} else {
|
||||
listeners.bubbled.delete(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function accumulateTwoPhaseListeners(
|
||||
event: ReactSyntheticEvent,
|
||||
accumulateUseEventListeners?: boolean,
|
||||
): void {
|
||||
export function accumulateTwoPhaseListeners(event: ReactSyntheticEvent): void {
|
||||
const phasedRegistrationNames = event.dispatchConfig.phasedRegistrationNames;
|
||||
const dispatchListeners = [];
|
||||
const dispatchInstances: Array<Fiber | null> = [];
|
||||
@@ -701,7 +473,6 @@ export function accumulateTwoPhaseListeners(
|
||||
// usual two phase accumulation using the React fiber tree to pick up
|
||||
// all relevant useEvent and on* prop events.
|
||||
let instance = event._targetInst;
|
||||
let lastHostComponent = null;
|
||||
|
||||
// Accumulate all instances and listeners via the target -> root path.
|
||||
while (instance !== null) {
|
||||
@@ -709,35 +480,6 @@ export function accumulateTwoPhaseListeners(
|
||||
// Handle listeners that are on HostComponents (i.e. <div>)
|
||||
if (tag === HostComponent && stateNode !== null) {
|
||||
const currentTarget = stateNode;
|
||||
lastHostComponent = currentTarget;
|
||||
// For useEvent listenrs
|
||||
if (enableUseEventAPI && accumulateUseEventListeners) {
|
||||
// useEvent event listeners
|
||||
const targetType = event.type;
|
||||
const listeners = getListenersFromTarget(currentTarget);
|
||||
|
||||
if (listeners !== null) {
|
||||
const listenersArr = Array.from(listeners);
|
||||
for (let i = 0; i < listenersArr.length; i++) {
|
||||
const listener = listenersArr[i];
|
||||
const {
|
||||
callback,
|
||||
event: {capture, type},
|
||||
} = listener;
|
||||
if (type === targetType) {
|
||||
if (capture === true) {
|
||||
dispatchListeners.unshift(callback);
|
||||
dispatchInstances.unshift(instance);
|
||||
dispatchCurrentTargets.unshift(currentTarget);
|
||||
} else {
|
||||
dispatchListeners.push(callback);
|
||||
dispatchInstances.push(instance);
|
||||
dispatchCurrentTargets.push(currentTarget);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Standard React on* listeners, i.e. onClick prop
|
||||
if (captured !== null) {
|
||||
const captureListener = getListener(instance, captured);
|
||||
@@ -760,40 +502,6 @@ export function accumulateTwoPhaseListeners(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
enableUseEventAPI &&
|
||||
enableScopeAPI &&
|
||||
accumulateUseEventListeners &&
|
||||
tag === ScopeComponent &&
|
||||
lastHostComponent !== null
|
||||
) {
|
||||
const reactScope = stateNode.methods;
|
||||
const eventTypeMap = reactScopeListenerStore.get(reactScope);
|
||||
if (eventTypeMap !== undefined) {
|
||||
const type = ((event.type: any): DOMTopLevelEventType);
|
||||
const listeners = eventTypeMap.get(type);
|
||||
if (listeners !== undefined) {
|
||||
const captureListeners = Array.from(listeners.captured);
|
||||
const bubbleListeners = Array.from(listeners.bubbled);
|
||||
const lastCurrentTarget = ((lastHostComponent: any): Element);
|
||||
|
||||
for (let i = 0; i < captureListeners.length; i++) {
|
||||
const listener = captureListeners[i];
|
||||
const {callback} = listener;
|
||||
dispatchListeners.unshift(callback);
|
||||
dispatchInstances.unshift(instance);
|
||||
dispatchCurrentTargets.unshift(lastCurrentTarget);
|
||||
}
|
||||
for (let i = 0; i < bubbleListeners.length; i++) {
|
||||
const listener = bubbleListeners[i];
|
||||
const {callback} = listener;
|
||||
dispatchListeners.push(callback);
|
||||
dispatchInstances.push(instance);
|
||||
dispatchCurrentTargets.push(lastCurrentTarget);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
instance = instance.return;
|
||||
}
|
||||
|
||||
@@ -806,57 +514,6 @@ export function accumulateTwoPhaseListeners(
|
||||
}
|
||||
}
|
||||
|
||||
export function accumulateEventTargetListeners(
|
||||
event: ReactSyntheticEvent,
|
||||
currentTarget: EventTarget,
|
||||
): void {
|
||||
const dispatchListeners = [];
|
||||
const dispatchInstances: Array<Fiber | null> = [];
|
||||
const dispatchCurrentTargets = [];
|
||||
|
||||
const eventTypeMap = eventTargetEventListenerStore.get(currentTarget);
|
||||
if (eventTypeMap !== undefined) {
|
||||
const type = ((event.type: any): DOMTopLevelEventType);
|
||||
const listeners = eventTypeMap.get(type);
|
||||
if (listeners !== undefined) {
|
||||
const isCapturePhase = (event: any).eventPhase === 1;
|
||||
|
||||
if (isCapturePhase) {
|
||||
const captureListeners = Array.from(listeners.captured);
|
||||
|
||||
for (let i = captureListeners.length - 1; i >= 0; i--) {
|
||||
const listener = captureListeners[i];
|
||||
const {callback} = listener;
|
||||
dispatchListeners.push(callback);
|
||||
// EventTarget listeners do not have instances, as there
|
||||
// is no backing Fiber instance for them (window, document etc).
|
||||
dispatchInstances.push(null);
|
||||
dispatchCurrentTargets.push(currentTarget);
|
||||
}
|
||||
} else {
|
||||
const bubbleListeners = Array.from(listeners.bubbled);
|
||||
|
||||
for (let i = 0; i < bubbleListeners.length; i++) {
|
||||
const listener = bubbleListeners[i];
|
||||
const {callback} = listener;
|
||||
dispatchListeners.push(callback);
|
||||
// EventTarget listeners do not have instances, as there
|
||||
// is no backing Fiber instance for them (window, document etc).
|
||||
dispatchInstances.push(null);
|
||||
dispatchCurrentTargets.push(currentTarget);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// To prevent allocation to the event unless we actually
|
||||
// have listeners we check the length of one of the arrays.
|
||||
if (dispatchListeners.length > 0) {
|
||||
event._dispatchListeners = dispatchListeners;
|
||||
event._dispatchInstances = dispatchInstances;
|
||||
event._dispatchCurrentTargets = dispatchCurrentTargets;
|
||||
}
|
||||
}
|
||||
|
||||
function getParent(inst: Fiber | null): Fiber | null {
|
||||
if (inst === null) {
|
||||
return null;
|
||||
|
||||
+10
-32
@@ -44,7 +44,6 @@ import {
|
||||
addEventBubbleListener,
|
||||
addEventCaptureListener,
|
||||
addEventCaptureListenerWithPassiveFlag,
|
||||
addEventBubbleListenerWithPassiveFlag,
|
||||
removeEventListener,
|
||||
} from './EventListener';
|
||||
import getEventTarget from './getEventTarget';
|
||||
@@ -56,7 +55,6 @@ import {
|
||||
enableDeprecatedFlareAPI,
|
||||
enableModernEventSystem,
|
||||
enableLegacyFBSupport,
|
||||
enableUseEventAPI,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
UserBlockingEvent,
|
||||
@@ -200,37 +198,17 @@ export function addTrappedEventListener(
|
||||
};
|
||||
}
|
||||
if (capture) {
|
||||
if (enableUseEventAPI && passive !== undefined) {
|
||||
// This is only used with passive is either true or false.
|
||||
unsubscribeListener = addEventCaptureListenerWithPassiveFlag(
|
||||
targetContainer,
|
||||
rawEventName,
|
||||
listener,
|
||||
passive,
|
||||
);
|
||||
} else {
|
||||
unsubscribeListener = addEventCaptureListener(
|
||||
targetContainer,
|
||||
rawEventName,
|
||||
listener,
|
||||
);
|
||||
}
|
||||
unsubscribeListener = addEventCaptureListener(
|
||||
targetContainer,
|
||||
rawEventName,
|
||||
listener,
|
||||
);
|
||||
} else {
|
||||
if (enableUseEventAPI && passive !== undefined) {
|
||||
// This is only used with passive is either true or false.
|
||||
unsubscribeListener = addEventBubbleListenerWithPassiveFlag(
|
||||
targetContainer,
|
||||
rawEventName,
|
||||
listener,
|
||||
passive,
|
||||
);
|
||||
} else {
|
||||
unsubscribeListener = addEventBubbleListener(
|
||||
targetContainer,
|
||||
rawEventName,
|
||||
listener,
|
||||
);
|
||||
}
|
||||
unsubscribeListener = addEventBubbleListener(
|
||||
targetContainer,
|
||||
rawEventName,
|
||||
listener,
|
||||
);
|
||||
}
|
||||
return unsubscribeListener;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,17 +17,13 @@ import type {PluginModule} from 'legacy-events/PluginModuleType';
|
||||
import type {EventSystemFlags} from '../EventSystemFlags';
|
||||
|
||||
import SyntheticEvent from 'legacy-events/SyntheticEvent';
|
||||
import {IS_TARGET_PHASE_ONLY} from '../EventSystemFlags';
|
||||
|
||||
import * as DOMTopLevelEventTypes from '../DOMTopLevelEventTypes';
|
||||
import {
|
||||
topLevelEventsToDispatchConfig,
|
||||
simpleEventPluginEventTypes,
|
||||
} from '../DOMEventProperties';
|
||||
import {
|
||||
accumulateEventTargetListeners,
|
||||
accumulateTwoPhaseListeners,
|
||||
} from '../DOMModernPluginEventSystem';
|
||||
import {accumulateTwoPhaseListeners} from '../DOMModernPluginEventSystem';
|
||||
|
||||
import SyntheticAnimationEvent from '../SyntheticAnimationEvent';
|
||||
import SyntheticClipboardEvent from '../SyntheticClipboardEvent';
|
||||
@@ -42,8 +38,6 @@ import SyntheticUIEvent from '../SyntheticUIEvent';
|
||||
import SyntheticWheelEvent from '../SyntheticWheelEvent';
|
||||
import getEventCharCode from '../getEventCharCode';
|
||||
|
||||
import {enableUseEventAPI} from 'shared/ReactFeatureFlags';
|
||||
|
||||
// Only used in DEV for exhaustiveness validation.
|
||||
const knownHTMLTopLevelTypes: Array<DOMTopLevelEventType> = [
|
||||
DOMTopLevelEventTypes.TOP_ABORT,
|
||||
@@ -204,22 +198,7 @@ const SimpleEventPlugin: PluginModule<MouseEvent> = {
|
||||
nativeEventTarget,
|
||||
);
|
||||
|
||||
// For TargetEvent only accumulation, we do not traverse through
|
||||
// the React tree looking for managed React DOM elements that have
|
||||
// events. Instead we only check the EventTarget Store Map to see
|
||||
// if the container has listeners for the particular phase we're
|
||||
// interested in. This is because we attach the native event listener
|
||||
// only in the given phase.
|
||||
if (
|
||||
enableUseEventAPI &&
|
||||
eventSystemFlags !== undefined &&
|
||||
eventSystemFlags & IS_TARGET_PHASE_ONLY &&
|
||||
targetContainer != null
|
||||
) {
|
||||
accumulateEventTargetListeners(event, targetContainer);
|
||||
} else {
|
||||
accumulateTwoPhaseListeners(event, true);
|
||||
}
|
||||
accumulateTwoPhaseListeners(event);
|
||||
return event;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -19,7 +19,6 @@ import type {
|
||||
ReactEventResponderListener,
|
||||
} from 'shared/ReactTypes';
|
||||
import type {SuspenseConfig} from 'react-reconciler/src/ReactFiberSuspenseConfig';
|
||||
import type {ReactDOMListenerMap} from '../shared/ReactDOMTypes';
|
||||
|
||||
import {validateContextBounds} from './ReactPartialRendererContext';
|
||||
import {makeServerId} from '../client/ReactDOMHostConfig';
|
||||
@@ -499,13 +498,6 @@ function useOpaqueIdentifier(): OpaqueIDType {
|
||||
return makeServerId();
|
||||
}
|
||||
|
||||
function useEvent(event: any): ReactDOMListenerMap {
|
||||
return {
|
||||
clear: noop,
|
||||
setListener: noop,
|
||||
};
|
||||
}
|
||||
|
||||
function noop(): void {}
|
||||
|
||||
export let currentThreadID: ThreadID = 0;
|
||||
@@ -532,7 +524,6 @@ export const Dispatcher: DispatcherType = {
|
||||
useResponder,
|
||||
useDeferredValue,
|
||||
useTransition,
|
||||
useEvent,
|
||||
useOpaqueIdentifier,
|
||||
// Subscriptions are not setup in a server environment.
|
||||
useMutableSource,
|
||||
|
||||
-24
@@ -12,9 +12,7 @@ import type {
|
||||
ReactEventResponder,
|
||||
ReactEventResponderInstance,
|
||||
EventPriority,
|
||||
ReactScopeMethods,
|
||||
} from 'shared/ReactTypes';
|
||||
import type {DOMTopLevelEventType} from 'legacy-events/TopLevelEventTypes';
|
||||
|
||||
type AnyNativeEvent = Event | KeyboardEvent | MouseEvent | Touch;
|
||||
|
||||
@@ -77,25 +75,3 @@ export type ReactDOMResponderContext = {
|
||||
getResponderNode(): Element | null,
|
||||
...
|
||||
};
|
||||
|
||||
export type ReactDOMListenerEvent = {|
|
||||
capture: boolean,
|
||||
passive: void | boolean,
|
||||
priority: EventPriority,
|
||||
type: DOMTopLevelEventType,
|
||||
|};
|
||||
|
||||
export type ReactDOMListenerMap = {|
|
||||
clear: () => void,
|
||||
setListener: (
|
||||
target: EventTarget | ReactScopeMethods,
|
||||
callback: ?(SyntheticEvent<EventTarget>) => void,
|
||||
) => void,
|
||||
|};
|
||||
|
||||
export type ReactDOMListener = {|
|
||||
callback: (SyntheticEvent<EventTarget>) => void,
|
||||
destroy: Node => void,
|
||||
event: ReactDOMListenerEvent,
|
||||
target: EventTarget | ReactScopeMethods,
|
||||
|};
|
||||
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
export * from './src/dom/use-event/Focus';
|
||||
@@ -1,440 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
|
||||
const useEvent = ReactDOM.unstable_useEvent;
|
||||
const {useEffect, useRef} = React;
|
||||
|
||||
type UseFocusOptions = {|
|
||||
disabled?: boolean,
|
||||
onBlur?: (SyntheticEvent<EventTarget>) => void,
|
||||
onFocus?: (SyntheticEvent<EventTarget>) => void,
|
||||
onFocusChange?: boolean => void,
|
||||
onFocusVisibleChange?: boolean => void,
|
||||
|};
|
||||
|
||||
type UseFocusWithinOptions = {|
|
||||
disabled?: boolean,
|
||||
onAfterBlurWithin?: (SyntheticEvent<EventTarget>) => void,
|
||||
onBeforeBlurWithin?: (SyntheticEvent<EventTarget>) => void,
|
||||
onBlurWithin?: (SyntheticEvent<EventTarget>) => void,
|
||||
onFocusWithin?: (SyntheticEvent<EventTarget>) => void,
|
||||
onFocusWithinChange?: boolean => void,
|
||||
onFocusWithinVisibleChange?: boolean => void,
|
||||
|};
|
||||
|
||||
const isMac =
|
||||
typeof window !== 'undefined' && window.navigator != null
|
||||
? /^Mac/.test(window.navigator.platform)
|
||||
: false;
|
||||
|
||||
const canUseDOM: boolean = !!(
|
||||
typeof window !== 'undefined' &&
|
||||
typeof window.document !== 'undefined' &&
|
||||
typeof window.document.createElement !== 'undefined'
|
||||
);
|
||||
|
||||
let passiveBrowserEventsSupported = false;
|
||||
|
||||
// Check if browser support events with passive listeners
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Safely_detecting_option_support
|
||||
if (canUseDOM) {
|
||||
try {
|
||||
const options = {};
|
||||
// $FlowFixMe: Ignore Flow complaining about needing a value
|
||||
Object.defineProperty(options, 'passive', {
|
||||
get: function() {
|
||||
passiveBrowserEventsSupported = true;
|
||||
},
|
||||
});
|
||||
window.addEventListener('test', options, options);
|
||||
window.removeEventListener('test', options, options);
|
||||
} catch (e) {
|
||||
passiveBrowserEventsSupported = false;
|
||||
}
|
||||
}
|
||||
|
||||
const hasPointerEvents =
|
||||
typeof window !== 'undefined' && window.PointerEvent != null;
|
||||
|
||||
const globalFocusVisibleEvents = hasPointerEvents
|
||||
? ['keydown', 'pointermove', 'pointerdown', 'pointerup']
|
||||
: ['keydown', 'mousedown', 'touchmove', 'touchstart', 'touchend'];
|
||||
|
||||
// Global state for tracking focus visible and emulation of mouse
|
||||
let isGlobalFocusVisible = true;
|
||||
let hasTrackedGlobalFocusVisible = false;
|
||||
let isEmulatingMouseEvents = false;
|
||||
|
||||
function trackGlobalFocusVisible() {
|
||||
globalFocusVisibleEvents.forEach(type => {
|
||||
window.addEventListener(
|
||||
type,
|
||||
handleGlobalFocusVisibleEvent,
|
||||
passiveBrowserEventsSupported ? {capture: true, passive: true} : true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function handleGlobalFocusVisibleEvent(
|
||||
nativeEvent: MouseEvent | TouchEvent | KeyboardEvent,
|
||||
): void {
|
||||
const {type} = nativeEvent;
|
||||
|
||||
switch (type) {
|
||||
case 'pointermove':
|
||||
case 'pointerdown':
|
||||
case 'pointerup': {
|
||||
isGlobalFocusVisible = false;
|
||||
break;
|
||||
}
|
||||
|
||||
case 'keydown': {
|
||||
const {metaKey, altKey, ctrlKey} = nativeEvent;
|
||||
const validKey = !(metaKey || (!isMac && altKey) || ctrlKey);
|
||||
|
||||
if (validKey) {
|
||||
isGlobalFocusVisible = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// fallbacks for no PointerEvent support
|
||||
case 'touchmove':
|
||||
case 'touchstart':
|
||||
case 'touchend': {
|
||||
isEmulatingMouseEvents = true;
|
||||
isGlobalFocusVisible = false;
|
||||
break;
|
||||
}
|
||||
case 'mousedown': {
|
||||
if (!isEmulatingMouseEvents) {
|
||||
isGlobalFocusVisible = false;
|
||||
} else {
|
||||
isEmulatingMouseEvents = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const passiveObject = {passive: true};
|
||||
|
||||
function handleFocusVisibleTargetEvent(
|
||||
type: string,
|
||||
focusTarget: EventTarget,
|
||||
callback: boolean => void,
|
||||
): void {
|
||||
isGlobalFocusVisible = false;
|
||||
|
||||
// Focus should stop being visible if a pointer is used on the element
|
||||
// after it was focused using a keyboard.
|
||||
if (
|
||||
focusTarget !== null &&
|
||||
(type === 'mousedown' || type === 'touchstart' || type === 'pointerdown')
|
||||
) {
|
||||
callback(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleFocusVisibleTargetEvents(
|
||||
event: SyntheticEvent<EventTarget>,
|
||||
focusTarget,
|
||||
callback,
|
||||
): void {
|
||||
const {type} = event;
|
||||
|
||||
switch (type) {
|
||||
case 'pointermove':
|
||||
case 'pointerdown':
|
||||
case 'pointerup': {
|
||||
handleFocusVisibleTargetEvent(type, focusTarget, callback);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'keydown':
|
||||
case 'keyup': {
|
||||
const {metaKey, altKey, ctrlKey} = (event: any);
|
||||
const validKey = !(metaKey || (!isMac && altKey) || ctrlKey);
|
||||
|
||||
if (validKey) {
|
||||
if (focusTarget !== null) {
|
||||
callback(true);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// fallbacks for no PointerEvent support
|
||||
case 'touchmove':
|
||||
case 'touchstart':
|
||||
case 'touchend': {
|
||||
handleFocusVisibleTargetEvent(type, focusTarget, callback);
|
||||
break;
|
||||
}
|
||||
case 'mousedown': {
|
||||
if (!isEmulatingMouseEvents) {
|
||||
handleFocusVisibleTargetEvent(type, focusTarget, callback);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isRelatedTargetWithin(
|
||||
focusWithinTarget: Node,
|
||||
relatedTarget: null | Node,
|
||||
): boolean {
|
||||
if (relatedTarget == null) {
|
||||
return false;
|
||||
}
|
||||
// To support experimental scopes, which can be the target:
|
||||
const containsNode = (focusWithinTarget: any).containsNode;
|
||||
if (typeof containsNode === 'function') {
|
||||
return containsNode(relatedTarget);
|
||||
}
|
||||
return focusWithinTarget.contains(relatedTarget);
|
||||
}
|
||||
|
||||
function setFocusVisibleListeners(focusVisibleHandles, focusTarget, callback) {
|
||||
focusVisibleHandles.forEach(focusVisibleHandle => {
|
||||
focusVisibleHandle.setListener(focusTarget, event =>
|
||||
handleFocusVisibleTargetEvents(event, focusTarget, callback),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function useFocusVisibleInputHandles() {
|
||||
return [
|
||||
useEvent('mousedown', passiveObject),
|
||||
useEvent(hasPointerEvents ? 'pointerdown' : 'touchstart', passiveObject),
|
||||
useEvent(hasPointerEvents ? 'pointermove' : 'touchmove', passiveObject),
|
||||
useEvent(hasPointerEvents ? 'pointerup' : 'touchend', passiveObject),
|
||||
useEvent('keydown', passiveObject),
|
||||
];
|
||||
}
|
||||
|
||||
function useFocusLifecycles(stateRef) {
|
||||
useEffect(() => {
|
||||
if (!hasTrackedGlobalFocusVisible) {
|
||||
hasTrackedGlobalFocusVisible = true;
|
||||
trackGlobalFocusVisible();
|
||||
}
|
||||
}, []);
|
||||
}
|
||||
|
||||
export function useFocus(
|
||||
focusTargetRef: {current: null | Node},
|
||||
{
|
||||
disabled,
|
||||
onBlur,
|
||||
onFocus,
|
||||
onFocusChange,
|
||||
onFocusVisibleChange,
|
||||
}: UseFocusOptions,
|
||||
): void {
|
||||
// Setup controlled state for this useFocus hook
|
||||
const stateRef = useRef({isFocused: false, isFocusVisible: false});
|
||||
const focusHandle = useEvent('focus', passiveObject);
|
||||
const blurHandle = useEvent('blur', passiveObject);
|
||||
const focusVisibleHandles = useFocusVisibleInputHandles();
|
||||
|
||||
useEffect(() => {
|
||||
const focusTarget = focusTargetRef.current;
|
||||
const state = stateRef.current;
|
||||
|
||||
if (focusTarget !== null && state !== null) {
|
||||
// Handle focus visible
|
||||
setFocusVisibleListeners(
|
||||
focusVisibleHandles,
|
||||
focusTarget,
|
||||
isFocusVisible => {
|
||||
if (state.isFocused && state.isFocusVisible !== isFocusVisible) {
|
||||
state.isFocusVisible = isFocusVisible;
|
||||
if (onFocusVisibleChange) {
|
||||
onFocusVisibleChange(isFocusVisible);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Handle focus
|
||||
focusHandle.setListener(focusTarget, event => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
// Limit focus events to the direct child of the event component.
|
||||
// Browser focus is not expected to bubble.
|
||||
if (!state.isFocused && focusTarget === event.target) {
|
||||
state.isFocused = true;
|
||||
state.isFocusVisible = isGlobalFocusVisible;
|
||||
if (onFocus) {
|
||||
onFocus(event);
|
||||
}
|
||||
if (onFocusChange) {
|
||||
onFocusChange(true);
|
||||
}
|
||||
if (state.isFocusVisible && onFocusVisibleChange) {
|
||||
onFocusVisibleChange(true);
|
||||
}
|
||||
isEmulatingMouseEvents = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Handle blur
|
||||
blurHandle.setListener(focusTarget, event => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
if (state.isFocused) {
|
||||
state.isFocused = false;
|
||||
state.isFocusVisible = isGlobalFocusVisible;
|
||||
if (onBlur) {
|
||||
onBlur(event);
|
||||
}
|
||||
if (onFocusChange) {
|
||||
onFocusChange(false);
|
||||
}
|
||||
if (state.isFocusVisible && onFocusVisibleChange) {
|
||||
onFocusVisibleChange(false);
|
||||
}
|
||||
}
|
||||
isEmulatingMouseEvents = false;
|
||||
});
|
||||
}
|
||||
}, [disabled, onBlur, onFocus, onFocusChange, onFocusVisibleChange]);
|
||||
|
||||
// Mount/Unmount logic
|
||||
useFocusLifecycles(stateRef);
|
||||
}
|
||||
|
||||
export function useFocusWithin(
|
||||
focusWithinTargetRef: {current: null | Node},
|
||||
{
|
||||
disabled,
|
||||
onAfterBlurWithin,
|
||||
onBeforeBlurWithin,
|
||||
onBlurWithin,
|
||||
onFocusWithin,
|
||||
onFocusWithinChange,
|
||||
onFocusWithinVisibleChange,
|
||||
}: UseFocusWithinOptions,
|
||||
) {
|
||||
// Setup controlled state for this useFocus hook
|
||||
const stateRef = useRef({isFocused: false, isFocusVisible: false});
|
||||
const focusHandle = useEvent('focus', passiveObject);
|
||||
const blurHandle = useEvent('blur', passiveObject);
|
||||
const afterBlurHandle = useEvent('afterblur', passiveObject);
|
||||
const beforeBlurHandle = useEvent('beforeblur', passiveObject);
|
||||
const focusVisibleHandles = useFocusVisibleInputHandles();
|
||||
|
||||
useEffect(() => {
|
||||
const focusWithinTarget = focusWithinTargetRef.current;
|
||||
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);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 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(true);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
if (state.isFocusVisible && onFocusWithinVisibleChange) {
|
||||
onFocusWithinVisibleChange(false);
|
||||
}
|
||||
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,
|
||||
]);
|
||||
|
||||
// Mount/Unmount logic
|
||||
useFocusLifecycles(stateRef);
|
||||
}
|
||||
-319
@@ -1,319 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import {createEventTarget, setPointerEvent} from 'dom-event-testing-library';
|
||||
|
||||
let React;
|
||||
let ReactFeatureFlags;
|
||||
let ReactDOM;
|
||||
let useFocus;
|
||||
|
||||
function initializeModules(hasPointerEvents) {
|
||||
setPointerEvent(hasPointerEvents);
|
||||
jest.resetModules();
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.enableModernEventSystem = true;
|
||||
ReactFeatureFlags.enableUseEventAPI = true;
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
|
||||
// TODO: This import throws outside of experimental mode. Figure out better
|
||||
// strategy for gated imports.
|
||||
if (__EXPERIMENTAL__) {
|
||||
useFocus = require('react-interactions/events/focus').useFocus;
|
||||
}
|
||||
}
|
||||
|
||||
const forcePointerEvents = true;
|
||||
const table = [[forcePointerEvents], [!forcePointerEvents]];
|
||||
|
||||
describe.each(table)(`useFocus`, hasPointerEvents => {
|
||||
let container;
|
||||
|
||||
beforeEach(() => {
|
||||
initializeModules(hasPointerEvents);
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
ReactDOM.render(null, container);
|
||||
document.body.removeChild(container);
|
||||
container = null;
|
||||
});
|
||||
|
||||
describe('disabled', () => {
|
||||
let onBlur, onFocus, ref;
|
||||
|
||||
const componentInit = () => {
|
||||
onBlur = jest.fn();
|
||||
onFocus = jest.fn();
|
||||
ref = React.createRef();
|
||||
const Component = () => {
|
||||
useFocus(ref, {
|
||||
disabled: true,
|
||||
onBlur,
|
||||
onFocus,
|
||||
});
|
||||
return <div ref={ref} />;
|
||||
};
|
||||
ReactDOM.render(<Component />, container);
|
||||
};
|
||||
|
||||
// @gate experimental
|
||||
it('does not call callbacks', () => {
|
||||
componentInit();
|
||||
const target = createEventTarget(ref.current);
|
||||
target.focus();
|
||||
target.blur();
|
||||
expect(onFocus).not.toBeCalled();
|
||||
expect(onBlur).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onBlur', () => {
|
||||
let onBlur, ref;
|
||||
|
||||
const componentInit = () => {
|
||||
onBlur = jest.fn();
|
||||
ref = React.createRef();
|
||||
const Component = () => {
|
||||
useFocus(ref, {
|
||||
onBlur,
|
||||
});
|
||||
return <div ref={ref} />;
|
||||
};
|
||||
ReactDOM.render(<Component />, container);
|
||||
};
|
||||
|
||||
// @gate experimental
|
||||
it('is called after "blur" event', () => {
|
||||
componentInit();
|
||||
const target = createEventTarget(ref.current);
|
||||
target.focus();
|
||||
target.blur();
|
||||
expect(onBlur).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onFocus', () => {
|
||||
let onFocus, ref, innerRef;
|
||||
|
||||
const componentInit = () => {
|
||||
onFocus = jest.fn();
|
||||
ref = React.createRef();
|
||||
innerRef = React.createRef();
|
||||
const Component = () => {
|
||||
useFocus(ref, {
|
||||
onFocus,
|
||||
});
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<a ref={innerRef} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
ReactDOM.render(<Component />, container);
|
||||
};
|
||||
|
||||
// @gate experimental
|
||||
it('is called after "focus" event', () => {
|
||||
componentInit();
|
||||
const target = createEventTarget(ref.current);
|
||||
target.focus();
|
||||
expect(onFocus).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
it('is not called if descendants of target receive focus', () => {
|
||||
componentInit();
|
||||
const target = createEventTarget(innerRef.current);
|
||||
target.focus();
|
||||
expect(onFocus).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onFocusChange', () => {
|
||||
let onFocusChange, ref, innerRef;
|
||||
|
||||
const componentInit = () => {
|
||||
onFocusChange = jest.fn();
|
||||
ref = React.createRef();
|
||||
innerRef = React.createRef();
|
||||
const Component = () => {
|
||||
useFocus(ref, {
|
||||
onFocusChange,
|
||||
});
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<div ref={innerRef} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
ReactDOM.render(<Component />, container);
|
||||
};
|
||||
|
||||
// @gate experimental
|
||||
it('is called after "blur" and "focus" events', () => {
|
||||
componentInit();
|
||||
const target = createEventTarget(ref.current);
|
||||
target.focus();
|
||||
expect(onFocusChange).toHaveBeenCalledTimes(1);
|
||||
expect(onFocusChange).toHaveBeenCalledWith(true);
|
||||
target.blur();
|
||||
expect(onFocusChange).toHaveBeenCalledTimes(2);
|
||||
expect(onFocusChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
it('is not called after "blur" and "focus" events on descendants', () => {
|
||||
componentInit();
|
||||
const target = createEventTarget(innerRef.current);
|
||||
target.focus();
|
||||
expect(onFocusChange).toHaveBeenCalledTimes(0);
|
||||
target.blur();
|
||||
expect(onFocusChange).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onFocusVisibleChange', () => {
|
||||
let onFocusVisibleChange, ref, innerRef;
|
||||
|
||||
const componentInit = () => {
|
||||
onFocusVisibleChange = jest.fn();
|
||||
ref = React.createRef();
|
||||
innerRef = React.createRef();
|
||||
const Component = () => {
|
||||
useFocus(ref, {
|
||||
onFocusVisibleChange,
|
||||
});
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<div ref={innerRef} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
ReactDOM.render(<Component />, container);
|
||||
};
|
||||
|
||||
// @gate experimental
|
||||
it('is called after "focus" and "blur" if keyboard navigation is active', () => {
|
||||
componentInit();
|
||||
const target = createEventTarget(ref.current);
|
||||
const containerTarget = createEventTarget(container);
|
||||
// use keyboard first
|
||||
containerTarget.keydown({key: 'Tab'});
|
||||
target.focus();
|
||||
expect(onFocusVisibleChange).toHaveBeenCalledTimes(1);
|
||||
expect(onFocusVisibleChange).toHaveBeenCalledWith(true);
|
||||
target.blur({relatedTarget: container});
|
||||
expect(onFocusVisibleChange).toHaveBeenCalledTimes(2);
|
||||
expect(onFocusVisibleChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
it('is called if non-keyboard event is dispatched on target previously focused with keyboard', () => {
|
||||
componentInit();
|
||||
const target = createEventTarget(ref.current);
|
||||
const containerTarget = createEventTarget(container);
|
||||
// use keyboard first
|
||||
containerTarget.keydown({key: 'Tab'});
|
||||
target.focus();
|
||||
expect(onFocusVisibleChange).toHaveBeenCalledTimes(1);
|
||||
expect(onFocusVisibleChange).toHaveBeenCalledWith(true);
|
||||
// then use pointer on the target, focus should no longer be visible
|
||||
target.pointerdown();
|
||||
expect(onFocusVisibleChange).toHaveBeenCalledTimes(2);
|
||||
expect(onFocusVisibleChange).toHaveBeenCalledWith(false);
|
||||
// onFocusVisibleChange should not be called again
|
||||
target.blur({relatedTarget: container});
|
||||
expect(onFocusVisibleChange).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
it('is not called after "focus" and "blur" events without keyboard', () => {
|
||||
componentInit();
|
||||
const target = createEventTarget(ref.current);
|
||||
const containerTarget = createEventTarget(container);
|
||||
target.pointerdown();
|
||||
target.pointerup();
|
||||
containerTarget.pointerdown();
|
||||
target.blur({relatedTarget: container});
|
||||
expect(onFocusVisibleChange).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
it('is not called after "blur" and "focus" events on descendants', () => {
|
||||
componentInit();
|
||||
const innerTarget = createEventTarget(innerRef.current);
|
||||
const containerTarget = createEventTarget(container);
|
||||
containerTarget.keydown({key: 'Tab'});
|
||||
innerTarget.focus();
|
||||
expect(onFocusVisibleChange).toHaveBeenCalledTimes(0);
|
||||
innerTarget.blur({relatedTarget: container});
|
||||
expect(onFocusVisibleChange).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('nested Focus components', () => {
|
||||
// @gate experimental
|
||||
it('propagates events in the correct order', () => {
|
||||
const events = [];
|
||||
const innerRef = React.createRef();
|
||||
const outerRef = React.createRef();
|
||||
const createEventHandler = msg => () => {
|
||||
events.push(msg);
|
||||
};
|
||||
|
||||
const Inner = () => {
|
||||
useFocus(innerRef, {
|
||||
onBlur: createEventHandler('inner: onBlur'),
|
||||
onFocus: createEventHandler('inner: onFocus'),
|
||||
onFocusChange: createEventHandler('inner: onFocusChange'),
|
||||
});
|
||||
return <div ref={innerRef} />;
|
||||
};
|
||||
|
||||
const Outer = () => {
|
||||
useFocus(outerRef, {
|
||||
onBlur: createEventHandler('outer: onBlur'),
|
||||
onFocus: createEventHandler('outer: onFocus'),
|
||||
onFocusChange: createEventHandler('outer: onFocusChange'),
|
||||
});
|
||||
return (
|
||||
<div ref={outerRef}>
|
||||
<Inner />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ReactDOM.render(<Outer />, container);
|
||||
|
||||
const innerTarget = createEventTarget(innerRef.current);
|
||||
const outerTarget = createEventTarget(outerRef.current);
|
||||
|
||||
outerTarget.focus();
|
||||
outerTarget.blur();
|
||||
innerTarget.focus();
|
||||
innerTarget.blur();
|
||||
expect(events).toEqual([
|
||||
'outer: onFocus',
|
||||
'outer: onFocusChange',
|
||||
'outer: onBlur',
|
||||
'outer: onFocusChange',
|
||||
'inner: onFocus',
|
||||
'inner: onFocusChange',
|
||||
'inner: onBlur',
|
||||
'inner: onFocusChange',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
-569
@@ -1,569 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import {createEventTarget, setPointerEvent} from 'dom-event-testing-library';
|
||||
|
||||
let React;
|
||||
let ReactFeatureFlags;
|
||||
let ReactDOM;
|
||||
let useFocusWithin;
|
||||
let ReactTestRenderer;
|
||||
let act;
|
||||
|
||||
function initializeModules(hasPointerEvents) {
|
||||
setPointerEvent(hasPointerEvents);
|
||||
jest.resetModules();
|
||||
ReactFeatureFlags = require('shared/ReactFeatureFlags');
|
||||
ReactFeatureFlags.enableScopeAPI = true;
|
||||
ReactFeatureFlags.enableModernEventSystem = true;
|
||||
ReactFeatureFlags.enableUseEventAPI = true;
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
ReactTestRenderer = require('react-test-renderer');
|
||||
act = ReactTestRenderer.act;
|
||||
|
||||
// TODO: This import throws outside of experimental mode. Figure out better
|
||||
// strategy for gated imports.
|
||||
if (__EXPERIMENTAL__) {
|
||||
useFocusWithin = require('react-interactions/events/focus').useFocusWithin;
|
||||
}
|
||||
}
|
||||
|
||||
const forcePointerEvents = true;
|
||||
const table = [[forcePointerEvents], [!forcePointerEvents]];
|
||||
|
||||
describe.each(table)(`useFocus`, hasPointerEvents => {
|
||||
let container;
|
||||
let container2;
|
||||
|
||||
beforeEach(() => {
|
||||
initializeModules(hasPointerEvents);
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
container2 = document.createElement('div');
|
||||
document.body.appendChild(container2);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
ReactDOM.render(null, container);
|
||||
document.body.removeChild(container);
|
||||
document.body.removeChild(container2);
|
||||
container = null;
|
||||
container2 = null;
|
||||
});
|
||||
|
||||
describe('disabled', () => {
|
||||
let onFocusWithinChange, onFocusWithinVisibleChange, ref;
|
||||
|
||||
const componentInit = () => {
|
||||
onFocusWithinChange = jest.fn();
|
||||
onFocusWithinVisibleChange = jest.fn();
|
||||
ref = React.createRef();
|
||||
const Component = () => {
|
||||
useFocusWithin(ref, {
|
||||
disabled: true,
|
||||
onFocusWithinChange,
|
||||
onFocusWithinVisibleChange,
|
||||
});
|
||||
return <div ref={ref} />;
|
||||
};
|
||||
ReactDOM.render(<Component />, container);
|
||||
};
|
||||
|
||||
// @gate experimental
|
||||
it('prevents custom events being dispatched', () => {
|
||||
componentInit();
|
||||
const target = createEventTarget(ref.current);
|
||||
target.focus();
|
||||
target.blur();
|
||||
expect(onFocusWithinChange).not.toBeCalled();
|
||||
expect(onFocusWithinVisibleChange).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onFocusWithinChange', () => {
|
||||
let onFocusWithinChange, ref, innerRef, innerRef2;
|
||||
|
||||
const Component = ({show}) => {
|
||||
useFocusWithin(ref, {
|
||||
onFocusWithinChange,
|
||||
});
|
||||
return (
|
||||
<div ref={ref}>
|
||||
{show && <input ref={innerRef} />}
|
||||
<div ref={innerRef2} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const componentInit = () => {
|
||||
onFocusWithinChange = jest.fn();
|
||||
ref = React.createRef();
|
||||
innerRef = React.createRef();
|
||||
innerRef2 = React.createRef();
|
||||
ReactDOM.render(<Component show={true} />, container);
|
||||
};
|
||||
|
||||
// @gate experimental
|
||||
it('is called after "blur" and "focus" events on focus target', () => {
|
||||
componentInit();
|
||||
const target = createEventTarget(ref.current);
|
||||
target.focus();
|
||||
expect(onFocusWithinChange).toHaveBeenCalledTimes(1);
|
||||
expect(onFocusWithinChange).toHaveBeenCalledWith(true);
|
||||
target.blur({relatedTarget: container});
|
||||
expect(onFocusWithinChange).toHaveBeenCalledTimes(2);
|
||||
expect(onFocusWithinChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
it('is called after "blur" and "focus" events on descendants', () => {
|
||||
componentInit();
|
||||
const target = createEventTarget(innerRef.current);
|
||||
target.focus();
|
||||
expect(onFocusWithinChange).toHaveBeenCalledTimes(1);
|
||||
expect(onFocusWithinChange).toHaveBeenCalledWith(true);
|
||||
target.blur({relatedTarget: container});
|
||||
expect(onFocusWithinChange).toHaveBeenCalledTimes(2);
|
||||
expect(onFocusWithinChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
it('is only called once when focus moves within and outside the subtree', () => {
|
||||
componentInit();
|
||||
const node = ref.current;
|
||||
const innerNode1 = innerRef.current;
|
||||
const innerNode2 = innerRef.current;
|
||||
const target = createEventTarget(node);
|
||||
const innerTarget1 = createEventTarget(innerNode1);
|
||||
const innerTarget2 = createEventTarget(innerNode2);
|
||||
|
||||
// focus shifts into subtree
|
||||
innerTarget1.focus();
|
||||
expect(onFocusWithinChange).toHaveBeenCalledTimes(1);
|
||||
expect(onFocusWithinChange).toHaveBeenCalledWith(true);
|
||||
// focus moves around subtree
|
||||
innerTarget1.blur({relatedTarget: innerNode2});
|
||||
innerTarget2.focus();
|
||||
innerTarget2.blur({relatedTarget: node});
|
||||
target.focus();
|
||||
target.blur({relatedTarget: innerNode1});
|
||||
expect(onFocusWithinChange).toHaveBeenCalledTimes(1);
|
||||
// focus shifts outside subtree
|
||||
innerTarget1.blur({relatedTarget: container});
|
||||
expect(onFocusWithinChange).toHaveBeenCalledTimes(2);
|
||||
expect(onFocusWithinChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onFocusWithinVisibleChange', () => {
|
||||
let onFocusWithinVisibleChange, ref, innerRef, innerRef2;
|
||||
|
||||
const Component = ({show}) => {
|
||||
useFocusWithin(ref, {
|
||||
onFocusWithinVisibleChange,
|
||||
});
|
||||
return (
|
||||
<div ref={ref}>
|
||||
{show && <input ref={innerRef} />}
|
||||
<div ref={innerRef2} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const componentInit = () => {
|
||||
onFocusWithinVisibleChange = jest.fn();
|
||||
ref = React.createRef();
|
||||
innerRef = React.createRef();
|
||||
innerRef2 = React.createRef();
|
||||
ReactDOM.render(<Component show={true} />, container);
|
||||
};
|
||||
|
||||
// @gate experimental
|
||||
it('is called after "focus" and "blur" on focus target if keyboard was used', () => {
|
||||
componentInit();
|
||||
const target = createEventTarget(ref.current);
|
||||
const containerTarget = createEventTarget(container);
|
||||
// use keyboard first
|
||||
containerTarget.keydown({key: 'Tab'});
|
||||
target.focus();
|
||||
expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1);
|
||||
expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true);
|
||||
target.blur({relatedTarget: container});
|
||||
expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2);
|
||||
expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
it('is called after "focus" and "blur" on descendants if keyboard was used', () => {
|
||||
componentInit();
|
||||
const innerTarget = createEventTarget(innerRef.current);
|
||||
const containerTarget = createEventTarget(container);
|
||||
// use keyboard first
|
||||
containerTarget.keydown({key: 'Tab'});
|
||||
innerTarget.focus();
|
||||
expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1);
|
||||
expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true);
|
||||
innerTarget.blur({relatedTarget: container});
|
||||
expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2);
|
||||
expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
it('is called if non-keyboard event is dispatched on target previously focused with keyboard', () => {
|
||||
componentInit();
|
||||
const node = ref.current;
|
||||
const innerNode1 = innerRef.current;
|
||||
const innerNode2 = innerRef2.current;
|
||||
|
||||
const target = createEventTarget(node);
|
||||
const innerTarget1 = createEventTarget(innerNode1);
|
||||
const innerTarget2 = createEventTarget(innerNode2);
|
||||
// use keyboard first
|
||||
target.focus();
|
||||
target.keydown({key: 'Tab'});
|
||||
target.blur({relatedTarget: innerNode1});
|
||||
innerTarget1.focus();
|
||||
expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1);
|
||||
expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true);
|
||||
// then use pointer on the next target, focus should no longer be visible
|
||||
innerTarget2.pointerdown();
|
||||
innerTarget1.blur({relatedTarget: innerNode2});
|
||||
innerTarget2.focus();
|
||||
expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2);
|
||||
expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
|
||||
// then use keyboard again
|
||||
innerTarget2.keydown({key: 'Tab', shiftKey: true});
|
||||
innerTarget2.blur({relatedTarget: innerNode1});
|
||||
innerTarget1.focus();
|
||||
expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(3);
|
||||
expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true);
|
||||
// then use pointer on the target, focus should no longer be visible
|
||||
innerTarget1.pointerdown();
|
||||
expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(4);
|
||||
expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
|
||||
// onFocusVisibleChange should not be called again
|
||||
innerTarget1.blur({relatedTarget: container});
|
||||
expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
it('is not called after "focus" and "blur" events without keyboard', () => {
|
||||
componentInit();
|
||||
const innerTarget = createEventTarget(innerRef.current);
|
||||
innerTarget.pointerdown();
|
||||
innerTarget.pointerup();
|
||||
innerTarget.blur({relatedTarget: container});
|
||||
expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
it('is only called once when focus moves within and outside the subtree', () => {
|
||||
componentInit();
|
||||
const node = ref.current;
|
||||
const innerNode1 = innerRef.current;
|
||||
const innerNode2 = innerRef2.current;
|
||||
const target = createEventTarget(node);
|
||||
const innerTarget1 = createEventTarget(innerNode1);
|
||||
const innerTarget2 = createEventTarget(innerNode2);
|
||||
|
||||
// focus shifts into subtree
|
||||
innerTarget1.focus();
|
||||
expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1);
|
||||
expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(true);
|
||||
// focus moves around subtree
|
||||
innerTarget1.blur({relatedTarget: innerNode2});
|
||||
innerTarget2.focus();
|
||||
innerTarget2.blur({relatedTarget: node});
|
||||
target.focus();
|
||||
target.blur({relatedTarget: innerNode1});
|
||||
expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(1);
|
||||
// focus shifts outside subtree
|
||||
innerTarget1.blur({relatedTarget: container});
|
||||
expect(onFocusWithinVisibleChange).toHaveBeenCalledTimes(2);
|
||||
expect(onFocusWithinVisibleChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onBeforeBlurWithin', () => {
|
||||
let onBeforeBlurWithin, onAfterBlurWithin, ref, innerRef, innerRef2;
|
||||
|
||||
beforeEach(() => {
|
||||
onBeforeBlurWithin = jest.fn();
|
||||
onAfterBlurWithin = jest.fn(e => {
|
||||
e.persist();
|
||||
});
|
||||
ref = React.createRef();
|
||||
innerRef = React.createRef();
|
||||
innerRef2 = React.createRef();
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
it('is called after a focused element is unmounted', () => {
|
||||
const Component = ({show}) => {
|
||||
useFocusWithin(ref, {
|
||||
onBeforeBlurWithin,
|
||||
onAfterBlurWithin,
|
||||
});
|
||||
return (
|
||||
<div ref={ref}>
|
||||
{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(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
|
||||
expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
|
||||
ReactDOM.render(<Component show={false} />, container);
|
||||
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
|
||||
expect(onAfterBlurWithin).toHaveBeenCalledTimes(1);
|
||||
expect(onAfterBlurWithin).toHaveBeenCalledWith(
|
||||
expect.objectContaining({relatedTarget: inner}),
|
||||
);
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
it('is called after a nested focused element is unmounted', () => {
|
||||
const Component = ({show}) => {
|
||||
useFocusWithin(ref, {
|
||||
onBeforeBlurWithin,
|
||||
onAfterBlurWithin,
|
||||
});
|
||||
return (
|
||||
<div ref={ref}>
|
||||
{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(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
|
||||
expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
|
||||
ReactDOM.render(<Component show={false} />, container);
|
||||
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
|
||||
expect(onAfterBlurWithin).toHaveBeenCalledTimes(1);
|
||||
expect(onAfterBlurWithin).toHaveBeenCalledWith(
|
||||
expect.objectContaining({relatedTarget: inner}),
|
||||
);
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
it('is called after many elements are unmounted', () => {
|
||||
const buttonRef = React.createRef();
|
||||
const inputRef = React.createRef();
|
||||
|
||||
const Component = ({show}) => {
|
||||
useFocusWithin(ref, {
|
||||
onBeforeBlurWithin,
|
||||
onAfterBlurWithin,
|
||||
});
|
||||
return (
|
||||
<div ref={ref}>
|
||||
{show && <button>Press me!</button>}
|
||||
{show && <button>Press me!</button>}
|
||||
{show && <input ref={inputRef} />}
|
||||
{show && <button>Press me!</button>}
|
||||
{!show && <button ref={buttonRef}>Press me!</button>}
|
||||
{show && <button>Press me!</button>}
|
||||
<button>Press me!</button>
|
||||
<button>Press me!</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ReactDOM.render(<Component show={true} />, container);
|
||||
|
||||
inputRef.current.focus();
|
||||
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
|
||||
expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
|
||||
ReactDOM.render(<Component show={false} />, container);
|
||||
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
|
||||
expect(onAfterBlurWithin).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
it('is called after a nested focused element is unmounted (with scope query)', () => {
|
||||
const TestScope = React.unstable_createScope();
|
||||
const testScopeQuery = (type, props) => true;
|
||||
let targetNodes;
|
||||
let targetNode;
|
||||
|
||||
const Component = ({show}) => {
|
||||
const scopeRef = React.useRef(null);
|
||||
useFocusWithin(scopeRef, {
|
||||
onBeforeBlurWithin(event) {
|
||||
const scope = scopeRef.current;
|
||||
targetNode = innerRef.current;
|
||||
targetNodes = scope.DO_NOT_USE_queryAllNodes(testScopeQuery);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<TestScope ref={scopeRef}>
|
||||
{show && <input ref={innerRef} />}
|
||||
</TestScope>
|
||||
);
|
||||
};
|
||||
|
||||
ReactDOM.render(<Component show={true} />, container);
|
||||
|
||||
const inner = innerRef.current;
|
||||
const target = createEventTarget(inner);
|
||||
target.keydown({key: 'Tab'});
|
||||
target.focus();
|
||||
ReactDOM.render(<Component show={false} />, container);
|
||||
expect(targetNodes).toEqual([targetNode]);
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
it('is called after a focused suspended element is hidden', () => {
|
||||
const Suspense = React.Suspense;
|
||||
let suspend = false;
|
||||
let resolve;
|
||||
const promise = new Promise(resolvePromise => (resolve = resolvePromise));
|
||||
|
||||
function Child() {
|
||||
if (suspend) {
|
||||
throw promise;
|
||||
} else {
|
||||
return <input ref={innerRef} />;
|
||||
}
|
||||
}
|
||||
|
||||
const Component = ({show}) => {
|
||||
useFocusWithin(ref, {
|
||||
onBeforeBlurWithin,
|
||||
onAfterBlurWithin,
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<Suspense fallback="Loading...">
|
||||
<Child />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const root = ReactDOM.createRoot(container2);
|
||||
|
||||
act(() => {
|
||||
root.render(<Component />);
|
||||
});
|
||||
jest.runAllTimers();
|
||||
expect(container2.innerHTML).toBe('<div><input></div>');
|
||||
|
||||
const inner = innerRef.current;
|
||||
const target = createEventTarget(inner);
|
||||
target.keydown({key: 'Tab'});
|
||||
target.focus();
|
||||
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
|
||||
expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
|
||||
|
||||
suspend = true;
|
||||
act(() => {
|
||||
root.render(<Component />);
|
||||
});
|
||||
jest.runAllTimers();
|
||||
expect(container2.innerHTML).toBe(
|
||||
'<div><input style="display: none;">Loading...</div>',
|
||||
);
|
||||
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
|
||||
expect(onAfterBlurWithin).toHaveBeenCalledTimes(1);
|
||||
resolve();
|
||||
});
|
||||
|
||||
// @gate experimental
|
||||
it('is called after a focused suspended element is hidden then shown', () => {
|
||||
const Suspense = React.Suspense;
|
||||
let suspend = false;
|
||||
let resolve;
|
||||
const promise = new Promise(resolvePromise => (resolve = resolvePromise));
|
||||
const buttonRef = React.createRef();
|
||||
|
||||
function Child() {
|
||||
if (suspend) {
|
||||
throw promise;
|
||||
} else {
|
||||
return <input ref={innerRef} />;
|
||||
}
|
||||
}
|
||||
|
||||
const Component = ({show}) => {
|
||||
useFocusWithin(ref, {
|
||||
onBeforeBlurWithin,
|
||||
onAfterBlurWithin,
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
<Suspense fallback={<button ref={buttonRef}>Loading...</button>}>
|
||||
<Child />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const root = ReactDOM.createRoot(container2);
|
||||
|
||||
act(() => {
|
||||
root.render(<Component />);
|
||||
});
|
||||
jest.runAllTimers();
|
||||
|
||||
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
|
||||
expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
|
||||
|
||||
suspend = true;
|
||||
act(() => {
|
||||
root.render(<Component />);
|
||||
});
|
||||
jest.runAllTimers();
|
||||
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
|
||||
expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
|
||||
|
||||
act(() => {
|
||||
root.render(<Component />);
|
||||
});
|
||||
jest.runAllTimers();
|
||||
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(0);
|
||||
expect(onAfterBlurWithin).toHaveBeenCalledTimes(0);
|
||||
|
||||
buttonRef.current.focus();
|
||||
suspend = false;
|
||||
act(() => {
|
||||
root.render(<Component />);
|
||||
});
|
||||
jest.runAllTimers();
|
||||
expect(onBeforeBlurWithin).toHaveBeenCalledTimes(1);
|
||||
expect(onAfterBlurWithin).toHaveBeenCalledTimes(1);
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -77,9 +77,6 @@ export type UpdatePayload = Object;
|
||||
export type TimeoutHandle = TimeoutID;
|
||||
export type NoTimeout = -1;
|
||||
|
||||
export type ReactListenerEvent = Object;
|
||||
export type ReactListenerMap = Object;
|
||||
export type ReactListener = Object;
|
||||
export type OpaqueIDType = void;
|
||||
|
||||
export type RendererInspectionConfig = $ReadOnly<{|
|
||||
@@ -510,22 +507,6 @@ export function makeServerId(): OpaqueIDType {
|
||||
throw new Error('Not yet implemented');
|
||||
}
|
||||
|
||||
export function registerEvent(event: any, rootContainerInstance: Container) {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
export function mountEventListener(listener: any) {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
export function unmountEventListener(listener: any) {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
export function validateEventListenerTarget(target: any, listener: any) {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
export function beforeActiveInstanceBlur() {
|
||||
// noop
|
||||
}
|
||||
|
||||
@@ -28,10 +28,6 @@ import ReactNativeFiberHostComponent from './ReactNativeFiberHostComponent';
|
||||
|
||||
const {get: getViewConfigForType} = ReactNativeViewConfigRegistry;
|
||||
|
||||
export type ReactListenerEvent = Object;
|
||||
export type ReactListenerMap = Object;
|
||||
export type ReactListener = Object;
|
||||
|
||||
export type Type = string;
|
||||
export type Props = Object;
|
||||
export type Container = number;
|
||||
@@ -559,22 +555,6 @@ export function makeServerId(): OpaqueIDType {
|
||||
throw new Error('Not yet implemented');
|
||||
}
|
||||
|
||||
export function registerEvent(event: any, rootContainerInstance: Container) {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
export function mountEventListener(listener: any) {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
export function unmountEventListener(listener: any) {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
export function validateEventListenerTarget(target: any, listener: any) {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
export function beforeActiveInstanceBlur() {
|
||||
// noop
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@ import {
|
||||
enableSuspenseCallback,
|
||||
enableScopeAPI,
|
||||
runAllPassiveEffectDestroysBeforeCreates,
|
||||
enableUseEventAPI,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
FunctionComponent,
|
||||
@@ -1020,9 +1019,7 @@ function commitUnmount(
|
||||
if (enableDeprecatedFlareAPI) {
|
||||
unmountDeprecatedResponderListeners(current);
|
||||
}
|
||||
if (enableDeprecatedFlareAPI || enableUseEventAPI) {
|
||||
beforeRemoveInstance(current.stateNode);
|
||||
}
|
||||
beforeRemoveInstance(current.stateNode);
|
||||
safelyDetachRef(current);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -36,7 +36,6 @@ import {
|
||||
enableSuspenseCallback,
|
||||
enableScopeAPI,
|
||||
runAllPassiveEffectDestroysBeforeCreates,
|
||||
enableUseEventAPI,
|
||||
} from 'shared/ReactFeatureFlags';
|
||||
import {
|
||||
FunctionComponent,
|
||||
@@ -1020,9 +1019,7 @@ function commitUnmount(
|
||||
if (enableDeprecatedFlareAPI) {
|
||||
unmountDeprecatedResponderListeners(current);
|
||||
}
|
||||
if (enableDeprecatedFlareAPI || enableUseEventAPI) {
|
||||
beforeRemoveInstance(current.stateNode);
|
||||
}
|
||||
beforeRemoveInstance(current.stateNode);
|
||||
safelyDetachRef(current);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import type {
|
||||
ReactEventResponder,
|
||||
ReactContext,
|
||||
ReactEventResponderListener,
|
||||
ReactScopeMethods,
|
||||
} from 'shared/ReactTypes';
|
||||
import type {Fiber, Dispatcher} from './ReactInternalTypes';
|
||||
import type {ExpirationTime} from './ReactFiberExpirationTime.new';
|
||||
@@ -22,15 +21,10 @@ import type {HookEffectTag} from './ReactHookEffectTags';
|
||||
import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
|
||||
import type {ReactPriorityLevel} from './ReactInternalTypes';
|
||||
import type {FiberRoot} from './ReactInternalTypes';
|
||||
import type {
|
||||
OpaqueIDType,
|
||||
ReactListenerEvent,
|
||||
ReactListenerMap,
|
||||
ReactListener,
|
||||
} from './ReactFiberHostConfig';
|
||||
import type {OpaqueIDType} from './ReactFiberHostConfig';
|
||||
|
||||
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
||||
import {enableDebugTracing, enableUseEventAPI} from 'shared/ReactFeatureFlags';
|
||||
import {enableDebugTracing} from 'shared/ReactFeatureFlags';
|
||||
|
||||
import {markRootExpiredAtTime} from './ReactFiberRoot.new';
|
||||
import {
|
||||
@@ -46,7 +40,6 @@ import {
|
||||
Passive as PassiveEffect,
|
||||
} from './ReactSideEffectTags';
|
||||
import {
|
||||
NoEffect as NoHookEffect,
|
||||
HasEffect as HookHasEffect,
|
||||
Layout as HookLayout,
|
||||
Passive as HookPassive,
|
||||
@@ -63,12 +56,6 @@ import {
|
||||
markUnprocessedUpdateTime,
|
||||
priorityLevelToLabel,
|
||||
} from './ReactFiberWorkLoop.new';
|
||||
import {
|
||||
registerEvent,
|
||||
mountEventListener as mountHostEventListener,
|
||||
unmountEventListener as unmountHostEventListener,
|
||||
validateEventListenerTarget,
|
||||
} from './ReactFiberHostConfig';
|
||||
|
||||
import invariant from 'shared/invariant';
|
||||
import getComponentName from 'shared/getComponentName';
|
||||
@@ -95,7 +82,6 @@ import {
|
||||
setWorkInProgressVersion,
|
||||
warnAboutMultipleRenderersDEV,
|
||||
} from './ReactMutableSource.new';
|
||||
import {getRootHostContainer} from './ReactFiberHostContext.new';
|
||||
import {getIsRendering} from './ReactCurrentFiber';
|
||||
import {logStateUpdateScheduled} from './DebugTracing';
|
||||
|
||||
@@ -136,7 +122,6 @@ export type HookType =
|
||||
| 'useDeferredValue'
|
||||
| 'useTransition'
|
||||
| 'useMutableSource'
|
||||
| 'useEvent'
|
||||
| 'useOpaqueIdentifier';
|
||||
|
||||
let didWarnAboutMismatchedHooksForComponent;
|
||||
@@ -1760,158 +1745,6 @@ function dispatchAction<S, A>(
|
||||
}
|
||||
}
|
||||
|
||||
const noOpMount = () => {};
|
||||
|
||||
function validateNotInFunctionRender(): boolean {
|
||||
if (currentlyRenderingFiber === null) {
|
||||
return true;
|
||||
}
|
||||
if (__DEV__) {
|
||||
console.warn(
|
||||
'Event listener methods from useEvent() cannot be called during render.' +
|
||||
' These methods should be called in an effect or event callback outside the render.',
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function createReactListener(
|
||||
event: ReactListenerEvent,
|
||||
callback: (SyntheticEvent<EventTarget>) => void,
|
||||
target: EventTarget | ReactScopeMethods,
|
||||
destroy: Node => void,
|
||||
): ReactListener {
|
||||
return {
|
||||
callback,
|
||||
destroy,
|
||||
event,
|
||||
target,
|
||||
};
|
||||
}
|
||||
|
||||
function mountEventListener(event: ReactListenerEvent): ReactListenerMap {
|
||||
if (enableUseEventAPI) {
|
||||
const hook = mountWorkInProgressHook();
|
||||
const listenerMap: Map<
|
||||
EventTarget | ReactScopeMethods,
|
||||
ReactListener,
|
||||
> = new Map();
|
||||
const rootContainerInstance = getRootHostContainer();
|
||||
|
||||
// Register the event to the current root to ensure event
|
||||
// replaying can pick up the event ahead of time.
|
||||
registerEvent(event, rootContainerInstance);
|
||||
|
||||
const clear = () => {
|
||||
if (validateNotInFunctionRender()) {
|
||||
const listeners = Array.from(listenerMap.values());
|
||||
for (let i = 0; i < listeners.length; i++) {
|
||||
unmountHostEventListener(listeners[i]);
|
||||
}
|
||||
listenerMap.clear();
|
||||
}
|
||||
};
|
||||
|
||||
const destroy = (target: Node) => {
|
||||
// We don't need to call detachListenerFromInstance
|
||||
// here as this method should only ever be called
|
||||
// from renderers that need to remove the instance
|
||||
// from the map representing an instance that still
|
||||
// holds a reference to the listenerMap. This means
|
||||
// things like "window" listeners on ReactDOM should
|
||||
// never enter this call path as the the instance in
|
||||
// those cases would be that of "window", which
|
||||
// should be handled via an optimized route in the
|
||||
// renderer, making less overhead here. If we change
|
||||
// this heuristic we should update this path to make
|
||||
// sure we call detachListenerFromInstance.
|
||||
listenerMap.delete(target);
|
||||
};
|
||||
|
||||
const reactListenerMap: ReactListenerMap = {
|
||||
clear,
|
||||
setListener(
|
||||
target: EventTarget | ReactScopeMethods,
|
||||
callback: ?(SyntheticEvent<EventTarget>) => void,
|
||||
): void {
|
||||
if (
|
||||
validateNotInFunctionRender() &&
|
||||
validateEventListenerTarget(target, callback)
|
||||
) {
|
||||
let listener = listenerMap.get(target);
|
||||
if (listener === undefined) {
|
||||
if (callback == null) {
|
||||
return;
|
||||
}
|
||||
listener = createReactListener(event, callback, target, destroy);
|
||||
listenerMap.set(target, listener);
|
||||
} else {
|
||||
if (callback == null) {
|
||||
listenerMap.delete(target);
|
||||
unmountHostEventListener(listener);
|
||||
return;
|
||||
}
|
||||
listener.callback = callback;
|
||||
}
|
||||
mountHostEventListener(listener);
|
||||
}
|
||||
},
|
||||
};
|
||||
// In order to clear up upon the hook unmounting,
|
||||
// we ensure we set the effecrt tag so that we visit
|
||||
// this effect in the commit phase, so we can handle
|
||||
// clean-up accordingly.
|
||||
currentlyRenderingFiber.effectTag |= UpdateEffect;
|
||||
pushEffect(NoHookEffect, noOpMount, clear, null);
|
||||
hook.memoizedState = [reactListenerMap, event, clear];
|
||||
return reactListenerMap;
|
||||
}
|
||||
// To make Flow not complain
|
||||
return (undefined: any);
|
||||
}
|
||||
|
||||
function updateEventListener(event: ReactListenerEvent): ReactListenerMap {
|
||||
if (enableUseEventAPI) {
|
||||
const hook = updateWorkInProgressHook();
|
||||
const [reactListenerMap, memoizedEvent, clear] = hook.memoizedState;
|
||||
if (__DEV__) {
|
||||
if (memoizedEvent.type !== event.type) {
|
||||
console.warn(
|
||||
'The event type argument passed to the useEvent() hook was different between renders.' +
|
||||
' The event type is static and should never change between renders.',
|
||||
);
|
||||
}
|
||||
if (memoizedEvent.capture !== event.capture) {
|
||||
console.warn(
|
||||
'The "capture" option passed to the useEvent() hook was different between renders.' +
|
||||
' The "capture" option is static and should never change between renders.',
|
||||
);
|
||||
}
|
||||
if (memoizedEvent.priority !== event.priority) {
|
||||
console.warn(
|
||||
'The "priority" option passed to the useEvent() hook was different between renders.' +
|
||||
' The "priority" option is static and should never change between renders.',
|
||||
);
|
||||
}
|
||||
if (memoizedEvent.passive !== event.passive) {
|
||||
console.warn(
|
||||
'The "passive" option passed to the useEvent() hook was different between renders.' +
|
||||
' The "passive" option is static and should never change between renders.',
|
||||
);
|
||||
}
|
||||
}
|
||||
// In order to clear up upon the hook unmounting,
|
||||
// we ensure we set the effecrt tag so that we visit
|
||||
// this effect in the commit phase, so we can handle
|
||||
// clean-up accordingly.
|
||||
currentlyRenderingFiber.effectTag |= UpdateEffect;
|
||||
pushEffect(NoHookEffect, noOpMount, clear, null);
|
||||
return reactListenerMap;
|
||||
}
|
||||
// To make Flow not complain
|
||||
return (undefined: any);
|
||||
}
|
||||
|
||||
export const ContextOnlyDispatcher: Dispatcher = {
|
||||
readContext,
|
||||
|
||||
@@ -1929,7 +1762,6 @@ export const ContextOnlyDispatcher: Dispatcher = {
|
||||
useDeferredValue: throwInvalidHookError,
|
||||
useTransition: throwInvalidHookError,
|
||||
useMutableSource: throwInvalidHookError,
|
||||
useEvent: throwInvalidHookError,
|
||||
useOpaqueIdentifier: throwInvalidHookError,
|
||||
};
|
||||
|
||||
@@ -1950,7 +1782,6 @@ const HooksDispatcherOnMount: Dispatcher = {
|
||||
useDeferredValue: mountDeferredValue,
|
||||
useTransition: mountTransition,
|
||||
useMutableSource: mountMutableSource,
|
||||
useEvent: mountEventListener,
|
||||
useOpaqueIdentifier: mountOpaqueIdentifier,
|
||||
};
|
||||
|
||||
@@ -1971,7 +1802,6 @@ const HooksDispatcherOnUpdate: Dispatcher = {
|
||||
useDeferredValue: updateDeferredValue,
|
||||
useTransition: updateTransition,
|
||||
useMutableSource: updateMutableSource,
|
||||
useEvent: updateEventListener,
|
||||
useOpaqueIdentifier: updateOpaqueIdentifier,
|
||||
};
|
||||
|
||||
@@ -1992,7 +1822,6 @@ const HooksDispatcherOnRerender: Dispatcher = {
|
||||
useDeferredValue: rerenderDeferredValue,
|
||||
useTransition: rerenderTransition,
|
||||
useMutableSource: updateMutableSource,
|
||||
useEvent: updateEventListener,
|
||||
useOpaqueIdentifier: rerenderOpaqueIdentifier,
|
||||
};
|
||||
|
||||
@@ -2151,11 +1980,6 @@ if (__DEV__) {
|
||||
mountHookTypesDev();
|
||||
return mountMutableSource(source, getSnapshot, subscribe);
|
||||
},
|
||||
useEvent(event: ReactListenerEvent): ReactListenerMap {
|
||||
currentHookNameInDev = 'useEvent';
|
||||
mountHookTypesDev();
|
||||
return mountEventListener(event);
|
||||
},
|
||||
useOpaqueIdentifier(): OpaqueIDType | void {
|
||||
currentHookNameInDev = 'useOpaqueIdentifier';
|
||||
mountHookTypesDev();
|
||||
@@ -2286,11 +2110,6 @@ if (__DEV__) {
|
||||
updateHookTypesDev();
|
||||
return mountMutableSource(source, getSnapshot, subscribe);
|
||||
},
|
||||
useEvent(event: ReactListenerEvent): ReactListenerMap {
|
||||
currentHookNameInDev = 'useEvent';
|
||||
updateHookTypesDev();
|
||||
return mountEventListener(event);
|
||||
},
|
||||
useOpaqueIdentifier(): OpaqueIDType | void {
|
||||
currentHookNameInDev = 'useOpaqueIdentifier';
|
||||
updateHookTypesDev();
|
||||
@@ -2421,11 +2240,6 @@ if (__DEV__) {
|
||||
updateHookTypesDev();
|
||||
return updateMutableSource(source, getSnapshot, subscribe);
|
||||
},
|
||||
useEvent(event: ReactListenerEvent): ReactListenerMap {
|
||||
currentHookNameInDev = 'useEvent';
|
||||
updateHookTypesDev();
|
||||
return updateEventListener(event);
|
||||
},
|
||||
useOpaqueIdentifier(): OpaqueIDType | void {
|
||||
currentHookNameInDev = 'useOpaqueIdentifier';
|
||||
updateHookTypesDev();
|
||||
@@ -2557,11 +2371,6 @@ if (__DEV__) {
|
||||
updateHookTypesDev();
|
||||
return updateMutableSource(source, getSnapshot, subscribe);
|
||||
},
|
||||
useEvent(event: ReactListenerEvent): ReactListenerMap {
|
||||
currentHookNameInDev = 'useEvent';
|
||||
updateHookTypesDev();
|
||||
return updateEventListener(event);
|
||||
},
|
||||
useOpaqueIdentifier(): OpaqueIDType | void {
|
||||
currentHookNameInDev = 'useOpaqueIdentifier';
|
||||
updateHookTypesDev();
|
||||
@@ -2707,12 +2516,6 @@ if (__DEV__) {
|
||||
mountHookTypesDev();
|
||||
return mountMutableSource(source, getSnapshot, subscribe);
|
||||
},
|
||||
useEvent(event: ReactListenerEvent): ReactListenerMap {
|
||||
currentHookNameInDev = 'useEvent';
|
||||
warnInvalidHookAccess();
|
||||
mountHookTypesDev();
|
||||
return mountEventListener(event);
|
||||
},
|
||||
useOpaqueIdentifier(): OpaqueIDType | void {
|
||||
currentHookNameInDev = 'useOpaqueIdentifier';
|
||||
warnInvalidHookAccess();
|
||||
@@ -2859,12 +2662,6 @@ if (__DEV__) {
|
||||
updateHookTypesDev();
|
||||
return updateMutableSource(source, getSnapshot, subscribe);
|
||||
},
|
||||
useEvent(event: ReactListenerEvent): ReactListenerMap {
|
||||
currentHookNameInDev = 'useEvent';
|
||||
warnInvalidHookAccess();
|
||||
updateHookTypesDev();
|
||||
return updateEventListener(event);
|
||||
},
|
||||
useOpaqueIdentifier(): OpaqueIDType | void {
|
||||
currentHookNameInDev = 'useOpaqueIdentifier';
|
||||
warnInvalidHookAccess();
|
||||
@@ -3012,12 +2809,6 @@ if (__DEV__) {
|
||||
updateHookTypesDev();
|
||||
return updateMutableSource(source, getSnapshot, subscribe);
|
||||
},
|
||||
useEvent(event: ReactListenerEvent): ReactListenerMap {
|
||||
currentHookNameInDev = 'useEvent';
|
||||
warnInvalidHookAccess();
|
||||
updateHookTypesDev();
|
||||
return updateEventListener(event);
|
||||
},
|
||||
useOpaqueIdentifier(): OpaqueIDType | void {
|
||||
currentHookNameInDev = 'useOpaqueIdentifier';
|
||||
warnInvalidHookAccess();
|
||||
|
||||
@@ -14,7 +14,6 @@ import type {
|
||||
ReactEventResponder,
|
||||
ReactContext,
|
||||
ReactEventResponderListener,
|
||||
ReactScopeMethods,
|
||||
} from 'shared/ReactTypes';
|
||||
import type {Fiber, Dispatcher} from './ReactInternalTypes';
|
||||
import type {ExpirationTime} from './ReactFiberExpirationTime.old';
|
||||
@@ -22,15 +21,10 @@ import type {HookEffectTag} from './ReactHookEffectTags';
|
||||
import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
|
||||
import type {ReactPriorityLevel} from './ReactInternalTypes';
|
||||
import type {FiberRoot} from './ReactInternalTypes';
|
||||
import type {
|
||||
OpaqueIDType,
|
||||
ReactListenerEvent,
|
||||
ReactListenerMap,
|
||||
ReactListener,
|
||||
} from './ReactFiberHostConfig';
|
||||
import type {OpaqueIDType} from './ReactFiberHostConfig';
|
||||
|
||||
import ReactSharedInternals from 'shared/ReactSharedInternals';
|
||||
import {enableDebugTracing, enableUseEventAPI} from 'shared/ReactFeatureFlags';
|
||||
import {enableDebugTracing} from 'shared/ReactFeatureFlags';
|
||||
|
||||
import {markRootExpiredAtTime} from './ReactFiberRoot.old';
|
||||
import {NoMode, BlockingMode, DebugTracingMode} from './ReactTypeOfMode';
|
||||
@@ -46,7 +40,6 @@ import {
|
||||
Passive as PassiveEffect,
|
||||
} from './ReactSideEffectTags';
|
||||
import {
|
||||
NoEffect as NoHookEffect,
|
||||
HasEffect as HookHasEffect,
|
||||
Layout as HookLayout,
|
||||
Passive as HookPassive,
|
||||
@@ -63,12 +56,6 @@ import {
|
||||
markUnprocessedUpdateTime,
|
||||
priorityLevelToLabel,
|
||||
} from './ReactFiberWorkLoop.old';
|
||||
import {
|
||||
registerEvent,
|
||||
mountEventListener as mountHostEventListener,
|
||||
unmountEventListener as unmountHostEventListener,
|
||||
validateEventListenerTarget,
|
||||
} from './ReactFiberHostConfig';
|
||||
|
||||
import invariant from 'shared/invariant';
|
||||
import getComponentName from 'shared/getComponentName';
|
||||
@@ -95,7 +82,6 @@ import {
|
||||
setWorkInProgressVersion,
|
||||
warnAboutMultipleRenderersDEV,
|
||||
} from './ReactMutableSource.old';
|
||||
import {getRootHostContainer} from './ReactFiberHostContext.old';
|
||||
import {getIsRendering} from './ReactCurrentFiber';
|
||||
import {logStateUpdateScheduled} from './DebugTracing';
|
||||
|
||||
@@ -133,7 +119,6 @@ export type HookType =
|
||||
| 'useDeferredValue'
|
||||
| 'useTransition'
|
||||
| 'useMutableSource'
|
||||
| 'useEvent'
|
||||
| 'useOpaqueIdentifier';
|
||||
|
||||
let didWarnAboutMismatchedHooksForComponent;
|
||||
@@ -1755,158 +1740,6 @@ function dispatchAction<S, A>(
|
||||
}
|
||||
}
|
||||
|
||||
const noOpMount = () => {};
|
||||
|
||||
function validateNotInFunctionRender(): boolean {
|
||||
if (currentlyRenderingFiber === null) {
|
||||
return true;
|
||||
}
|
||||
if (__DEV__) {
|
||||
console.warn(
|
||||
'Event listener methods from useEvent() cannot be called during render.' +
|
||||
' These methods should be called in an effect or event callback outside the render.',
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function createReactListener(
|
||||
event: ReactListenerEvent,
|
||||
callback: (SyntheticEvent<EventTarget>) => void,
|
||||
target: EventTarget | ReactScopeMethods,
|
||||
destroy: Node => void,
|
||||
): ReactListener {
|
||||
return {
|
||||
callback,
|
||||
destroy,
|
||||
event,
|
||||
target,
|
||||
};
|
||||
}
|
||||
|
||||
function mountEventListener(event: ReactListenerEvent): ReactListenerMap {
|
||||
if (enableUseEventAPI) {
|
||||
const hook = mountWorkInProgressHook();
|
||||
const listenerMap: Map<
|
||||
EventTarget | ReactScopeMethods,
|
||||
ReactListener,
|
||||
> = new Map();
|
||||
const rootContainerInstance = getRootHostContainer();
|
||||
|
||||
// Register the event to the current root to ensure event
|
||||
// replaying can pick up the event ahead of time.
|
||||
registerEvent(event, rootContainerInstance);
|
||||
|
||||
const clear = () => {
|
||||
if (validateNotInFunctionRender()) {
|
||||
const listeners = Array.from(listenerMap.values());
|
||||
for (let i = 0; i < listeners.length; i++) {
|
||||
unmountHostEventListener(listeners[i]);
|
||||
}
|
||||
listenerMap.clear();
|
||||
}
|
||||
};
|
||||
|
||||
const destroy = (target: Node) => {
|
||||
// We don't need to call detachListenerFromInstance
|
||||
// here as this method should only ever be called
|
||||
// from renderers that need to remove the instance
|
||||
// from the map representing an instance that still
|
||||
// holds a reference to the listenerMap. This means
|
||||
// things like "window" listeners on ReactDOM should
|
||||
// never enter this call path as the the instance in
|
||||
// those cases would be that of "window", which
|
||||
// should be handled via an optimized route in the
|
||||
// renderer, making less overhead here. If we change
|
||||
// this heuristic we should update this path to make
|
||||
// sure we call detachListenerFromInstance.
|
||||
listenerMap.delete(target);
|
||||
};
|
||||
|
||||
const reactListenerMap: ReactListenerMap = {
|
||||
clear,
|
||||
setListener(
|
||||
target: EventTarget | ReactScopeMethods,
|
||||
callback: ?(SyntheticEvent<EventTarget>) => void,
|
||||
): void {
|
||||
if (
|
||||
validateNotInFunctionRender() &&
|
||||
validateEventListenerTarget(target, callback)
|
||||
) {
|
||||
let listener = listenerMap.get(target);
|
||||
if (listener === undefined) {
|
||||
if (callback == null) {
|
||||
return;
|
||||
}
|
||||
listener = createReactListener(event, callback, target, destroy);
|
||||
listenerMap.set(target, listener);
|
||||
} else {
|
||||
if (callback == null) {
|
||||
listenerMap.delete(target);
|
||||
unmountHostEventListener(listener);
|
||||
return;
|
||||
}
|
||||
listener.callback = callback;
|
||||
}
|
||||
mountHostEventListener(listener);
|
||||
}
|
||||
},
|
||||
};
|
||||
// In order to clear up upon the hook unmounting,
|
||||
// we ensure we set the effecrt tag so that we visit
|
||||
// this effect in the commit phase, so we can handle
|
||||
// clean-up accordingly.
|
||||
currentlyRenderingFiber.effectTag |= UpdateEffect;
|
||||
pushEffect(NoHookEffect, noOpMount, clear, null);
|
||||
hook.memoizedState = [reactListenerMap, event, clear];
|
||||
return reactListenerMap;
|
||||
}
|
||||
// To make Flow not complain
|
||||
return (undefined: any);
|
||||
}
|
||||
|
||||
function updateEventListener(event: ReactListenerEvent): ReactListenerMap {
|
||||
if (enableUseEventAPI) {
|
||||
const hook = updateWorkInProgressHook();
|
||||
const [reactListenerMap, memoizedEvent, clear] = hook.memoizedState;
|
||||
if (__DEV__) {
|
||||
if (memoizedEvent.type !== event.type) {
|
||||
console.warn(
|
||||
'The event type argument passed to the useEvent() hook was different between renders.' +
|
||||
' The event type is static and should never change between renders.',
|
||||
);
|
||||
}
|
||||
if (memoizedEvent.capture !== event.capture) {
|
||||
console.warn(
|
||||
'The "capture" option passed to the useEvent() hook was different between renders.' +
|
||||
' The "capture" option is static and should never change between renders.',
|
||||
);
|
||||
}
|
||||
if (memoizedEvent.priority !== event.priority) {
|
||||
console.warn(
|
||||
'The "priority" option passed to the useEvent() hook was different between renders.' +
|
||||
' The "priority" option is static and should never change between renders.',
|
||||
);
|
||||
}
|
||||
if (memoizedEvent.passive !== event.passive) {
|
||||
console.warn(
|
||||
'The "passive" option passed to the useEvent() hook was different between renders.' +
|
||||
' The "passive" option is static and should never change between renders.',
|
||||
);
|
||||
}
|
||||
}
|
||||
// In order to clear up upon the hook unmounting,
|
||||
// we ensure we set the effecrt tag so that we visit
|
||||
// this effect in the commit phase, so we can handle
|
||||
// clean-up accordingly.
|
||||
currentlyRenderingFiber.effectTag |= UpdateEffect;
|
||||
pushEffect(NoHookEffect, noOpMount, clear, null);
|
||||
return reactListenerMap;
|
||||
}
|
||||
// To make Flow not complain
|
||||
return (undefined: any);
|
||||
}
|
||||
|
||||
export const ContextOnlyDispatcher: Dispatcher = {
|
||||
readContext,
|
||||
|
||||
@@ -1924,7 +1757,6 @@ export const ContextOnlyDispatcher: Dispatcher = {
|
||||
useDeferredValue: throwInvalidHookError,
|
||||
useTransition: throwInvalidHookError,
|
||||
useMutableSource: throwInvalidHookError,
|
||||
useEvent: throwInvalidHookError,
|
||||
useOpaqueIdentifier: throwInvalidHookError,
|
||||
};
|
||||
|
||||
@@ -1945,7 +1777,6 @@ const HooksDispatcherOnMount: Dispatcher = {
|
||||
useDeferredValue: mountDeferredValue,
|
||||
useTransition: mountTransition,
|
||||
useMutableSource: mountMutableSource,
|
||||
useEvent: mountEventListener,
|
||||
useOpaqueIdentifier: mountOpaqueIdentifier,
|
||||
};
|
||||
|
||||
@@ -1966,7 +1797,6 @@ const HooksDispatcherOnUpdate: Dispatcher = {
|
||||
useDeferredValue: updateDeferredValue,
|
||||
useTransition: updateTransition,
|
||||
useMutableSource: updateMutableSource,
|
||||
useEvent: updateEventListener,
|
||||
useOpaqueIdentifier: updateOpaqueIdentifier,
|
||||
};
|
||||
|
||||
@@ -1987,7 +1817,6 @@ const HooksDispatcherOnRerender: Dispatcher = {
|
||||
useDeferredValue: rerenderDeferredValue,
|
||||
useTransition: rerenderTransition,
|
||||
useMutableSource: updateMutableSource,
|
||||
useEvent: updateEventListener,
|
||||
useOpaqueIdentifier: rerenderOpaqueIdentifier,
|
||||
};
|
||||
|
||||
@@ -2146,11 +1975,6 @@ if (__DEV__) {
|
||||
mountHookTypesDev();
|
||||
return mountMutableSource(source, getSnapshot, subscribe);
|
||||
},
|
||||
useEvent(event: ReactListenerEvent): ReactListenerMap {
|
||||
currentHookNameInDev = 'useEvent';
|
||||
mountHookTypesDev();
|
||||
return mountEventListener(event);
|
||||
},
|
||||
useOpaqueIdentifier(): OpaqueIDType | void {
|
||||
currentHookNameInDev = 'useOpaqueIdentifier';
|
||||
mountHookTypesDev();
|
||||
@@ -2281,11 +2105,6 @@ if (__DEV__) {
|
||||
updateHookTypesDev();
|
||||
return mountMutableSource(source, getSnapshot, subscribe);
|
||||
},
|
||||
useEvent(event: ReactListenerEvent): ReactListenerMap {
|
||||
currentHookNameInDev = 'useEvent';
|
||||
updateHookTypesDev();
|
||||
return mountEventListener(event);
|
||||
},
|
||||
useOpaqueIdentifier(): OpaqueIDType | void {
|
||||
currentHookNameInDev = 'useOpaqueIdentifier';
|
||||
updateHookTypesDev();
|
||||
@@ -2416,11 +2235,6 @@ if (__DEV__) {
|
||||
updateHookTypesDev();
|
||||
return updateMutableSource(source, getSnapshot, subscribe);
|
||||
},
|
||||
useEvent(event: ReactListenerEvent): ReactListenerMap {
|
||||
currentHookNameInDev = 'useEvent';
|
||||
updateHookTypesDev();
|
||||
return updateEventListener(event);
|
||||
},
|
||||
useOpaqueIdentifier(): OpaqueIDType | void {
|
||||
currentHookNameInDev = 'useOpaqueIdentifier';
|
||||
updateHookTypesDev();
|
||||
@@ -2552,11 +2366,6 @@ if (__DEV__) {
|
||||
updateHookTypesDev();
|
||||
return updateMutableSource(source, getSnapshot, subscribe);
|
||||
},
|
||||
useEvent(event: ReactListenerEvent): ReactListenerMap {
|
||||
currentHookNameInDev = 'useEvent';
|
||||
updateHookTypesDev();
|
||||
return updateEventListener(event);
|
||||
},
|
||||
useOpaqueIdentifier(): OpaqueIDType | void {
|
||||
currentHookNameInDev = 'useOpaqueIdentifier';
|
||||
updateHookTypesDev();
|
||||
@@ -2702,12 +2511,6 @@ if (__DEV__) {
|
||||
mountHookTypesDev();
|
||||
return mountMutableSource(source, getSnapshot, subscribe);
|
||||
},
|
||||
useEvent(event: ReactListenerEvent): ReactListenerMap {
|
||||
currentHookNameInDev = 'useEvent';
|
||||
warnInvalidHookAccess();
|
||||
mountHookTypesDev();
|
||||
return mountEventListener(event);
|
||||
},
|
||||
useOpaqueIdentifier(): OpaqueIDType | void {
|
||||
currentHookNameInDev = 'useOpaqueIdentifier';
|
||||
warnInvalidHookAccess();
|
||||
@@ -2854,12 +2657,6 @@ if (__DEV__) {
|
||||
updateHookTypesDev();
|
||||
return updateMutableSource(source, getSnapshot, subscribe);
|
||||
},
|
||||
useEvent(event: ReactListenerEvent): ReactListenerMap {
|
||||
currentHookNameInDev = 'useEvent';
|
||||
warnInvalidHookAccess();
|
||||
updateHookTypesDev();
|
||||
return updateEventListener(event);
|
||||
},
|
||||
useOpaqueIdentifier(): OpaqueIDType | void {
|
||||
currentHookNameInDev = 'useOpaqueIdentifier';
|
||||
warnInvalidHookAccess();
|
||||
@@ -3007,12 +2804,6 @@ if (__DEV__) {
|
||||
updateHookTypesDev();
|
||||
return updateMutableSource(source, getSnapshot, subscribe);
|
||||
},
|
||||
useEvent(event: ReactListenerEvent): ReactListenerMap {
|
||||
currentHookNameInDev = 'useEvent';
|
||||
warnInvalidHookAccess();
|
||||
updateHookTypesDev();
|
||||
return updateEventListener(event);
|
||||
},
|
||||
useOpaqueIdentifier(): OpaqueIDType | void {
|
||||
currentHookNameInDev = 'useOpaqueIdentifier';
|
||||
warnInvalidHookAccess();
|
||||
|
||||
+2
-9
@@ -18,10 +18,7 @@ import type {
|
||||
MutableSourceGetSnapshotFn,
|
||||
MutableSource,
|
||||
} from 'shared/ReactTypes';
|
||||
import type {
|
||||
SuspenseInstance,
|
||||
ReactListenerEvent,
|
||||
} from './ReactFiberHostConfig';
|
||||
import type {SuspenseInstance} from './ReactFiberHostConfig';
|
||||
import type {WorkTag} from './ReactWorkTags';
|
||||
import type {TypeOfMode} from './ReactTypeOfMode';
|
||||
import type {SideEffectTag} from './ReactSideEffectTags';
|
||||
@@ -32,10 +29,7 @@ import type {RootTag} from './ReactRootTags';
|
||||
import type {TimeoutHandle, NoTimeout} from './ReactFiberHostConfig';
|
||||
import type {Wakeable} from 'shared/ReactTypes';
|
||||
import type {Interaction} from 'scheduler/src/Tracing';
|
||||
import type {
|
||||
OpaqueIDType,
|
||||
ReactListenerMap,
|
||||
} from 'react-reconciler/src/ReactFiberHostConfig';
|
||||
import type {OpaqueIDType} from 'react-reconciler/src/ReactFiberHostConfig';
|
||||
import type {SuspenseConfig, TimeoutConfig} from './ReactFiberSuspenseConfig';
|
||||
|
||||
export type ReactPriorityLevel = 99 | 98 | 97 | 96 | 95 | 90;
|
||||
@@ -325,6 +319,5 @@ export type Dispatcher = {|
|
||||
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
|
||||
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
|
||||
): Snapshot,
|
||||
useEvent(event: ReactListenerEvent): ReactListenerMap,
|
||||
useOpaqueIdentifier(): OpaqueIDType | void,
|
||||
|};
|
||||
|
||||
@@ -40,9 +40,6 @@ export opaque type NoTimeout = mixed; // eslint-disable-line no-undef
|
||||
export opaque type RendererInspectionConfig = mixed; // eslint-disable-line no-undef
|
||||
export opaque type OpaqueIDType = mixed;
|
||||
export type EventResponder = any;
|
||||
export type ReactListenerEvent = Object;
|
||||
export type ReactListenerMap = Object;
|
||||
export type ReactListener = Object;
|
||||
|
||||
export const getPublicInstance = $$$hostConfig.getPublicInstance;
|
||||
export const getRootHostContext = $$$hostConfig.getRootHostContext;
|
||||
@@ -78,11 +75,6 @@ export const shouldUpdateFundamentalComponent =
|
||||
$$$hostConfig.shouldUpdateFundamentalComponent;
|
||||
export const getInstanceFromNode = $$$hostConfig.getInstanceFromNode;
|
||||
export const beforeRemoveInstance = $$$hostConfig.beforeRemoveInstance;
|
||||
export const registerEvent = $$$hostConfig.registerEvent;
|
||||
export const mountEventListener = $$$hostConfig.mountEventListener;
|
||||
export const unmountEventListener = $$$hostConfig.unmountEventListener;
|
||||
export const validateEventListenerTarget =
|
||||
$$$hostConfig.validateEventListenerTarget;
|
||||
export const isOpaqueHydratingObject = $$$hostConfig.isOpaqueHydratingObject;
|
||||
export const makeOpaqueHydratingObject =
|
||||
$$$hostConfig.makeOpaqueHydratingObject;
|
||||
|
||||
@@ -52,9 +52,6 @@ export opaque type OpaqueIDType =
|
||||
valueOf: () => string | void,
|
||||
};
|
||||
|
||||
export type ReactListenerEvent = Object;
|
||||
export type ReactListenerMap = Object;
|
||||
export type ReactListener = Object;
|
||||
export type RendererInspectionConfig = $ReadOnly<{||}>;
|
||||
|
||||
export * from 'react-reconciler/src/ReactFiberHostConfigWithNoPersistence';
|
||||
@@ -431,22 +428,6 @@ export function makeOpaqueHydratingObject(
|
||||
};
|
||||
}
|
||||
|
||||
export function registerEvent(event: any, rootContainerInstance: Container) {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
export function mountEventListener(listener: any) {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
export function unmountEventListener(listener: any) {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
export function validateEventListenerTarget(target: any, listener: any) {
|
||||
throw new Error('Not yet implemented.');
|
||||
}
|
||||
|
||||
export function beforeActiveInstanceBlur() {
|
||||
// noop
|
||||
}
|
||||
|
||||
@@ -57,9 +57,6 @@ export const enableFundamentalAPI = false;
|
||||
// Experimental Scope support.
|
||||
export const enableScopeAPI = false;
|
||||
|
||||
// Experimental useEvent support.
|
||||
export const enableUseEventAPI = false;
|
||||
|
||||
// New API for JSX transforms to target - https://github.com/reactjs/rfcs/pull/107
|
||||
|
||||
// We will enforce mocking scheduler with scheduler/unstable_mock at some point. (v17?)
|
||||
|
||||
@@ -27,7 +27,6 @@ export const warnAboutDeprecatedLifecycles = true;
|
||||
export const enableDeprecatedFlareAPI = false;
|
||||
export const enableFundamentalAPI = false;
|
||||
export const enableScopeAPI = false;
|
||||
export const enableUseEventAPI = false;
|
||||
export const warnAboutUnmockedScheduler = true;
|
||||
export const enableSuspenseCallback = false;
|
||||
export const warnAboutDefaultPropsOnFunctionComponents = false;
|
||||
|
||||
@@ -26,7 +26,6 @@ export const enableSchedulerDebugging = false;
|
||||
export const enableDeprecatedFlareAPI = false;
|
||||
export const enableFundamentalAPI = false;
|
||||
export const enableScopeAPI = false;
|
||||
export const enableUseEventAPI = false;
|
||||
export const warnAboutUnmockedScheduler = false;
|
||||
export const enableSuspenseCallback = false;
|
||||
export const warnAboutDefaultPropsOnFunctionComponents = false;
|
||||
|
||||
@@ -26,7 +26,6 @@ export const enableSchedulerDebugging = false;
|
||||
export const enableDeprecatedFlareAPI = false;
|
||||
export const enableFundamentalAPI = false;
|
||||
export const enableScopeAPI = false;
|
||||
export const enableUseEventAPI = false;
|
||||
export const warnAboutUnmockedScheduler = false;
|
||||
export const enableSuspenseCallback = false;
|
||||
export const warnAboutDefaultPropsOnFunctionComponents = false;
|
||||
|
||||
@@ -26,7 +26,6 @@ export const disableInputAttributeSyncing = false;
|
||||
export const enableDeprecatedFlareAPI = true;
|
||||
export const enableFundamentalAPI = false;
|
||||
export const enableScopeAPI = true;
|
||||
export const enableUseEventAPI = true;
|
||||
export const warnAboutUnmockedScheduler = true;
|
||||
export const enableSuspenseCallback = true;
|
||||
export const warnAboutDefaultPropsOnFunctionComponents = false;
|
||||
|
||||
@@ -26,7 +26,6 @@ export const enableSchedulerDebugging = false;
|
||||
export const enableDeprecatedFlareAPI = false;
|
||||
export const enableFundamentalAPI = false;
|
||||
export const enableScopeAPI = false;
|
||||
export const enableUseEventAPI = false;
|
||||
export const warnAboutUnmockedScheduler = false;
|
||||
export const enableSuspenseCallback = false;
|
||||
export const warnAboutDefaultPropsOnFunctionComponents = false;
|
||||
|
||||
@@ -26,7 +26,6 @@ export const enableSchedulerDebugging = false;
|
||||
export const enableDeprecatedFlareAPI = true;
|
||||
export const enableFundamentalAPI = false;
|
||||
export const enableScopeAPI = true;
|
||||
export const enableUseEventAPI = true;
|
||||
export const warnAboutUnmockedScheduler = true;
|
||||
export const enableSuspenseCallback = true;
|
||||
export const warnAboutDefaultPropsOnFunctionComponents = false;
|
||||
|
||||
@@ -60,8 +60,6 @@ export const enableFundamentalAPI = false;
|
||||
|
||||
export const enableScopeAPI = true;
|
||||
|
||||
export const enableUseEventAPI = true;
|
||||
|
||||
export const warnAboutUnmockedScheduler = true;
|
||||
|
||||
export const enableSuspenseCallback = true;
|
||||
|
||||
Reference in New Issue
Block a user