ReactDOM.useEvent: support custom types (#18351)

* ReactDOM.useEvent: support custom types
This commit is contained in:
Dominic Gannaway
2020-03-20 14:45:33 +00:00
committed by GitHub
parent 7c1478680f
commit dbd85a08d9
6 changed files with 147 additions and 29 deletions
@@ -13,19 +13,27 @@ import type {EventPriority} from 'shared/ReactTypes';
import type {TopLevelType} from './TopLevelEventTypes';
export type DispatchConfig = {|
dependencies: Array<TopLevelType>,
phasedRegistrationNames?: {|
bubbled: string,
captured: string,
dependencies?: Array<TopLevelType>,
phasedRegistrationNames: {|
bubbled: null | string,
captured: null | string,
|},
registrationName?: string,
eventPriority: EventPriority,
|};
export type CustomDispatchConfig = {|
phasedRegistrationNames: {|
bubbled: null,
captured: null,
|},
customEvent: true,
|};
export type ReactSyntheticEvent = {|
dispatchConfig: DispatchConfig,
dispatchConfig: DispatchConfig | CustomDispatchConfig,
getPooled: (
dispatchConfig: DispatchConfig,
dispatchConfig: DispatchConfig | CustomDispatchConfig,
targetInst: Fiber,
nativeTarget: Event,
nativeEventTarget: EventTarget,
+5 -2
View File
@@ -12,7 +12,10 @@ import type {
TopLevelType,
DOMTopLevelEventType,
} from 'legacy-events/TopLevelEventTypes';
import type {DispatchConfig} from 'legacy-events/ReactSyntheticEventType';
import type {
DispatchConfig,
CustomDispatchConfig,
} from 'legacy-events/ReactSyntheticEventType';
import * as DOMTopLevelEventTypes from './DOMTopLevelEventTypes';
import {
@@ -31,7 +34,7 @@ export const simpleEventPluginEventTypes = {};
export const topLevelEventsToDispatchConfig: Map<
TopLevelType,
DispatchConfig,
DispatchConfig | CustomDispatchConfig,
> = new Map();
const eventPriorities = new Map();
+27 -2
View File
@@ -17,7 +17,10 @@ import type {EventSystemFlags} from 'legacy-events/EventSystemFlags';
import type {EventPriority} from 'shared/ReactTypes';
import type {Fiber} from 'react-reconciler/src/ReactFiber';
import type {PluginModule} from 'legacy-events/PluginModuleType';
import type {ReactSyntheticEvent} from 'legacy-events/ReactSyntheticEventType';
import type {
ReactSyntheticEvent,
CustomDispatchConfig,
} from 'legacy-events/ReactSyntheticEventType';
import type {ReactDOMListener} from 'shared/ReactDOMTypes';
import {registrationNameDependencies} from 'legacy-events/EventPluginRegistry';
@@ -79,6 +82,7 @@ import {
COMMENT_NODE,
ELEMENT_NODE,
} from '../shared/HTMLNodeType';
import {topLevelEventsToDispatchConfig} from './DOMEventProperties';
import {enableLegacyFBSupport} from 'shared/ReactFeatureFlags';
@@ -118,6 +122,14 @@ const capturePhaseEvents = new Set([
TOP_WAITING,
]);
const emptyDispatchConfigForCustomEvents: CustomDispatchConfig = {
customEvent: true,
phasedRegistrationNames: {
bubbled: null,
captured: null,
},
};
const isArray = Array.isArray;
function dispatchEventsForPlugins(
@@ -419,8 +431,21 @@ export function attachElementListener(listener: ReactDOMListener): void {
listeners = new Set();
initListenersSet(target, listeners);
}
// Finally, add our listener to the listeners Set.
// Add our listener to the listeners Set.
listeners.add(listener);
// Finally, add the event to our known event types list.
let 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 detachElementListener(listener: ReactDOMListener): void {
+4 -1
View File
@@ -172,7 +172,10 @@ const SimpleEventPlugin: PluginModule<MouseEvent> = {
break;
default:
if (__DEV__) {
if (knownHTMLTopLevelTypes.indexOf(topLevelType) === -1) {
if (
knownHTMLTopLevelTypes.indexOf(topLevelType) === -1 &&
dispatchConfig.customEvent !== true
) {
console.error(
'SimpleEventPlugin: Unhandled event type, `%s`. This warning ' +
'is likely caused by a bug in React. Please file an issue.',
@@ -15,12 +15,16 @@ let ReactDOM;
let ReactDOMServer;
let Scheduler;
function dispatchClickEvent(element) {
function dispatchEvent(element, type) {
const event = document.createEvent('Event');
event.initEvent('click', true, true);
event.initEvent(type, true, true);
element.dispatchEvent(event);
}
function dispatchClickEvent(element) {
dispatchEvent(element, 'click');
}
describe('DOMModernPluginEventSystem', () => {
let container;
@@ -1782,6 +1786,80 @@ describe('DOMModernPluginEventSystem', () => {
dispatchClickEvent(button);
expect(clickEvent).toHaveBeenCalledTimes(1);
});
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]),
);
const onCustomEventCapture = jest.fn(e =>
log.push(['capture', e.currentTarget]),
);
function Test() {
let customEventHandle;
// Test that we get a warning when we don't provide an explicit priortiy
expect(() => {
customEventHandle = ReactDOM.unstable_useEvent('custom-event');
}).toWarnDev(
'Warning: The event "type" provided to useEvent() does not have a known priority type. ' +
'It is recommended to provide a "priority" option to specify a priority.',
);
customEventHandle = ReactDOM.unstable_useEvent('custom-event', {
priority: 0, // Discrete
});
const customCaptureHandle = ReactDOM.unstable_useEvent(
'custom-event',
{
capture: true,
priority: 0, // Discrete
},
);
React.useEffect(() => {
customEventHandle.setListener(buttonRef.current, onCustomEvent);
customCaptureHandle.setListener(
buttonRef.current,
onCustomEventCapture,
);
customEventHandle.setListener(divRef.current, onCustomEvent);
customCaptureHandle.setListener(
divRef.current,
onCustomEventCapture,
);
});
return (
<button ref={buttonRef}>
<div ref={divRef}>Click me!</div>
</button>
);
}
ReactDOM.render(<Test />, container);
Scheduler.unstable_flushAll();
let 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]);
let 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]);
});
});
},
);
+17 -16
View File
@@ -20,12 +20,9 @@ export default function accumulateTwoPhaseListeners(
accumulateUseEventListeners?: boolean,
): void {
const phasedRegistrationNames = event.dispatchConfig.phasedRegistrationNames;
if (phasedRegistrationNames == null) {
return;
}
const {bubbled, captured} = phasedRegistrationNames;
const dispatchListeners = [];
const dispatchInstances = [];
const {bubbled, captured} = phasedRegistrationNames;
let node = event._targetInst;
// Accumulate all instances and listeners via the target -> root path.
@@ -60,19 +57,23 @@ export default function accumulateTwoPhaseListeners(
}
}
// Standard React on* listeners, i.e. onClick prop
const captureListener = getListener(node, captured);
if (captureListener != null) {
// Capture listeners/instances should go at the start, so we
// unshift them to the start of the array.
dispatchListeners.unshift(captureListener);
dispatchInstances.unshift(node);
if (captured !== null) {
const captureListener = getListener(node, captured);
if (captureListener != null) {
// Capture listeners/instances should go at the start, so we
// unshift them to the start of the array.
dispatchListeners.unshift(captureListener);
dispatchInstances.unshift(node);
}
}
const bubbleListener = getListener(node, bubbled);
if (bubbleListener != null) {
// Bubble listeners/instances should go at the end, so we
// push them to the end of the array.
dispatchListeners.push(bubbleListener);
dispatchInstances.push(node);
if (bubbled !== null) {
const bubbleListener = getListener(node, bubbled);
if (bubbleListener != null) {
// Bubble listeners/instances should go at the end, so we
// push them to the end of the array.
dispatchListeners.push(bubbleListener);
dispatchInstances.push(node);
}
}
}
node = node.return;