Skip to content

Commit

Permalink
ReactDOM.useEvent: support custom types (#18351)
Browse files Browse the repository at this point in the history
* ReactDOM.useEvent: support custom types
  • Loading branch information
trueadm committed Mar 20, 2020
1 parent 7c14786 commit dbd85a0
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 29 deletions.
20 changes: 14 additions & 6 deletions packages/legacy-events/ReactSyntheticEventType.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 5 additions & 2 deletions packages/react-dom/src/events/DOMEventProperties.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -31,7 +34,7 @@ export const simpleEventPluginEventTypes = {};

export const topLevelEventsToDispatchConfig: Map<
TopLevelType,
DispatchConfig,
DispatchConfig | CustomDispatchConfig,
> = new Map();

const eventPriorities = new Map();
Expand Down
29 changes: 27 additions & 2 deletions packages/react-dom/src/events/DOMModernPluginEventSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -79,6 +82,7 @@ import {
COMMENT_NODE,
ELEMENT_NODE,
} from '../shared/HTMLNodeType';
import {topLevelEventsToDispatchConfig} from './DOMEventProperties';

import {enableLegacyFBSupport} from 'shared/ReactFeatureFlags';

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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 {
Expand Down
5 changes: 4 additions & 1 deletion packages/react-dom/src/events/SimpleEventPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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]);
});
});
},
);
Expand Down
33 changes: 17 additions & 16 deletions packages/react-dom/src/events/accumulateTwoPhaseListeners.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit dbd85a0

Please sign in to comment.