Refactor event object creation for the experimental event API (#15295)

* Refactor event object creation for the experimental event API
This commit is contained in:
Dominic Gannaway
2019-04-02 20:03:11 +01:00
committed by GitHub
parent 6a1e6b2f78
commit 9307932fe5
9 changed files with 348 additions and 147 deletions
+14 -14
View File
@@ -7,29 +7,29 @@
* @flow
*/
import SyntheticEvent from 'events/SyntheticEvent';
import type {AnyNativeEvent} from 'events/PluginModuleType';
import type {ReactEventResponderEventType} from 'shared/ReactTypes';
export type EventResponderContext = {
event: AnyNativeEvent,
eventTarget: EventTarget,
eventTarget: Element | Document,
eventType: string,
isPassive: () => boolean,
isPassiveSupported: () => boolean,
dispatchEvent: (
name: string,
listener: (e: SyntheticEvent) => void | null,
pressTarget: EventTarget | null,
discrete: boolean,
extraProperties?: Object,
dispatchEvent: <E>(
eventObject: E,
{
capture?: boolean,
discrete?: boolean,
stopPropagation?: boolean,
},
) => void,
isTargetWithinElement: (
childTarget: EventTarget,
parentTarget: EventTarget,
childTarget: Element | Document,
parentTarget: Element | Document,
) => boolean,
isTargetOwned: EventTarget => boolean,
isTargetWithinEventComponent: EventTarget => boolean,
isTargetOwned: (Element | Document) => boolean,
isTargetWithinEventComponent: (Element | Document) => boolean,
isPositionWithinTouchHitTarget: (x: number, y: number) => boolean,
addRootEventTypes: (
rootEventTypes: Array<ReactEventResponderEventType>,
@@ -37,6 +37,6 @@ export type EventResponderContext = {
removeRootEventTypes: (
rootEventTypes: Array<ReactEventResponderEventType>,
) => void,
requestOwnership: (target: EventTarget | null) => boolean,
releaseOwnership: (target: EventTarget | null) => boolean,
requestOwnership: (target: Element | Document | null) => boolean,
releaseOwnership: (target: Element | Document | null) => boolean,
};
+132 -58
View File
@@ -18,15 +18,13 @@ import type {
ReactEventResponderEventType,
} from 'shared/ReactTypes';
import type {DOMTopLevelEventType} from 'events/TopLevelEventTypes';
import SyntheticEvent from 'events/SyntheticEvent';
import {runEventsInBatch} from 'events/EventBatching';
import {interactiveUpdates} from 'events/ReactGenericBatching';
import {executeDispatch} from 'events/EventPluginUtils';
import {batchedUpdates, interactiveUpdates} from 'events/ReactGenericBatching';
import type {Fiber} from 'react-reconciler/src/ReactFiber';
import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree';
import {enableEventAPI} from 'shared/ReactFeatureFlags';
import {invokeGuardedCallbackAndCatchFirstError} from 'shared/ReactErrorUtils';
import warning from 'shared/warning';
let listenToResponderEventTypesImpl;
@@ -36,6 +34,8 @@ export function setListenToResponderEventTypes(
listenToResponderEventTypesImpl = _listenToResponderEventTypesImpl;
}
const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set;
const rootEventTypesToEventComponents: Map<
DOMTopLevelEventType | string,
Set<Fiber>,
@@ -45,12 +45,76 @@ const targetEventTypeCached: Map<
Set<DOMTopLevelEventType>,
> = new Map();
const targetOwnership: Map<EventTarget, Fiber> = new Map();
const eventsWithStopPropagation:
| WeakSet
| Set<$Shape<PartialEventObject>> = new PossiblyWeakSet();
type EventListener = (event: SyntheticEvent) => void;
type PartialEventObject = {
listener: ($Shape<PartialEventObject>) => void,
target: Element | Document,
type: string,
};
type EventQueue = {
bubble: null | Array<$Shape<PartialEventObject>>,
capture: null | Array<$Shape<PartialEventObject>>,
discrete: boolean,
phase: EventQueuePhase,
};
type EventQueuePhase = 0 | 1;
function copyEventProperties(eventData, syntheticEvent) {
for (let propName in eventData) {
syntheticEvent[propName] = eventData[propName];
const DURING_EVENT_PHASE = 0;
const AFTER_EVENT_PHASE = 1;
function createEventQueue(phase: EventQueuePhase): EventQueue {
return {
bubble: null,
capture: null,
discrete: false,
phase,
};
}
function processEvent(event: $Shape<PartialEventObject>): void {
const type = event.type;
const listener = event.listener;
invokeGuardedCallbackAndCatchFirstError(type, listener, undefined, event);
}
function processEvents(
bubble: null | Array<$Shape<PartialEventObject>>,
capture: null | Array<$Shape<PartialEventObject>>,
): void {
let i, length;
if (capture !== null) {
for (i = capture.length; i-- > 0; ) {
const event = capture[i];
processEvent(capture[i]);
if (eventsWithStopPropagation.has(event)) {
return;
}
}
}
if (bubble !== null) {
for (i = 0, length = bubble.length; i < length; ++i) {
const event = bubble[i];
processEvent(event);
if (eventsWithStopPropagation.has(event)) {
return;
}
}
}
}
function processEventQueue(eventQueue: EventQueue): void {
const {bubble, capture, discrete} = eventQueue;
if (discrete) {
interactiveUpdates(() => {
processEvents(bubble, capture);
});
} else {
processEvents(bubble, capture);
}
}
@@ -70,6 +134,7 @@ function DOMEventResponderContext(
this._discreteEvents = null;
this._nonDiscreteEvents = null;
this._isBatching = true;
this._eventQueue = createEventQueue(DURING_EVENT_PHASE);
}
DOMEventResponderContext.prototype.isPassive = function(): boolean {
@@ -81,49 +146,66 @@ DOMEventResponderContext.prototype.isPassiveSupported = function(): boolean {
};
DOMEventResponderContext.prototype.dispatchEvent = function(
eventName: string,
eventListener: EventListener,
eventTarget: AnyNativeEvent,
discrete: boolean,
extraProperties?: Object,
possibleEventObject: Object,
{
capture,
discrete,
stopPropagation,
}: {
capture?: boolean,
discrete?: boolean,
stopPropagation?: boolean,
},
): void {
const eventTargetFiber = getClosestInstanceFromNode(eventTarget);
const syntheticEvent = SyntheticEvent.getPooled(
null,
eventTargetFiber,
this.event,
eventTarget,
);
if (extraProperties !== undefined) {
copyEventProperties(extraProperties, syntheticEvent);
}
syntheticEvent.type = eventName;
syntheticEvent._dispatchInstances = [eventTargetFiber];
syntheticEvent._dispatchListeners = [eventListener];
const eventQueue = this._eventQueue;
const {listener, target, type} = possibleEventObject;
if (this._isBatching) {
let events;
if (discrete) {
events = this._discreteEvents;
if (events === null) {
events = this._discreteEvents = [];
}
} else {
events = this._nonDiscreteEvents;
if (events === null) {
events = this._nonDiscreteEvents = [];
}
if (listener == null || target == null || type == null) {
throw new Error(
'context.dispatchEvent: "listener", "target" and "type" fields on event object are required.',
);
}
if (__DEV__) {
possibleEventObject.preventDefault = () => {
// Update this warning when we have a story around dealing with preventDefault
warning(
false,
'preventDefault() is no longer available on event objects created from event responder modules.',
);
};
possibleEventObject.stopPropagation = () => {
// Update this warning when we have a story around dealing with stopPropgation
warning(
false,
'stopPropagation() is no longer available on event objects created from event responder modules.',
);
};
}
const eventObject = ((possibleEventObject: any): $Shape<PartialEventObject>);
let events;
if (capture) {
events = eventQueue.capture;
if (events === null) {
events = eventQueue.capture = [];
}
events.push(syntheticEvent);
} else {
if (discrete) {
interactiveUpdates(() => {
executeDispatch(syntheticEvent, eventListener, eventTargetFiber);
});
} else {
executeDispatch(syntheticEvent, eventListener, eventTargetFiber);
events = eventQueue.bubble;
if (events === null) {
events = eventQueue.bubble = [];
}
}
if (discrete) {
eventQueue.discrete = true;
}
events.push(eventObject);
if (stopPropagation) {
eventsWithStopPropagation.add(eventObject);
}
if (eventQueue.phase === AFTER_EVENT_PHASE) {
batchedUpdates(processEventQueue, eventQueue);
}
};
DOMEventResponderContext.prototype.isTargetWithinEventComponent = function(
@@ -318,17 +400,9 @@ export function runResponderEventsInBatch(
);
}
}
// Run batched events
const discreteEvents = context._discreteEvents;
if (discreteEvents !== null) {
interactiveUpdates(() => {
runEventsInBatch(discreteEvents);
});
}
const nonDiscreteEvents = context._nonDiscreteEvents;
if (nonDiscreteEvents !== null) {
runEventsInBatch(nonDiscreteEvents);
}
context._isBatching = false;
processEventQueue(context._eventQueue);
// In order to capture and process async events from responder modules
// we create a new event queue.
context._eventQueue = createEventQueue(AFTER_EVENT_PHASE);
}
}
@@ -228,12 +228,12 @@ describe('DOMEventResponderSystem', () => {
['click'],
(context, props) => {
if (props.onMagicClick) {
context.dispatchEvent(
'magicclick',
props.onMagicClick,
context.eventTarget,
false,
);
const event = {
listener: props.onMagicClick,
target: context.eventTarget,
type: 'magicclick',
};
context.dispatchEvent(event, {discrete: true});
}
},
);
+41 -12
View File
@@ -14,7 +14,7 @@ const targetEventTypes = ['pointerdown', 'pointercancel'];
const rootEventTypes = ['pointerup', {name: 'pointermove', passive: false}];
type DragState = {
dragTarget: null | EventTarget,
dragTarget: null | Element | Document,
isPointerDown: boolean,
isDragging: boolean,
startX: number,
@@ -33,18 +33,45 @@ if (typeof window !== 'undefined' && window.PointerEvent === undefined) {
});
}
type EventData = {
diffX: number,
diffY: number,
};
type DragEventType = 'dragend' | 'dragchange' | 'dragmove';
type DragEvent = {|
listener: DragEvent => void,
target: Element | Document,
type: DragEventType,
diffX?: number,
diffY?: number,
|};
function createDragEvent(
type: DragEventType,
target: Element | Document,
listener: DragEvent => void,
eventData?: EventData,
): DragEvent {
return {
listener,
target,
type,
...eventData,
};
}
function dispatchDragEvent(
context: EventResponderContext,
name: string,
listener: (e: Object) => void,
name: DragEventType,
listener: DragEvent => void,
state: DragState,
discrete: boolean,
eventData?: {
diffX: number,
diffY: number,
},
eventData?: EventData,
): void {
context.dispatchEvent(name, listener, state.dragTarget, discrete, eventData);
const target = ((state.dragTarget: any): Element | Document);
const syntheticEvent = createDragEvent(name, target, listener, eventData);
context.dispatchEvent(syntheticEvent, {discrete});
}
const DragResponder = {
@@ -112,10 +139,11 @@ const DragResponder = {
const dragChangeEventListener = () => {
props.onDragChange(true);
};
context.dispatchEvent(
dispatchDragEvent(
context,
'dragchange',
dragChangeEventListener,
state.dragTarget,
state,
true,
);
}
@@ -160,10 +188,11 @@ const DragResponder = {
const dragChangeEventListener = () => {
props.onDragChange(false);
};
context.dispatchEvent(
dispatchDragEvent(
context,
'dragchange',
dragChangeEventListener,
state.dragTarget,
state,
true,
);
}
+34 -8
View File
@@ -19,24 +19,49 @@ type FocusState = {
isFocused: boolean,
};
type FocusEventType = 'focus' | 'blur' | 'focuschange';
type FocusEvent = {|
listener: FocusEvent => void,
target: Element | Document,
type: FocusEventType,
|};
function createFocusEvent(
type: FocusEventType,
target: Element | Document,
listener: FocusEvent => void,
): FocusEvent {
return {
listener,
target,
type,
};
}
function dispatchFocusInEvents(context: EventResponderContext, props: Object) {
const {event, eventTarget} = context;
if (context.isTargetWithinEventComponent((event: any).relatedTarget)) {
return;
}
if (props.onFocus) {
context.dispatchEvent('focus', props.onFocus, eventTarget, true);
const syntheticEvent = createFocusEvent(
'focus',
eventTarget,
props.onFocus,
);
context.dispatchEvent(syntheticEvent, {discrete: true});
}
if (props.onFocusChange) {
const focusChangeEventListener = () => {
props.onFocusChange(true);
};
context.dispatchEvent(
const syntheticEvent = createFocusEvent(
'focuschange',
focusChangeEventListener,
eventTarget,
true,
focusChangeEventListener,
);
context.dispatchEvent(syntheticEvent, {discrete: true});
}
}
@@ -46,18 +71,19 @@ function dispatchFocusOutEvents(context: EventResponderContext, props: Object) {
return;
}
if (props.onBlur) {
context.dispatchEvent('blur', props.onBlur, eventTarget, true);
const syntheticEvent = createFocusEvent('blur', eventTarget, props.onBlur);
context.dispatchEvent(syntheticEvent, {discrete: true});
}
if (props.onFocusChange) {
const focusChangeEventListener = () => {
props.onFocusChange(false);
};
context.dispatchEvent(
const syntheticEvent = createFocusEvent(
'focuschange',
focusChangeEventListener,
eventTarget,
true,
focusChangeEventListener,
);
context.dispatchEvent(syntheticEvent, {discrete: true});
}
}
+38 -8
View File
@@ -23,6 +23,26 @@ type HoverState = {
isTouched: boolean,
};
type HoverEventType = 'hoverstart' | 'hoverend' | 'hoverchange';
type HoverEvent = {|
listener: HoverEvent => void,
target: Element | Document,
type: HoverEventType,
|};
function createHoverEvent(
type: HoverEventType,
target: Element | Document,
listener: HoverEvent => void,
): HoverEvent {
return {
listener,
target,
type,
};
}
// In the case we don't have PointerEvents (Safari), we listen to touch events
// too
if (typeof window !== 'undefined' && window.PointerEvent === undefined) {
@@ -39,18 +59,23 @@ function dispatchHoverStartEvents(
return;
}
if (props.onHoverStart) {
context.dispatchEvent('hoverstart', props.onHoverStart, eventTarget, true);
const syntheticEvent = createHoverEvent(
'hoverstart',
eventTarget,
props.onHoverStart,
);
context.dispatchEvent(syntheticEvent, {discrete: true});
}
if (props.onHoverChange) {
const hoverChangeEventListener = () => {
props.onHoverChange(true);
};
context.dispatchEvent(
const syntheticEvent = createHoverEvent(
'hoverchange',
hoverChangeEventListener,
eventTarget,
true,
hoverChangeEventListener,
);
context.dispatchEvent(syntheticEvent, {discrete: true});
}
}
@@ -60,18 +85,23 @@ function dispatchHoverEndEvents(context: EventResponderContext, props: Object) {
return;
}
if (props.onHoverEnd) {
context.dispatchEvent('hoverend', props.onHoverEnd, eventTarget, true);
const syntheticEvent = createHoverEvent(
'hoverend',
eventTarget,
props.onHoverEnd,
);
context.dispatchEvent(syntheticEvent, {discrete: true});
}
if (props.onHoverChange) {
const hoverChangeEventListener = () => {
props.onHoverChange(false);
};
context.dispatchEvent(
const syntheticEvent = createHoverEvent(
'hoverchange',
hoverChangeEventListener,
eventTarget,
true,
hoverChangeEventListener,
);
context.dispatchEvent(syntheticEvent, {discrete: true});
}
}
+44 -31
View File
@@ -36,13 +36,13 @@ type PressProps = {
delayLongPress: number,
delayPressEnd: number,
delayPressStart: number,
onLongPress: (e: Object) => void,
onLongPress: (e: PressEvent) => void,
onLongPressChange: boolean => void,
onLongPressShouldCancelPress: () => boolean,
onPress: (e: Object) => void,
onPress: (e: PressEvent) => void,
onPressChange: boolean => void,
onPressEnd: (e: Object) => void,
onPressStart: (e: Object) => void,
onPressEnd: (e: PressEvent) => void,
onPressStart: (e: PressEvent) => void,
pressRententionOffset: Object,
};
@@ -52,17 +52,45 @@ type PressState = {
isLongPressed: boolean,
isPressed: boolean,
longPressTimeout: null | TimeoutID,
pressTarget: null | EventTarget,
pressTarget: null | Element | Document,
shouldSkipMouseAfterTouch: boolean,
};
type PressEventType =
| 'press'
| 'pressstart'
| 'pressend'
| 'presschange'
| 'longpress'
| 'longpresschange';
type PressEvent = {|
listener: PressEvent => void,
target: Element | Document,
type: PressEventType,
|};
function createPressEvent(
type: PressEventType,
target: Element | Document,
listener: PressEvent => void,
): PressEvent {
return {
listener,
target,
type,
};
}
function dispatchPressEvent(
context: EventResponderContext,
state: PressState,
name: string,
name: PressEventType,
listener: (e: Object) => void,
): void {
context.dispatchEvent(name, listener, state.pressTarget, true);
const target = ((state.pressTarget: any): Element | Document);
const syntheticEvent = createPressEvent(name, target, listener);
context.dispatchEvent(syntheticEvent, {discrete: true});
}
function dispatchPressStartEvents(
@@ -97,9 +125,10 @@ function dispatchPressStartEvents(
if (props.onLongPress) {
const longPressEventListener = e => {
props.onLongPress(e);
if (e.nativeEvent.defaultPrevented) {
state.defaultPrevented = true;
}
// TODO address this again at some point
// if (e.nativeEvent.defaultPrevented) {
// state.defaultPrevented = true;
// }
};
dispatchPressEvent(context, state, 'longpress', longPressEventListener);
}
@@ -193,24 +222,7 @@ const PressResponder = {
) {
return;
}
let keyPressEventListener = props.onPress;
// Wrap listener with prevent default behaviour, unless
// we are dealing with an anchor. Anchor tags are special beacuse
// we need to use the "click" event, to properly allow browser
// heuristics for cancelling link clicks. Furthermore, iOS and
// Android can show previous of anchor tags that requires working
// with click rather than touch events (and mouse down/up).
if (!isAnchorTagElement(eventTarget)) {
keyPressEventListener = e => {
if (!e.isDefaultPrevented() && !e.nativeEvent.defaultPrevented) {
e.preventDefault();
state.defaultPrevented = true;
props.onPress(e);
}
};
}
dispatchPressEvent(context, state, 'press', keyPressEventListener);
dispatchPressEvent(context, state, 'press', props.onPress);
break;
}
@@ -332,9 +344,10 @@ const PressResponder = {
) {
const pressEventListener = e => {
props.onPress(e);
if (e.nativeEvent.defaultPrevented) {
state.defaultPrevented = true;
}
// TODO address this again at some point
// if (e.nativeEvent.defaultPrevented) {
// state.defaultPrevented = true;
// }
};
dispatchPressEvent(context, state, 'press', pressEventListener);
}
+35 -8
View File
@@ -23,18 +23,45 @@ if (typeof window !== 'undefined' && window.PointerEvent === undefined) {
});
}
type EventData = {
diffX: number,
diffY: number,
};
type SwipeEventType = 'swipeleft' | 'swiperight' | 'swipeend' | 'swipemove';
type SwipeEvent = {|
listener: SwipeEvent => void,
target: Element | Document,
type: SwipeEventType,
diffX?: number,
diffY?: number,
|};
function createSwipeEvent(
type: SwipeEventType,
target: Element | Document,
listener: SwipeEvent => void,
eventData?: EventData,
): SwipeEvent {
return {
listener,
target,
type,
...eventData,
};
}
function dispatchSwipeEvent(
context: EventResponderContext,
name: string,
listener: (e: Object) => void,
name: SwipeEventType,
listener: SwipeEvent => void,
state: SwipeState,
discrete: boolean,
eventData?: {
diffX: number,
diffY: number,
},
eventData?: EventData,
) {
context.dispatchEvent(name, listener, state.swipeTarget, discrete, eventData);
const target = ((state.swipeTarget: any): Element | Document);
const syntheticEvent = createSwipeEvent(name, target, listener, eventData);
context.dispatchEvent(syntheticEvent, {discrete});
}
type SwipeState = {
@@ -44,7 +71,7 @@ type SwipeState = {
startX: number,
startY: number,
touchId: null | number,
swipeTarget: null | EventTarget,
swipeTarget: null | Element | Document,
x: number,
y: number,
};
@@ -388,8 +388,10 @@ describe('Event responder: Press', () => {
events = [];
ref.current.dispatchEvent(createKeyboardEvent('keydown', {key: 'Enter'}));
// Outer press should not occur as inner press will preventDefault
expect(events).toEqual(['keydown', 'inner: onPress']);
// TODO update this test once we have a form of stopPropagation in
// the responder system again. This test had to be updated because
// we have removed stopPropagation() from synthetic events.
expect(events).toEqual(['keydown', 'inner: onPress', 'outer: onPress']);
});
});
});