[Events] Make passiveness and priority non-configurable (#19807)

This commit is contained in:
Dan Abramov 2020-09-14 13:54:08 +01:00 committed by GitHub
parent ebb2253428
commit 11ee82df45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 161 additions and 480 deletions

View File

@ -20,7 +20,6 @@ import type {
SuspenseInstance,
Props,
} from './ReactDOMHostConfig';
import type {DOMEventName} from '../events/DOMEventNames';
import {
HostComponent,
@ -44,16 +43,6 @@ const internalEventHandlersKey = '__reactEvents$' + randomKey;
const internalEventHandlerListenersKey = '__reactListeners$' + randomKey;
const internalEventHandlesSetKey = '__reactHandles$' + randomKey;
export type ElementListenerMap = Map<
DOMEventName | string,
ElementListenerMapEntry | null,
>;
export type ElementListenerMapEntry = {
passive: void | boolean,
listener: any => void,
};
export function precacheFiberNode(
hostInst: Fiber,
node: Instance | TextInstance | SuspenseInstance | ReactScopeInstance,
@ -207,12 +196,12 @@ export function updateFiberProps(
(node: any)[internalPropsKey] = props;
}
export function getEventListenerMap(node: EventTarget): ElementListenerMap {
let elementListenerMap = (node: any)[internalEventHandlersKey];
if (elementListenerMap === undefined) {
elementListenerMap = (node: any)[internalEventHandlersKey] = new Map();
export function getEventListenerSet(node: EventTarget): Set<string> {
let elementListenerSet = (node: any)[internalEventHandlersKey];
if (elementListenerSet === undefined) {
elementListenerSet = (node: any)[internalEventHandlersKey] = new Set();
}
return elementListenerMap;
return elementListenerSet;
}
export function getFiberFromScopeInstance(

View File

@ -8,13 +8,12 @@
*/
import type {DOMEventName} from '../events/DOMEventNames';
import type {EventPriority, ReactScopeInstance} from 'shared/ReactTypes';
import type {ReactScopeInstance} from 'shared/ReactTypes';
import type {
ReactDOMEventHandle,
ReactDOMEventHandleListener,
} from '../shared/ReactDOMTypes';
import {getEventPriorityForListenerSystem} from '../events/DOMEventProperties';
import {allNativeEvents} from '../events/EventRegistry';
import {
getClosestInstanceFromNode,
@ -25,10 +24,7 @@ import {
addEventHandleToTarget,
} from './ReactDOMComponentTree';
import {ELEMENT_NODE, COMMENT_NODE} from '../shared/HTMLNodeType';
import {
listenToNativeEvent,
addEventTypeToDispatchConfig,
} from '../events/DOMPluginEventSystem';
import {listenToNativeEvent} from '../events/DOMPluginEventSystem';
import {HostRoot, HostPortal} from 'react-reconciler/src/ReactWorkTags';
import {IS_EVENT_HANDLE_NON_MANAGED_NODE} from '../events/EventSystemFlags';
@ -42,8 +38,6 @@ import invariant from 'shared/invariant';
type EventHandleOptions = {|
capture?: boolean,
passive?: boolean,
priority?: EventPriority,
|};
function getNearestRootOrPortalContainer(node: Fiber): null | Element {
@ -82,76 +76,76 @@ function createEventHandleListener(
function registerEventOnNearestTargetContainer(
targetFiber: Fiber,
domEventName: DOMEventName,
isPassiveListener: boolean | void,
listenerPriority: EventPriority | void,
isCapturePhaseListener: boolean,
targetElement: Element | null,
): void {
// If it is, find the nearest root or portal and make it
// our event handle target container.
let targetContainer = getNearestRootOrPortalContainer(targetFiber);
if (targetContainer === null) {
invariant(
false,
'ReactDOM.createEventHandle: setListener called on an target ' +
'that did not have a corresponding root. This is likely a bug in React.',
if (!enableEagerRootListeners) {
// If it is, find the nearest root or portal and make it
// our event handle target container.
let targetContainer = getNearestRootOrPortalContainer(targetFiber);
if (targetContainer === null) {
if (__DEV__) {
console.error(
'ReactDOM.createEventHandle: setListener called on an target ' +
'that did not have a corresponding root. This is likely a bug in React.',
);
}
return;
}
if (targetContainer.nodeType === COMMENT_NODE) {
targetContainer = ((targetContainer.parentNode: any): Element);
}
listenToNativeEvent(
domEventName,
isCapturePhaseListener,
targetContainer,
targetElement,
);
}
if (targetContainer.nodeType === COMMENT_NODE) {
targetContainer = ((targetContainer.parentNode: any): Element);
}
listenToNativeEvent(
domEventName,
isCapturePhaseListener,
targetContainer,
targetElement,
isPassiveListener,
listenerPriority,
);
}
function registerReactDOMEvent(
target: EventTarget | ReactScopeInstance,
domEventName: DOMEventName,
isPassiveListener: boolean | void,
isCapturePhaseListener: boolean,
listenerPriority: EventPriority | void,
): void {
// Check if the target is a DOM element.
if ((target: any).nodeType === ELEMENT_NODE) {
const targetElement = ((target: any): Element);
// Check if the DOM element is managed by React.
const targetFiber = getClosestInstanceFromNode(targetElement);
if (targetFiber === null) {
invariant(
false,
'ReactDOM.createEventHandle: setListener called on an element ' +
'target that is not managed by React. Ensure React rendered the DOM element.',
if (!enableEagerRootListeners) {
const targetElement = ((target: any): Element);
// Check if the DOM element is managed by React.
const targetFiber = getClosestInstanceFromNode(targetElement);
if (targetFiber === null) {
if (__DEV__) {
console.error(
'ReactDOM.createEventHandle: setListener called on an element ' +
'target that is not managed by React. Ensure React rendered the DOM element.',
);
}
return;
}
registerEventOnNearestTargetContainer(
targetFiber,
domEventName,
isCapturePhaseListener,
targetElement,
);
}
registerEventOnNearestTargetContainer(
targetFiber,
domEventName,
isPassiveListener,
listenerPriority,
isCapturePhaseListener,
targetElement,
);
} else if (enableScopeAPI && isReactScope(target)) {
const scopeTarget = ((target: any): ReactScopeInstance);
const targetFiber = getFiberFromScopeInstance(scopeTarget);
if (targetFiber === null) {
// Scope is unmounted, do not proceed.
return;
if (!enableEagerRootListeners) {
const scopeTarget = ((target: any): ReactScopeInstance);
const targetFiber = getFiberFromScopeInstance(scopeTarget);
if (targetFiber === null) {
// Scope is unmounted, do not proceed.
return;
}
registerEventOnNearestTargetContainer(
targetFiber,
domEventName,
isCapturePhaseListener,
null,
);
}
registerEventOnNearestTargetContainer(
targetFiber,
domEventName,
isPassiveListener,
listenerPriority,
isCapturePhaseListener,
null,
);
} else if (isValidEventTarget(target)) {
const eventTarget = ((target: any): EventTarget);
// These are valid event targets, but they are also
@ -161,8 +155,6 @@ function registerReactDOMEvent(
isCapturePhaseListener,
eventTarget,
null,
isPassiveListener,
listenerPriority,
IS_EVENT_HANDLE_NON_MANAGED_NODE,
);
} else {
@ -181,46 +173,27 @@ export function createEventHandle(
if (enableCreateEventHandleAPI) {
const domEventName = ((type: any): DOMEventName);
if (enableEagerRootListeners) {
// We cannot support arbitrary native events with eager root listeners
// because the eager strategy relies on knowing the whole list ahead of time.
// If we wanted to support this, we'd have to add code to keep track
// (or search) for all portal and root containers, and lazily add listeners
// to them whenever we see a previously unknown event. This seems like a lot
// of complexity for something we don't even have a particular use case for.
// Unfortunately, the downside of this invariant is that *removing* a native
// event from the list of known events has now become a breaking change for
// any code relying on the createEventHandle API.
invariant(
allNativeEvents.has(domEventName) ||
domEventName === 'beforeblur' ||
domEventName === 'afterblur',
'Cannot call unstable_createEventHandle with "%s", as it is not an event known to React.',
domEventName,
);
}
// We cannot support arbitrary native events with eager root listeners
// because the eager strategy relies on knowing the whole list ahead of time.
// If we wanted to support this, we'd have to add code to keep track
// (or search) for all portal and root containers, and lazily add listeners
// to them whenever we see a previously unknown event. This seems like a lot
// of complexity for something we don't even have a particular use case for.
// Unfortunately, the downside of this invariant is that *removing* a native
// event from the list of known events has now become a breaking change for
// any code relying on the createEventHandle API.
invariant(
allNativeEvents.has(domEventName),
'Cannot call unstable_createEventHandle with "%s", as it is not an event known to React.',
domEventName,
);
let isCapturePhaseListener = false;
let isPassiveListener = undefined; // Undefined means to use the browser default
let listenerPriority;
if (options != null) {
const optionsCapture = options.capture;
const optionsPassive = options.passive;
const optionsPriority = options.priority;
if (typeof optionsCapture === 'boolean') {
isCapturePhaseListener = optionsCapture;
}
if (typeof optionsPassive === 'boolean') {
isPassiveListener = optionsPassive;
}
if (typeof optionsPriority === 'number') {
listenerPriority = optionsPriority;
}
}
if (listenerPriority === undefined) {
listenerPriority = getEventPriorityForListenerSystem(domEventName);
}
const eventHandle = (
@ -234,15 +207,7 @@ export function createEventHandle(
);
if (!doesTargetHaveEventHandle(target, eventHandle)) {
addEventHandleToTarget(target, eventHandle);
registerReactDOMEvent(
target,
domEventName,
isPassiveListener,
isCapturePhaseListener,
listenerPriority,
);
// Add the event to our known event types list.
addEventTypeToDispatchConfig(domEventName);
registerReactDOMEvent(target, domEventName, isCapturePhaseListener);
}
const listener = createEventHandleListener(
domEventName,

View File

@ -89,6 +89,10 @@ const otherDiscreteEvents: Array<DOMEventName> = [
];
if (enableCreateEventHandleAPI) {
// Special case: these two events don't have on* React handler
// and are only accessible via the createEventHandle API.
topLevelEventsToReactNames.set('beforeblur', null);
topLevelEventsToReactNames.set('afterblur', null);
otherDiscreteEvents.push('beforeblur', 'afterblur');
}
@ -202,7 +206,7 @@ export function getEventPriorityForListenerSystem(
if (__DEV__) {
console.warn(
'The event "%s" provided to createEventHandle() does not have a known priority type.' +
' It is recommended to provide a "priority" option to specify a priority.',
' This is likely a bug in React.',
type,
);
}

View File

@ -19,8 +19,6 @@ import type {
KnownReactSyntheticEvent,
ReactSyntheticEvent,
} from './ReactSyntheticEventType';
import type {ElementListenerMapEntry} from '../client/ReactDOMComponentTree';
import type {EventPriority} from 'shared/ReactTypes';
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
import {registrationNameDependencies, allNativeEvents} from './EventRegistry';
@ -41,7 +39,7 @@ import {
import getEventTarget from './getEventTarget';
import {
getClosestInstanceFromNode,
getEventListenerMap,
getEventListenerSet,
getEventHandlerListeners,
} from '../client/ReactDOMComponentTree';
import {COMMENT_NODE} from '../shared/HTMLNodeType';
@ -69,7 +67,6 @@ import {
addEventBubbleListenerWithPassiveFlag,
addEventCaptureListenerWithPassiveFlag,
} from './EventListener';
import {topLevelEventsToReactNames} from './DOMEventProperties';
import * as BeforeInputEventPlugin from './plugins/BeforeInputEventPlugin';
import * as ChangeEventPlugin from './plugins/ChangeEventPlugin';
import * as EnterLeaveEventPlugin from './plugins/EnterLeaveEventPlugin';
@ -296,36 +293,24 @@ function dispatchEventsForPlugins(
processDispatchQueue(dispatchQueue, eventSystemFlags);
}
function shouldUpgradeListener(
listenerEntry: void | ElementListenerMapEntry,
passive: void | boolean,
): boolean {
return (
listenerEntry !== undefined && listenerEntry.passive === true && !passive
);
}
export function listenToNonDelegatedEvent(
domEventName: DOMEventName,
targetElement: Element,
): void {
const isCapturePhaseListener = false;
const listenerMap = getEventListenerMap(targetElement);
const listenerMapKey = getListenerMapKey(
const listenerSet = getEventListenerSet(targetElement);
const listenerSetKey = getListenerSetKey(
domEventName,
isCapturePhaseListener,
);
const listenerEntry = ((listenerMap.get(
listenerMapKey,
): any): ElementListenerMapEntry | void);
if (listenerEntry === undefined) {
const listener = addTrappedEventListener(
if (!listenerSet.has(listenerSetKey)) {
addTrappedEventListener(
targetElement,
domEventName,
IS_NON_DELEGATED,
isCapturePhaseListener,
);
listenerMap.set(listenerMapKey, {passive: false, listener});
listenerSet.add(listenerSetKey);
}
}
@ -369,8 +354,6 @@ export function listenToNativeEvent(
isCapturePhaseListener: boolean,
rootContainerElement: EventTarget,
targetElement: Element | null,
isPassiveListener?: boolean,
listenerPriority?: EventPriority,
eventSystemFlags?: EventSystemFlags = 0,
): void {
let target = rootContainerElement;
@ -384,21 +367,6 @@ export function listenToNativeEvent(
) {
target = (rootContainerElement: any).ownerDocument;
}
if (enablePassiveEventIntervention && isPassiveListener === undefined) {
// Browsers introduced an intervention, making these events
// passive by default on document. React doesn't bind them
// to document anymore, but changing this now would undo
// the performance wins from the change. So we emulate
// the existing behavior manually on the roots now.
// https://github.com/facebook/react/issues/19651
if (
domEventName === 'touchstart' ||
domEventName === 'touchmove' ||
domEventName === 'wheel'
) {
isPassiveListener = true;
}
}
// If the event can be delegated (or is capture phase), we can
// register it to the root container. Otherwise, we should
// register the event to the target element and mark it as
@ -423,42 +391,24 @@ export function listenToNativeEvent(
eventSystemFlags |= IS_NON_DELEGATED;
target = targetElement;
}
const listenerMap = getEventListenerMap(target);
const listenerMapKey = getListenerMapKey(
const listenerSet = getEventListenerSet(target);
const listenerSetKey = getListenerSetKey(
domEventName,
isCapturePhaseListener,
);
const listenerEntry = ((listenerMap.get(
listenerMapKey,
): any): ElementListenerMapEntry | void);
const shouldUpgrade = shouldUpgradeListener(listenerEntry, isPassiveListener);
// If the listener entry is empty or we should upgrade, then
// we need to trap an event listener onto the target.
if (listenerEntry === undefined || shouldUpgrade) {
// If we should upgrade, then we need to remove the existing trapped
// event listener for the target container.
if (shouldUpgrade) {
removeEventListener(
target,
domEventName,
((listenerEntry: any): ElementListenerMapEntry).listener,
isCapturePhaseListener,
);
}
if (!listenerSet.has(listenerSetKey)) {
if (isCapturePhaseListener) {
eventSystemFlags |= IS_CAPTURE_PHASE;
}
const listener = addTrappedEventListener(
addTrappedEventListener(
target,
domEventName,
eventSystemFlags,
isCapturePhaseListener,
false,
isPassiveListener,
listenerPriority,
);
listenerMap.set(listenerMapKey, {passive: isPassiveListener, listener});
listenerSet.add(listenerSetKey);
}
}
@ -480,11 +430,13 @@ export function listenToReactEvent(
const isPolyfillEventPlugin = dependenciesLength !== 1;
if (isPolyfillEventPlugin) {
const listenerMap = getEventListenerMap(rootContainerElement);
// For optimization, we register plugins on the listener map, so we
// don't need to check each of their dependencies each time.
if (!listenerMap.has(reactEvent)) {
listenerMap.set(reactEvent, null);
const listenerSet = getEventListenerSet(rootContainerElement);
// When eager listeners are off, this Set has a dual purpose: it both
// captures which native listeners we registered (e.g. "click__bubble")
// and *React* lazy listeners (e.g. "onClick") so we don't do extra checks.
// This second usage does not exist in the eager mode.
if (!listenerSet.has(reactEvent)) {
listenerSet.add(reactEvent);
for (let i = 0; i < dependenciesLength; i++) {
listenToNativeEvent(
dependencies[i],
@ -520,19 +472,29 @@ function addTrappedEventListener(
eventSystemFlags: EventSystemFlags,
isCapturePhaseListener: boolean,
isDeferredListenerForLegacyFBSupport?: boolean,
isPassiveListener?: boolean,
listenerPriority?: EventPriority,
): any => void {
) {
let listener = createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags,
listenerPriority,
);
// If passive option is not supported, then the event will be
// active and not passive.
if (isPassiveListener === true && !passiveBrowserEventsSupported) {
isPassiveListener = false;
let isPassiveListener = undefined;
if (enablePassiveEventIntervention && passiveBrowserEventsSupported) {
// Browsers introduced an intervention, making these events
// passive by default on document. React doesn't bind them
// to document anymore, but changing this now would undo
// the performance wins from the change. So we emulate
// the existing behavior manually on the roots now.
// https://github.com/facebook/react/issues/19651
if (
domEventName === 'touchstart' ||
domEventName === 'touchmove' ||
domEventName === 'wheel'
) {
isPassiveListener = true;
}
}
targetContainer =
@ -564,6 +526,7 @@ function addTrappedEventListener(
return originalListener.apply(this, p);
};
}
// TODO: There are too many combinations here. Consolidate them.
if (isCapturePhaseListener) {
if (isPassiveListener !== undefined) {
unsubscribeListener = addEventCaptureListenerWithPassiveFlag(
@ -595,7 +558,6 @@ function addTrappedEventListener(
);
}
}
return unsubscribeListener;
}
function deferClickToDocumentForLegacyFBSupport(
@ -1085,19 +1047,7 @@ export function accumulateEventHandleNonManagedNodeListeners(
}
}
export function addEventTypeToDispatchConfig(type: DOMEventName): void {
const reactName = topLevelEventsToReactNames.get(type);
// If we don't have a reactName, 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 (reactName === undefined) {
topLevelEventsToReactNames.set(type, null);
}
}
export function getListenerMapKey(
export function getListenerSetKey(
domEventName: DOMEventName,
capture: boolean,
): string {

View File

@ -9,10 +9,15 @@
import type {DOMEventName} from './DOMEventNames';
import {enableEagerRootListeners} from 'shared/ReactFeatureFlags';
import {enableCreateEventHandleAPI} from 'shared/ReactFeatureFlags';
export const allNativeEvents: Set<DOMEventName> = new Set();
if (enableCreateEventHandleAPI) {
allNativeEvents.add('beforeblur');
allNativeEvents.add('afterblur');
}
/**
* Mapping from registration name to event name
*/
@ -60,9 +65,7 @@ export function registerDirectEvent(
}
}
if (enableEagerRootListeners) {
for (let i = 0; i < dependencies.length; i++) {
allNativeEvents.add(dependencies[i]);
}
for (let i = 0; i < dependencies.length; i++) {
allNativeEvents.add(dependencies[i]);
}
}

View File

@ -8,7 +8,6 @@
*/
import type {AnyNativeEvent} from '../events/PluginModuleType';
import type {EventPriority} from 'shared/ReactTypes';
import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
import type {Container, SuspenseInstance} from '../client/ReactDOMHostConfig';
import type {DOMEventName} from '../events/DOMEventNames';
@ -96,12 +95,8 @@ export function createEventListenerWrapperWithPriority(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
priority?: EventPriority,
): Function {
const eventPriority =
priority === undefined
? getEventPriorityForPluginSystem(domEventName)
: priority;
const eventPriority = getEventPriorityForPluginSystem(domEventName);
let listenerWrapper;
switch (eventPriority) {
case DiscreteEvent:

View File

@ -1949,106 +1949,6 @@ describe('DOMPluginEventSystem', () => {
expect(log).toEqual([{counter: 1}]);
});
// @gate experimental
it('should correctly work for a basic "click" listener that upgrades', () => {
const clickEvent = jest.fn();
const buttonRef = React.createRef();
const button2Ref = React.createRef();
const setClick1 = ReactDOM.unstable_createEventHandle('click', {
passive: false,
});
const setClick2 = ReactDOM.unstable_createEventHandle('click', {
passive: true,
});
function Test2() {
React.useEffect(() => {
return setClick1(button2Ref.current, clickEvent);
});
return <button ref={button2Ref}>Click me!</button>;
}
function Test({extra}) {
React.useEffect(() => {
return setClick2(buttonRef.current, clickEvent);
});
return (
<>
<button ref={buttonRef}>Click me!</button>
{extra && <Test2 />}
</>
);
}
ReactDOM.render(<Test />, container);
Scheduler.unstable_flushAll();
let button = buttonRef.current;
dispatchClickEvent(button);
expect(clickEvent).toHaveBeenCalledTimes(1);
ReactDOM.render(<Test extra={true} />, container);
Scheduler.unstable_flushAll();
clickEvent.mockClear();
button = button2Ref.current;
dispatchClickEvent(button);
expect(clickEvent).toHaveBeenCalledTimes(1);
});
// @gate experimental
it('should correctly work for a basic "click" listener that upgrades #2', () => {
const clickEvent = jest.fn();
const buttonRef = React.createRef();
const button2Ref = React.createRef();
const setClick1 = ReactDOM.unstable_createEventHandle('click', {
passive: false,
});
const setClick2 = ReactDOM.unstable_createEventHandle('click', {
passive: undefined,
});
function Test2() {
React.useEffect(() => {
return setClick1(button2Ref.current, clickEvent);
});
return <button ref={button2Ref}>Click me!</button>;
}
function Test({extra}) {
React.useEffect(() => {
return setClick2(buttonRef.current, clickEvent);
});
return (
<>
<button ref={buttonRef}>Click me!</button>
{extra && <Test2 />}
</>
);
}
ReactDOM.render(<Test />, container);
Scheduler.unstable_flushAll();
let button = buttonRef.current;
dispatchClickEvent(button);
expect(clickEvent).toHaveBeenCalledTimes(1);
ReactDOM.render(<Test extra={true} />, container);
Scheduler.unstable_flushAll();
clickEvent.mockClear();
button = button2Ref.current;
dispatchClickEvent(button);
expect(clickEvent).toHaveBeenCalledTimes(1);
});
// @gate experimental
it('should correctly work for a basic "click" window listener', () => {
const log = [];
@ -2391,109 +2291,15 @@ describe('DOMPluginEventSystem', () => {
});
// @gate experimental
it('handles propagation of custom user events', () => {
const buttonRef = React.createRef();
const divRef = React.createRef();
const log = [];
const onCustomEvent = jest.fn(e =>
log.push(['bubble', e.currentTarget]),
it('does not support custom user events', () => {
// With eager listeners, supporting custom events via this API doesn't make sense
// because we can't know a full list of them ahead of time. Let's check we throw
// since otherwise we'd end up with inconsistent behavior, like no portal bubbling.
expect(() => {
ReactDOM.unstable_createEventHandle('custom-event');
}).toThrow(
'Cannot call unstable_createEventHandle with "custom-event", as it is not an event known to React.',
);
const onCustomEventCapture = jest.fn(e =>
log.push(['capture', e.currentTarget]),
);
let setCustomEventHandle;
if (gate(flags => flags.enableEagerRootListeners)) {
// With eager listeners, supporting custom events via this API doesn't make sense
// because we can't know a full list of them ahead of time. Let's check we throw
// since otherwise we'd end up with inconsistent behavior, like no portal bubbling.
expect(() => {
setCustomEventHandle = ReactDOM.unstable_createEventHandle(
'custom-event',
);
}).toThrow(
'Cannot call unstable_createEventHandle with "custom-event", as it is not an event known to React.',
);
} else {
// Test that we get a warning when we don't provide an explicit priority
expect(() => {
setCustomEventHandle = ReactDOM.unstable_createEventHandle(
'custom-event',
);
}).toWarnDev(
'Warning: The event "custom-event" provided to createEventHandle() does not have a known priority type. ' +
'It is recommended to provide a "priority" option to specify a priority.',
{withoutStack: true},
);
setCustomEventHandle = ReactDOM.unstable_createEventHandle(
'custom-event',
{
priority: 0, // Discrete
},
);
const setCustomCaptureHandle = ReactDOM.unstable_createEventHandle(
'custom-event',
{
capture: true,
priority: 0, // Discrete
},
);
const Test = () => {
React.useEffect(() => {
const clearCustom1 = setCustomEventHandle(
buttonRef.current,
onCustomEvent,
);
const clearCustom2 = setCustomCaptureHandle(
buttonRef.current,
onCustomEventCapture,
);
const clearCustom3 = setCustomEventHandle(
divRef.current,
onCustomEvent,
);
const clearCustom4 = setCustomCaptureHandle(
divRef.current,
onCustomEventCapture,
);
return () => {
clearCustom1();
clearCustom2();
clearCustom3();
clearCustom4();
};
});
return (
<button ref={buttonRef}>
<div ref={divRef}>Click me!</div>
</button>
);
};
ReactDOM.render(<Test />, container);
Scheduler.unstable_flushAll();
const buttonElement = buttonRef.current;
dispatchEvent(buttonElement, 'custom-event');
expect(onCustomEvent).toHaveBeenCalledTimes(1);
expect(onCustomEventCapture).toHaveBeenCalledTimes(1);
expect(log[0]).toEqual(['capture', buttonElement]);
expect(log[1]).toEqual(['bubble', buttonElement]);
const divElement = divRef.current;
dispatchEvent(divElement, 'custom-event');
expect(onCustomEvent).toHaveBeenCalledTimes(3);
expect(onCustomEventCapture).toHaveBeenCalledTimes(3);
expect(log[2]).toEqual(['capture', buttonElement]);
expect(log[3]).toEqual(['capture', divElement]);
expect(log[4]).toEqual(['bubble', divElement]);
expect(log[5]).toEqual(['bubble', buttonElement]);
}
});
// @gate experimental
@ -3211,12 +3017,14 @@ describe('DOMPluginEventSystem', () => {
});
// @gate experimental
it('should be able to register non-passive handlers for events affected by the intervention', () => {
it('should be able to register handlers for events affected by the intervention', () => {
const rootContainer = document.createElement('div');
container.appendChild(rootContainer);
const allEvents = [];
const defaultPreventedEvents = [];
const handler = e => {
allEvents.push(e.type);
if (e.defaultPrevented) defaultPreventedEvents.push(e.type);
};
@ -3227,15 +3035,11 @@ describe('DOMPluginEventSystem', () => {
const ref = React.createRef();
const setTouchStart = ReactDOM.unstable_createEventHandle(
'touchstart',
{passive: false},
);
const setTouchMove = ReactDOM.unstable_createEventHandle(
'touchmove',
{passive: false},
);
const setWheel = ReactDOM.unstable_createEventHandle('wheel', {
passive: false,
});
const setWheel = ReactDOM.unstable_createEventHandle('wheel');
function Component() {
React.useEffect(() => {
@ -3264,11 +3068,17 @@ describe('DOMPluginEventSystem', () => {
dispatchEvent(ref.current, 'touchmove');
dispatchEvent(ref.current, 'wheel');
expect(defaultPreventedEvents).toEqual([
'touchstart',
'touchmove',
'wheel',
]);
expect(allEvents).toEqual(['touchstart', 'touchmove', 'wheel']);
// These events are passive by default, so we can't preventDefault.
if (gate(flags => flags.enablePassiveEventIntervention)) {
expect(defaultPreventedEvents).toEqual([]);
} else {
expect(defaultPreventedEvents).toEqual([
'touchstart',
'touchmove',
'wheel',
]);
}
});
});
});

View File

@ -22,7 +22,7 @@ import {registerTwoPhaseEvent} from '../EventRegistry';
import getActiveElement from '../../client/getActiveElement';
import {
getNodeFromInstance,
getEventListenerMap,
getEventListenerSet,
} from '../../client/ReactDOMComponentTree';
import {hasSelectionCapabilities} from '../../client/ReactInputSelection';
import {DOCUMENT_NODE} from '../../shared/HTMLNodeType';
@ -154,7 +154,7 @@ function extractEvents(
targetContainer: EventTarget,
) {
if (!enableEagerRootListeners) {
const eventListenerMap = getEventListenerMap(targetContainer);
const eventListenerSet = getEventListenerSet(targetContainer);
// Track whether all listeners exists for this plugin. If none exist, we do
// not extract events. See #3639.
if (
@ -163,8 +163,8 @@ function extractEvents(
// event attached from the onChange plugin and we don't expose an
// onSelectionChange event from React.
domEventName !== 'selectionchange' &&
!eventListenerMap.has('onSelect') &&
!eventListenerMap.has('onSelectCapture')
!eventListenerSet.has('onSelect') &&
!eventListenerSet.has('onSelectCapture')
) {
return;
}

View File

@ -37,32 +37,6 @@ const isMac =
? /^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;
@ -78,20 +52,13 @@ const globalFocusVisibleEvents = hasPointerEvents
'touchend',
];
const passiveObject = {passive: true};
const passiveObjectWithPriority = {passive: true, priority: 0};
// Global state for tracking focus visible and emulation of mouse
let isGlobalFocusVisible = true;
let hasTrackedGlobalFocusVisible = false;
function trackGlobalFocusVisible() {
globalFocusVisibleEvents.forEach(type => {
window.addEventListener(
type,
handleGlobalFocusVisibleEvent,
passiveBrowserEventsSupported ? {capture: true, passive: true} : true,
);
window.addEventListener(type, handleGlobalFocusVisibleEvent, true);
});
}
@ -171,9 +138,9 @@ function setFocusVisibleListeners(
function useFocusVisibleInputHandles() {
return [
useEvent('mousedown', passiveObject),
useEvent(hasPointerEvents ? 'pointerdown' : 'touchstart', passiveObject),
useEvent('keydown', passiveObject),
useEvent('mousedown'),
useEvent(hasPointerEvents ? 'pointerdown' : 'touchstart'),
useEvent('keydown'),
];
}
@ -200,8 +167,8 @@ export function useFocus(
const stateRef = useRef<null | {isFocused: boolean, isFocusVisible: boolean}>(
{isFocused: false, isFocusVisible: false},
);
const focusHandle = useEvent('focusin', passiveObjectWithPriority);
const blurHandle = useEvent('focusout', passiveObjectWithPriority);
const focusHandle = useEvent('focusin');
const blurHandle = useEvent('focusout');
const focusVisibleHandles = useFocusVisibleInputHandles();
useLayoutEffect(() => {
@ -297,10 +264,10 @@ export function useFocusWithin<T>(
const stateRef = useRef<null | {isFocused: boolean, isFocusVisible: boolean}>(
{isFocused: false, isFocusVisible: false},
);
const focusHandle = useEvent('focusin', passiveObjectWithPriority);
const blurHandle = useEvent('focusout', passiveObjectWithPriority);
const afterBlurHandle = useEvent('afterblur', passiveObject);
const beforeBlurHandle = useEvent('beforeblur', passiveObject);
const focusHandle = useEvent('focusin');
const blurHandle = useEvent('focusout');
const afterBlurHandle = useEvent('afterblur');
const beforeBlurHandle = useEvent('beforeblur');
const focusVisibleHandles = useFocusVisibleInputHandles();
const useFocusWithinRef = useCallback(

View File

@ -25,8 +25,6 @@ export default function useEvent(
event: string,
options?: {|
capture?: boolean,
passive?: boolean,
priority?: 0 | 1 | 2,
|},
): UseEventHandle {
const handleRef = useRef<UseEventHandle | null>(null);