Experimental event API: refactor responder modules for lifecycle inclusion (#15322)

This commit is contained in:
Dominic Gannaway
2019-04-04 23:28:23 +01:00
committed by GitHub
parent 937d262f55
commit b93a8a9bb8
11 changed files with 486 additions and 479 deletions
+23 -17
View File
@@ -10,34 +10,40 @@
import type {AnyNativeEvent} from 'events/PluginModuleType';
import type {ReactEventResponderEventType} from 'shared/ReactTypes';
export type EventResponderContext = {
event: AnyNativeEvent,
eventTarget: Element | Document,
eventType: string,
isPassive: () => boolean,
isPassiveSupported: () => boolean,
dispatchEvent: <E>(
eventObject: E,
{
capture?: boolean,
discrete?: boolean,
stopPropagation?: boolean,
},
export type ResponderEvent = {
nativeEvent: AnyNativeEvent,
target: Element | Document,
type: string,
passive: boolean,
passiveSupported: boolean,
};
export type ResponderDispatchEventOptions = {
capture?: boolean,
discrete?: boolean,
stopPropagation?: boolean,
};
export type ResponderContext = {
dispatchEvent: (
eventObject: Object,
otpions: ResponderDispatchEventOptions,
) => void,
isTargetWithinElement: (
childTarget: Element | Document,
parentTarget: Element | Document,
) => boolean,
isTargetOwned: (Element | Document) => boolean,
isTargetWithinEventComponent: (Element | Document) => boolean,
isPositionWithinTouchHitTarget: (x: number, y: number) => boolean,
addRootEventTypes: (
document: Document,
rootEventTypes: Array<ReactEventResponderEventType>,
) => void,
removeRootEventTypes: (
rootEventTypes: Array<ReactEventResponderEventType>,
) => void,
requestOwnership: (target: Element | Document | null) => boolean,
releaseOwnership: (target: Element | Document | null) => boolean,
withAsyncDispatching: (func: () => void) => void,
hasOwnership: () => boolean,
requestOwnership: () => boolean,
releaseOwnership: () => boolean,
setTimeout: (func: () => void, timeout: number) => TimeoutID,
};
+225 -246
View File
@@ -6,6 +6,11 @@
* @flow
*/
import type {
ResponderContext,
ResponderEvent,
ResponderDispatchEventOptions,
} from 'events/EventTypes';
import {
type EventSystemFlags,
IS_PASSIVE,
@@ -20,13 +25,13 @@ import type {
import type {DOMTopLevelEventType} from 'events/TopLevelEventTypes';
import {batchedUpdates, interactiveUpdates} from 'events/ReactGenericBatching';
import type {Fiber} from 'react-reconciler/src/ReactFiber';
import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree';
import warning from 'shared/warning';
import {enableEventAPI} from 'shared/ReactFeatureFlags';
import {invokeGuardedCallbackAndCatchFirstError} from 'shared/ReactErrorUtils';
import warning from 'shared/warning';
let listenToResponderEventTypesImpl;
import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree';
export let listenToResponderEventTypesImpl;
export function setListenToResponderEventTypes(
_listenToResponderEventTypesImpl: Function,
@@ -34,32 +39,216 @@ export function setListenToResponderEventTypes(
listenToResponderEventTypesImpl = _listenToResponderEventTypesImpl;
}
const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set;
const rootEventTypesToEventComponents: Map<
DOMTopLevelEventType | string,
Set<Fiber>,
> = new Map();
const targetEventTypeCached: Map<
Array<ReactEventResponderEventType>,
Set<DOMTopLevelEventType>,
> = new Map();
const targetOwnership: Map<EventTarget, Fiber> = new Map();
const eventsWithStopPropagation:
| WeakSet
| Set<$Shape<PartialEventObject>> = new PossiblyWeakSet();
type EventQueue = {
bubble: null | Array<$Shape<PartialEventObject>>,
capture: null | Array<$Shape<PartialEventObject>>,
discrete: boolean,
};
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,
let currentOwner = null;
let currentFiber: Fiber;
let currentResponder: ReactEventResponder;
let currentEventQueue: EventQueue;
const eventResponderContext: ResponderContext = {
dispatchEvent(
possibleEventObject: Object,
{capture, discrete, stopPropagation}: ResponderDispatchEventOptions,
): void {
const eventQueue = currentEventQueue;
const {listener, target, type} = possibleEventObject;
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 = [];
}
} else {
events = eventQueue.bubble;
if (events === null) {
events = eventQueue.bubble = [];
}
}
if (discrete) {
eventQueue.discrete = true;
}
events.push(eventObject);
if (stopPropagation) {
eventsWithStopPropagation.add(eventObject);
}
},
isPositionWithinTouchHitTarget(x: number, y: number): boolean {
return false;
},
isTargetWithinEventComponent(target: Element | Document): boolean {
const eventFiber = currentFiber;
if (target != null) {
let fiber = getClosestInstanceFromNode(target);
while (fiber !== null) {
if (fiber === eventFiber || fiber === eventFiber.alternate) {
return true;
}
fiber = fiber.return;
}
}
return false;
},
isTargetWithinElement(
childTarget: Element | Document,
parentTarget: Element | Document,
): boolean {
const childFiber = getClosestInstanceFromNode(childTarget);
const parentFiber = getClosestInstanceFromNode(parentTarget);
let node = childFiber;
while (node !== null) {
if (node === parentFiber) {
return true;
}
node = node.return;
}
return false;
},
addRootEventTypes(
doc: Document,
rootEventTypes: Array<ReactEventResponderEventType>,
): void {
listenToResponderEventTypesImpl(rootEventTypes, doc);
const eventComponent = currentFiber;
for (let i = 0; i < rootEventTypes.length; i++) {
const rootEventType = rootEventTypes[i];
const topLevelEventType =
typeof rootEventType === 'string' ? rootEventType : rootEventType.name;
let rootEventComponents = rootEventTypesToEventComponents.get(
topLevelEventType,
);
if (rootEventComponents === undefined) {
rootEventComponents = new Set();
rootEventTypesToEventComponents.set(
topLevelEventType,
rootEventComponents,
);
}
rootEventComponents.add(eventComponent);
}
},
removeRootEventTypes(
rootEventTypes: Array<ReactEventResponderEventType>,
): void {
const eventComponent = currentFiber;
for (let i = 0; i < rootEventTypes.length; i++) {
const rootEventType = rootEventTypes[i];
const topLevelEventType =
typeof rootEventType === 'string' ? rootEventType : rootEventType.name;
let rootEventComponents = rootEventTypesToEventComponents.get(
topLevelEventType,
);
if (rootEventComponents !== undefined) {
rootEventComponents.delete(eventComponent);
}
}
},
hasOwnership(): boolean {
return currentOwner === currentFiber;
},
requestOwnership(): boolean {
if (currentOwner !== null) {
return false;
}
currentOwner = currentFiber;
return true;
},
releaseOwnership(): boolean {
if (currentOwner !== currentFiber) {
return false;
}
currentOwner = null;
return false;
},
setTimeout(func: () => void, delay): TimeoutID {
const contextResponder = currentResponder;
const contextFiber = currentFiber;
return setTimeout(() => {
const previousEventQueue = currentEventQueue;
const previousFiber = currentFiber;
const previousResponder = currentResponder;
currentEventQueue = createEventQueue();
currentResponder = contextResponder;
currentFiber = contextFiber;
try {
func();
batchedUpdates(processEventQueue, currentEventQueue);
} finally {
currentFiber = previousFiber;
currentEventQueue = previousEventQueue;
currentResponder = previousResponder;
}
}, delay);
},
};
const rootEventTypesToEventComponents: Map<
DOMTopLevelEventType | string,
Set<Fiber>,
> = new Map();
const PossiblyWeakSet = typeof WeakSet === 'function' ? WeakSet : Set;
const eventsWithStopPropagation:
| WeakSet
| Set<$Shape<PartialEventObject>> = new PossiblyWeakSet();
const targetEventTypeCached: Map<
Array<ReactEventResponderEventType>,
Set<DOMTopLevelEventType>,
> = new Map();
function createResponderEvent(
topLevelType: string,
nativeEvent: AnyNativeEvent,
nativeEventTarget: Element | Document,
eventSystemFlags: EventSystemFlags,
): ResponderEvent {
return {
nativeEvent: nativeEvent,
target: nativeEventTarget,
type: topLevelType,
passive: (eventSystemFlags & IS_PASSIVE) !== 0,
passiveSupported: (eventSystemFlags & PASSIVE_NOT_SUPPORTED) === 0,
};
}
function createEventQueue(): EventQueue {
return {
bubble: null,
@@ -100,8 +289,8 @@ function processEvents(
}
}
function processEventQueue(eventQueue: EventQueue): void {
const {bubble, capture, discrete} = eventQueue;
export function processEventQueue(): void {
const {bubble, capture, discrete} = currentEventQueue;
if (discrete) {
interactiveUpdates(() => {
@@ -112,218 +301,6 @@ function processEventQueue(eventQueue: EventQueue): void {
}
}
// TODO add context methods for dispatching events
function DOMEventResponderContext(
topLevelType: DOMTopLevelEventType,
nativeEvent: AnyNativeEvent,
nativeEventTarget: EventTarget,
eventSystemFlags: EventSystemFlags,
) {
this.event = nativeEvent;
this.eventTarget = nativeEventTarget;
this.eventType = topLevelType;
this._flags = eventSystemFlags;
this._fiber = null;
this._responder = null;
this._discreteEvents = null;
this._nonDiscreteEvents = null;
this._isBatching = true;
this._eventQueue = createEventQueue();
}
DOMEventResponderContext.prototype.isPassive = function(): boolean {
return (this._flags & IS_PASSIVE) !== 0;
};
DOMEventResponderContext.prototype.isPassiveSupported = function(): boolean {
return (this._flags & PASSIVE_NOT_SUPPORTED) === 0;
};
DOMEventResponderContext.prototype.dispatchEvent = function(
possibleEventObject: Object,
{
capture,
discrete,
stopPropagation,
}: {
capture?: boolean,
discrete?: boolean,
stopPropagation?: boolean,
},
): void {
const eventQueue = this._eventQueue;
const {listener, target, type} = possibleEventObject;
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 = [];
}
} else {
events = eventQueue.bubble;
if (events === null) {
events = eventQueue.bubble = [];
}
}
if (discrete) {
eventQueue.discrete = true;
}
events.push(eventObject);
if (stopPropagation) {
eventsWithStopPropagation.add(eventObject);
}
};
DOMEventResponderContext.prototype.isTargetWithinEventComponent = function(
target: AnyNativeEvent,
): boolean {
const eventFiber = this._fiber;
if (target != null) {
let fiber = getClosestInstanceFromNode(target);
while (fiber !== null) {
if (fiber === eventFiber || fiber === eventFiber.alternate) {
return true;
}
fiber = fiber.return;
}
}
return false;
};
DOMEventResponderContext.prototype.isTargetWithinElement = function(
childTarget: EventTarget,
parentTarget: EventTarget,
): boolean {
const childFiber = getClosestInstanceFromNode(childTarget);
const parentFiber = getClosestInstanceFromNode(parentTarget);
let currentFiber = childFiber;
while (currentFiber !== null) {
if (currentFiber === parentFiber) {
return true;
}
currentFiber = currentFiber.return;
}
return false;
};
DOMEventResponderContext.prototype.addRootEventTypes = function(
rootEventTypes: Array<ReactEventResponderEventType>,
) {
const element = this.eventTarget.ownerDocument;
listenToResponderEventTypesImpl(rootEventTypes, element);
const eventComponent = this._fiber;
for (let i = 0; i < rootEventTypes.length; i++) {
const rootEventType = rootEventTypes[i];
const topLevelEventType =
typeof rootEventType === 'string' ? rootEventType : rootEventType.name;
let rootEventComponents = rootEventTypesToEventComponents.get(
topLevelEventType,
);
if (rootEventComponents === undefined) {
rootEventComponents = new Set();
rootEventTypesToEventComponents.set(
topLevelEventType,
rootEventComponents,
);
}
rootEventComponents.add(eventComponent);
}
};
DOMEventResponderContext.prototype.removeRootEventTypes = function(
rootEventTypes: Array<ReactEventResponderEventType>,
): void {
const eventComponent = this._fiber;
for (let i = 0; i < rootEventTypes.length; i++) {
const rootEventType = rootEventTypes[i];
const topLevelEventType =
typeof rootEventType === 'string' ? rootEventType : rootEventType.name;
let rootEventComponents = rootEventTypesToEventComponents.get(
topLevelEventType,
);
if (rootEventComponents !== undefined) {
rootEventComponents.delete(eventComponent);
}
}
};
DOMEventResponderContext.prototype.isPositionWithinTouchHitTarget = function() {
// TODO
};
DOMEventResponderContext.prototype.isTargetOwned = function(
targetElement: Element | Node,
): boolean {
const targetDoc = targetElement.ownerDocument;
return targetOwnership.has(targetDoc);
};
DOMEventResponderContext.prototype.requestOwnership = function(
targetElement: Element | Node,
): boolean {
const targetDoc = targetElement.ownerDocument;
if (targetOwnership.has(targetDoc)) {
return false;
}
targetOwnership.set(targetDoc, this._fiber);
return true;
};
DOMEventResponderContext.prototype.releaseOwnership = function(
targetElement: Element | Node,
): boolean {
const targetDoc = targetElement.ownerDocument;
if (!targetOwnership.has(targetDoc)) {
return false;
}
const owner = targetOwnership.get(targetDoc);
if (owner === this._fiber || owner === this._fiber.alternate) {
targetOwnership.delete(targetDoc);
return true;
}
return false;
};
DOMEventResponderContext.prototype.withAsyncDispatching = function(
func: () => void,
) {
const previousEventQueue = this._eventQueue;
this._eventQueue = createEventQueue();
try {
func();
batchedUpdates(processEventQueue, this._eventQueue);
} finally {
this._eventQueue = previousEventQueue;
}
};
function getTargetEventTypes(
eventTypes: Array<ReactEventResponderEventType>,
): Set<DOMTopLevelEventType> {
@@ -345,7 +322,7 @@ function getTargetEventTypes(
function handleTopLevelType(
topLevelType: DOMTopLevelEventType,
fiber: Fiber,
context: Object,
responderEvent: ResponderEvent,
isRootLevelEvent: boolean,
): void {
const responder: ReactEventResponder = fiber.type.responder;
@@ -360,9 +337,10 @@ function handleTopLevelType(
if (state === null && responder.createInitialState !== undefined) {
state = fiber.stateNode.state = responder.createInitialState(props);
}
context._fiber = fiber;
context._responder = responder;
responder.handleEvent(context, props, state);
currentFiber = fiber;
currentResponder = responder;
responder.onEvent(responderEvent, eventResponderContext, props, state);
}
export function runResponderEventsInBatch(
@@ -373,17 +351,18 @@ export function runResponderEventsInBatch(
eventSystemFlags: EventSystemFlags,
): void {
if (enableEventAPI) {
const context = new DOMEventResponderContext(
topLevelType,
currentEventQueue = createEventQueue();
const responderEvent = createResponderEvent(
((topLevelType: any): string),
nativeEvent,
nativeEventTarget,
((nativeEventTarget: any): Element | Document),
eventSystemFlags,
);
let node = targetFiber;
// Traverse up the fiber tree till we find event component fibers.
while (node !== null) {
if (node.tag === EventComponent) {
handleTopLevelType(topLevelType, node, context, false);
handleTopLevelType(topLevelType, node, responderEvent, false);
}
node = node.return;
}
@@ -399,11 +378,11 @@ export function runResponderEventsInBatch(
handleTopLevelType(
topLevelType,
rootEventComponentFiber,
context,
responderEvent,
true,
);
}
}
processEventQueue(context._eventQueue);
processEventQueue();
}
}
@@ -13,10 +13,10 @@ let React;
let ReactFeatureFlags;
let ReactDOM;
function createReactEventComponent(targetEventTypes, handleEvent) {
function createReactEventComponent(targetEventTypes, onEvent) {
const testEventResponder = {
targetEventTypes,
handleEvent,
onEvent,
};
return {
@@ -53,19 +53,19 @@ describe('DOMEventResponderSystem', () => {
container = null;
});
it('the event responder handleEvent() function should fire on click event', () => {
it('the event responder onEvent() function should fire on click event', () => {
let eventResponderFiredCount = 0;
let eventLog = [];
const buttonRef = React.createRef();
const ClickEventComponent = createReactEventComponent(
['click'],
(context, props) => {
(event, context, props) => {
eventResponderFiredCount++;
eventLog.push({
name: context.eventType,
passive: context.isPassive(),
passiveSupported: context.isPassiveSupported(),
name: event.type,
passive: event.passive,
passiveSupported: event.passiveSupported,
});
},
);
@@ -79,7 +79,7 @@ describe('DOMEventResponderSystem', () => {
ReactDOM.render(<Test />, container);
expect(container.innerHTML).toBe('<button>Click me!</button>');
// Clicking the button should trigger the event responder handleEvent()
// Clicking the button should trigger the event responder onEvent()
let buttonElement = buttonRef.current;
dispatchClickEvent(buttonElement);
expect(eventResponderFiredCount).toBe(1);
@@ -103,7 +103,7 @@ describe('DOMEventResponderSystem', () => {
expect(eventResponderFiredCount).toBe(2);
});
it('the event responder handleEvent() function should fire on click event (passive events forced)', () => {
it('the event responder onEvent() function should fire on click event (passive events forced)', () => {
// JSDOM does not support passive events, so this manually overrides the value to be true
const checkPassiveEvents = require('react-dom/src/events/checkPassiveEvents');
checkPassiveEvents.passiveBrowserEventsSupported = true;
@@ -113,11 +113,11 @@ describe('DOMEventResponderSystem', () => {
const ClickEventComponent = createReactEventComponent(
['click'],
(context, props) => {
(event, context, props) => {
eventLog.push({
name: context.eventType,
passive: context.isPassive(),
passiveSupported: context.isPassiveSupported(),
name: event.type,
passive: event.passive,
passiveSupported: event.passiveSupported,
});
},
);
@@ -130,7 +130,7 @@ describe('DOMEventResponderSystem', () => {
ReactDOM.render(<Test />, container);
// Clicking the button should trigger the event responder handleEvent()
// Clicking the button should trigger the event responder onEvent()
let buttonElement = buttonRef.current;
dispatchClickEvent(buttonElement);
expect(eventLog.length).toBe(1);
@@ -141,19 +141,19 @@ describe('DOMEventResponderSystem', () => {
});
});
it('nested event responders and their handleEvent() function should fire multiple times', () => {
it('nested event responders and their onEvent() function should fire multiple times', () => {
let eventResponderFiredCount = 0;
let eventLog = [];
const buttonRef = React.createRef();
const ClickEventComponent = createReactEventComponent(
['click'],
(context, props) => {
(event, context, props) => {
eventResponderFiredCount++;
eventLog.push({
name: context.eventType,
passive: context.isPassive(),
passiveSupported: context.isPassiveSupported(),
name: event.type,
passive: event.passive,
passiveSupported: event.passiveSupported,
});
},
);
@@ -168,7 +168,7 @@ describe('DOMEventResponderSystem', () => {
ReactDOM.render(<Test />, container);
// Clicking the button should trigger the event responder handleEvent()
// Clicking the button should trigger the event responder onEvent()
let buttonElement = buttonRef.current;
dispatchClickEvent(buttonElement);
expect(eventResponderFiredCount).toBe(2);
@@ -186,7 +186,7 @@ describe('DOMEventResponderSystem', () => {
});
});
it('nested event responders and their handleEvent() should fire in the correct order', () => {
it('nested event responders and their onEvent() should fire in the correct order', () => {
let eventLog = [];
const buttonRef = React.createRef();
@@ -214,7 +214,7 @@ describe('DOMEventResponderSystem', () => {
ReactDOM.render(<Test />, container);
// Clicking the button should trigger the event responder handleEvent()
// Clicking the button should trigger the event responder onEvent()
let buttonElement = buttonRef.current;
dispatchClickEvent(buttonElement);
@@ -227,14 +227,14 @@ describe('DOMEventResponderSystem', () => {
const ClickEventComponent = createReactEventComponent(
['click'],
(context, props) => {
(event, context, props) => {
if (props.onMagicClick) {
const event = {
const syntheticEvent = {
listener: props.onMagicClick,
target: context.eventTarget,
target: event.target,
type: 'magicclick',
};
context.dispatchEvent(event, {discrete: true});
context.dispatchEvent(syntheticEvent, {discrete: true});
}
},
);
@@ -251,7 +251,7 @@ describe('DOMEventResponderSystem', () => {
ReactDOM.render(<Test />, container);
// Clicking the button should trigger the event responder handleEvent()
// Clicking the button should trigger the event responder onEvent()
let buttonElement = buttonRef.current;
dispatchClickEvent(buttonElement);
@@ -264,37 +264,33 @@ describe('DOMEventResponderSystem', () => {
const LongPressEventComponent = createReactEventComponent(
['click'],
(context, props) => {
(event, context, props) => {
const pressEvent = {
listener: props.onPress,
target: context.eventTarget,
target: event.target,
type: 'press',
};
context.dispatchEvent(pressEvent, {discrete: true});
setTimeout(
() =>
context.withAsyncDispatching(() => {
if (props.onLongPress) {
const longPressEvent = {
listener: props.onLongPress,
target: context.eventTarget,
type: 'longpress',
};
context.dispatchEvent(longPressEvent, {discrete: true});
}
context.setTimeout(() => {
if (props.onLongPress) {
const longPressEvent = {
listener: props.onLongPress,
target: event.target,
type: 'longpress',
};
context.dispatchEvent(longPressEvent, {discrete: true});
}
if (props.onLongPressChange) {
const longPressChangeEvent = {
listener: props.onLongPressChange,
target: context.eventTarget,
type: 'longpresschange',
};
context.dispatchEvent(longPressChangeEvent, {discrete: true});
}
}),
500,
);
if (props.onLongPressChange) {
const longPressChangeEvent = {
listener: props.onLongPressChange,
target: event.target,
type: 'longpresschange',
};
context.dispatchEvent(longPressChangeEvent, {discrete: true});
}
}, 500);
},
);
@@ -313,7 +309,7 @@ describe('DOMEventResponderSystem', () => {
ReactDOM.render(<Test />, container);
// Clicking the button should trigger the event responder handleEvent()
// Clicking the button should trigger the event responder onEvent()
let buttonElement = buttonRef.current;
dispatchClickEvent(buttonElement);
jest.runAllTimers();
+20 -15
View File
@@ -7,7 +7,7 @@
* @flow
*/
import type {EventResponderContext} from 'events/EventTypes';
import type {ResponderEvent, ResponderContext} from 'events/EventTypes';
import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols';
const targetEventTypes = ['pointerdown', 'pointercancel'];
@@ -62,7 +62,7 @@ function createDragEvent(
}
function dispatchDragEvent(
context: EventResponderContext,
context: ResponderContext,
name: DragEventType,
listener: DragEvent => void,
state: DragState,
@@ -87,28 +87,31 @@ const DragResponder = {
y: 0,
};
},
handleEvent(
context: EventResponderContext,
onEvent(
event: ResponderEvent,
context: ResponderContext,
props: Object,
state: DragState,
): void {
const {eventTarget, eventType, event} = context;
const {target, type, nativeEvent} = event;
switch (eventType) {
switch (type) {
case 'touchstart':
case 'mousedown':
case 'pointerdown': {
if (!state.isDragging) {
if (props.onShouldClaimOwnership) {
context.releaseOwnership(state.dragTarget);
context.releaseOwnership();
}
const obj =
eventType === 'touchstart' ? (event: any).changedTouches[0] : event;
type === 'touchstart'
? (nativeEvent: any).changedTouches[0]
: nativeEvent;
const x = (state.startX = (obj: any).screenX);
const y = (state.startY = (obj: any).screenY);
state.x = x;
state.y = y;
state.dragTarget = eventTarget;
state.dragTarget = target;
state.isPointerDown = true;
if (props.onDragStart) {
@@ -121,19 +124,21 @@ const DragResponder = {
);
}
context.addRootEventTypes(rootEventTypes);
context.addRootEventTypes(target.ownerDocument, rootEventTypes);
}
break;
}
case 'touchmove':
case 'mousemove':
case 'pointermove': {
if (context.isPassive()) {
if (event.passive) {
return;
}
if (state.isPointerDown) {
const obj =
eventType === 'touchmove' ? (event: any).changedTouches[0] : event;
type === 'touchmove'
? (nativeEvent: any).changedTouches[0]
: nativeEvent;
const x = (obj: any).screenX;
const y = (obj: any).screenY;
state.x = x;
@@ -145,7 +150,7 @@ const DragResponder = {
props.onShouldClaimOwnership &&
props.onShouldClaimOwnership()
) {
shouldEnableDragging = context.requestOwnership(state.dragTarget);
shouldEnableDragging = context.requestOwnership();
}
if (shouldEnableDragging) {
state.isDragging = true;
@@ -181,7 +186,7 @@ const DragResponder = {
eventData,
);
}
(event: any).preventDefault();
(nativeEvent: any).preventDefault();
}
}
break;
@@ -193,7 +198,7 @@ const DragResponder = {
case 'pointerup': {
if (state.isDragging) {
if (props.onShouldClaimOwnership) {
context.releaseOwnership(state.dragTarget);
context.releaseOwnership();
}
if (props.onDragEnd) {
dispatchDragEvent(context, 'dragend', props.onDragEnd, state, true);
+21 -30
View File
@@ -7,7 +7,7 @@
* @flow
*/
import type {EventResponderContext} from 'events/EventTypes';
import type {ResponderEvent, ResponderContext} from 'events/EventTypes';
import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols';
type FocusProps = {
@@ -47,55 +47,45 @@ function createFocusEvent(
}
function dispatchFocusInEvents(
context: EventResponderContext,
event: ResponderEvent,
context: ResponderContext,
props: FocusProps,
) {
const {event, eventTarget} = context;
if (context.isTargetWithinEventComponent((event: any).relatedTarget)) {
const {nativeEvent, target} = event;
if (context.isTargetWithinEventComponent((nativeEvent: any).relatedTarget)) {
return;
}
if (props.onFocus) {
const syntheticEvent = createFocusEvent(
'focus',
eventTarget,
props.onFocus,
);
const syntheticEvent = createFocusEvent('focus', target, props.onFocus);
context.dispatchEvent(syntheticEvent, {discrete: true});
}
if (props.onFocusChange) {
const listener = () => {
props.onFocusChange(true);
};
const syntheticEvent = createFocusEvent(
'focuschange',
eventTarget,
listener,
);
const syntheticEvent = createFocusEvent('focuschange', target, listener);
context.dispatchEvent(syntheticEvent, {discrete: true});
}
}
function dispatchFocusOutEvents(
context: EventResponderContext,
event: ResponderEvent,
context: ResponderContext,
props: FocusProps,
) {
const {event, eventTarget} = context;
if (context.isTargetWithinEventComponent((event: any).relatedTarget)) {
const {nativeEvent, target} = event;
if (context.isTargetWithinEventComponent((nativeEvent: any).relatedTarget)) {
return;
}
if (props.onBlur) {
const syntheticEvent = createFocusEvent('blur', eventTarget, props.onBlur);
const syntheticEvent = createFocusEvent('blur', target, props.onBlur);
context.dispatchEvent(syntheticEvent, {discrete: true});
}
if (props.onFocusChange) {
const listener = () => {
props.onFocusChange(false);
};
const syntheticEvent = createFocusEvent(
'focuschange',
eventTarget,
listener,
);
const syntheticEvent = createFocusEvent('focuschange', target, listener);
context.dispatchEvent(syntheticEvent, {discrete: true});
}
}
@@ -107,24 +97,25 @@ const FocusResponder = {
isFocused: false,
};
},
handleEvent(
context: EventResponderContext,
onEvent(
event: ResponderEvent,
context: ResponderContext,
props: Object,
state: FocusState,
): void {
const {eventTarget, eventType} = context;
const {type} = event;
switch (eventType) {
switch (type) {
case 'focus': {
if (!state.isFocused && !context.isTargetOwned(eventTarget)) {
dispatchFocusInEvents(context, props);
if (!state.isFocused && !context.hasOwnership()) {
dispatchFocusInEvents(event, context, props);
state.isFocused = true;
}
break;
}
case 'blur': {
if (state.isFocused) {
dispatchFocusOutEvents(context, props);
dispatchFocusOutEvents(event, context, props);
state.isFocused = false;
}
break;
+31 -40
View File
@@ -7,7 +7,7 @@
* @flow
*/
import type {EventResponderContext} from 'events/EventTypes';
import type {ResponderEvent, ResponderContext} from 'events/EventTypes';
import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols';
type HoverProps = {
@@ -61,17 +61,18 @@ function createHoverEvent(
}
function dispatchHoverStartEvents(
context: EventResponderContext,
event: ResponderEvent,
context: ResponderContext,
props: HoverProps,
): void {
const {event, eventTarget} = context;
if (context.isTargetWithinEventComponent((event: any).relatedTarget)) {
const {nativeEvent, target} = event;
if (context.isTargetWithinEventComponent((nativeEvent: any).relatedTarget)) {
return;
}
if (props.onHoverStart) {
const syntheticEvent = createHoverEvent(
'hoverstart',
eventTarget,
target,
props.onHoverStart,
);
context.dispatchEvent(syntheticEvent, {discrete: true});
@@ -80,27 +81,24 @@ function dispatchHoverStartEvents(
const listener = () => {
props.onHoverChange(true);
};
const syntheticEvent = createHoverEvent(
'hoverchange',
eventTarget,
listener,
);
const syntheticEvent = createHoverEvent('hoverchange', target, listener);
context.dispatchEvent(syntheticEvent, {discrete: true});
}
}
function dispatchHoverEndEvents(
context: EventResponderContext,
event: ResponderEvent,
context: ResponderContext,
props: HoverProps,
) {
const {event, eventTarget} = context;
if (context.isTargetWithinEventComponent((event: any).relatedTarget)) {
const {nativeEvent, target} = event;
if (context.isTargetWithinEventComponent((nativeEvent: any).relatedTarget)) {
return;
}
if (props.onHoverEnd) {
const syntheticEvent = createHoverEvent(
'hoverend',
eventTarget,
target,
props.onHoverEnd,
);
context.dispatchEvent(syntheticEvent, {discrete: true});
@@ -109,11 +107,7 @@ function dispatchHoverEndEvents(
const listener = () => {
props.onHoverChange(false);
};
const syntheticEvent = createHoverEvent(
'hoverchange',
eventTarget,
listener,
);
const syntheticEvent = createHoverEvent('hoverchange', target, listener);
context.dispatchEvent(syntheticEvent, {discrete: true});
}
}
@@ -127,14 +121,15 @@ const HoverResponder = {
isTouched: false,
};
},
handleEvent(
context: EventResponderContext,
onEvent(
event: ResponderEvent,
context: ResponderContext,
props: HoverProps,
state: HoverState,
): void {
const {eventType, eventTarget, event} = context;
const {type, nativeEvent} = event;
switch (eventType) {
switch (type) {
/**
* Prevent hover events when touch is being used.
*/
@@ -147,25 +142,21 @@ const HoverResponder = {
case 'pointerover':
case 'mouseover': {
if (
!state.isHovered &&
!state.isTouched &&
!context.isTargetOwned(eventTarget)
) {
if ((event: any).pointerType === 'touch') {
if (!state.isHovered && !state.isTouched && !context.hasOwnership()) {
if ((nativeEvent: any).pointerType === 'touch') {
state.isTouched = true;
return;
}
if (
context.isPositionWithinTouchHitTarget(
(event: any).x,
(event: any).y,
(nativeEvent: any).x,
(nativeEvent: any).y,
)
) {
state.isInHitSlop = true;
return;
}
dispatchHoverStartEvents(context, props);
dispatchHoverStartEvents(event, context, props);
state.isHovered = true;
}
break;
@@ -173,7 +164,7 @@ const HoverResponder = {
case 'pointerout':
case 'mouseout': {
if (state.isHovered && !state.isTouched) {
dispatchHoverEndEvents(context, props);
dispatchHoverEndEvents(event, context, props);
state.isHovered = false;
}
state.isInHitSlop = false;
@@ -185,22 +176,22 @@ const HoverResponder = {
if (state.isInHitSlop) {
if (
!context.isPositionWithinTouchHitTarget(
(event: any).x,
(event: any).y,
(nativeEvent: any).x,
(nativeEvent: any).y,
)
) {
dispatchHoverStartEvents(context, props);
dispatchHoverStartEvents(event, context, props);
state.isHovered = true;
state.isInHitSlop = false;
}
} else if (
state.isHovered &&
context.isPositionWithinTouchHitTarget(
(event: any).x,
(event: any).y,
(nativeEvent: any).x,
(nativeEvent: any).y,
)
) {
dispatchHoverEndEvents(context, props);
dispatchHoverEndEvents(event, context, props);
state.isHovered = false;
state.isInHitSlop = true;
}
@@ -209,7 +200,7 @@ const HoverResponder = {
}
case 'pointercancel': {
if (state.isHovered && !state.isTouched) {
dispatchHoverEndEvents(context, props);
dispatchHoverEndEvents(event, context, props);
state.isHovered = false;
state.isTouched = false;
}
+83 -59
View File
@@ -7,7 +7,7 @@
* @flow
*/
import type {EventResponderContext} from 'events/EventTypes';
import type {ResponderEvent, ResponderContext} from 'events/EventTypes';
import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols';
type PressProps = {
@@ -86,7 +86,7 @@ function createPressEvent(
}
function dispatchEvent(
context: EventResponderContext,
context: ResponderContext,
state: PressState,
name: PressEventType,
listener: (e: Object) => void,
@@ -97,7 +97,7 @@ function dispatchEvent(
}
function dispatchPressChangeEvent(
context: EventResponderContext,
context: ResponderContext,
props: PressProps,
state: PressState,
): void {
@@ -108,7 +108,7 @@ function dispatchPressChangeEvent(
}
function dispatchLongPressChangeEvent(
context: EventResponderContext,
context: ResponderContext,
props: PressProps,
state: PressState,
): void {
@@ -119,7 +119,7 @@ function dispatchLongPressChangeEvent(
}
function dispatchPressStartEvents(
context: EventResponderContext,
context: ResponderContext,
props: PressProps,
state: PressState,
): void {
@@ -138,34 +138,30 @@ function dispatchPressStartEvents(
DEFAULT_LONG_PRESS_DELAY_MS,
);
state.longPressTimeout = setTimeout(
() =>
context.withAsyncDispatching(() => {
state.isLongPressed = true;
state.longPressTimeout = null;
state.longPressTimeout = context.setTimeout(() => {
state.isLongPressed = true;
state.longPressTimeout = null;
if (props.onLongPress) {
const listener = e => {
props.onLongPress(e);
// TODO address this again at some point
// if (e.nativeEvent.defaultPrevented) {
// state.defaultPrevented = true;
// }
};
dispatchEvent(context, state, 'longpress', listener);
}
if (props.onLongPress) {
const listener = e => {
props.onLongPress(e);
// TODO address this again at some point
// if (e.nativeEvent.defaultPrevented) {
// state.defaultPrevented = true;
// }
};
dispatchEvent(context, state, 'longpress', listener);
}
if (props.onLongPressChange) {
dispatchLongPressChangeEvent(context, props, state);
}
}),
delayLongPress,
);
if (props.onLongPressChange) {
dispatchLongPressChangeEvent(context, props, state);
}
}, delayLongPress);
}
}
function dispatchPressEndEvents(
context: EventResponderContext,
context: ResponderContext,
props: PressProps,
state: PressState,
): void {
@@ -206,6 +202,21 @@ function calculateDelayMS(delay: ?number, min = 0, fallback = 0) {
return Math.max(min, maybeNumber != null ? maybeNumber : fallback);
}
function unmountResponder(
context: ResponderContext,
props: PressProps,
state: PressState,
): void {
if (state.isPressed) {
state.isPressed = false;
dispatchPressEndEvents(context, props, state);
if (state.longPressTimeout !== null) {
clearTimeout(state.longPressTimeout);
state.longPressTimeout = null;
}
}
}
const PressResponder = {
targetEventTypes,
createInitialState(): PressState {
@@ -219,14 +230,15 @@ const PressResponder = {
shouldSkipMouseAfterTouch: false,
};
},
handleEvent(
context: EventResponderContext,
onEvent(
event: ResponderEvent,
context: ResponderContext,
props: PressProps,
state: PressState,
): void {
const {eventTarget, eventType, event} = context;
const {target, type, nativeEvent} = event;
switch (eventType) {
switch (type) {
/**
* Respond to pointer events and fall back to mouse.
*/
@@ -234,29 +246,29 @@ const PressResponder = {
case 'mousedown': {
if (
!state.isPressed &&
!context.isTargetOwned(eventTarget) &&
!context.hasOwnership() &&
!state.shouldSkipMouseAfterTouch
) {
if (
(event: any).pointerType === 'mouse' ||
eventType === 'mousedown'
(nativeEvent: any).pointerType === 'mouse' ||
type === 'mousedown'
) {
if (
// Ignore right- and middle-clicks
event.button === 1 ||
event.button === 2 ||
nativeEvent.button === 1 ||
nativeEvent.button === 2 ||
// Ignore pressing on hit slop area with mouse
context.isPositionWithinTouchHitTarget(
(event: any).x,
(event: any).y,
(nativeEvent: any).x,
(nativeEvent: any).y,
)
) {
return;
}
}
state.pressTarget = eventTarget;
state.pressTarget = target;
dispatchPressStartEvents(context, props, state);
context.addRootEventTypes(rootEventTypes);
context.addRootEventTypes(target.ownerDocument, rootEventTypes);
}
break;
}
@@ -273,7 +285,7 @@ const PressResponder = {
dispatchPressEndEvents(context, props, state);
if (state.pressTarget !== null && props.onPress) {
if (context.isTargetWithinElement(eventTarget, state.pressTarget)) {
if (context.isTargetWithinElement(target, state.pressTarget)) {
if (
!(
wasLongPressed &&
@@ -303,16 +315,16 @@ const PressResponder = {
* support for pointer events.
*/
case 'touchstart': {
if (!state.isPressed && !context.isTargetOwned(eventTarget)) {
if (!state.isPressed && !context.hasOwnership()) {
// We bail out of polyfilling anchor tags, given the same heuristics
// explained above in regards to needing to use click events.
if (isAnchorTagElement(eventTarget)) {
if (isAnchorTagElement(target)) {
state.isAnchorTouched = true;
return;
}
state.pressTarget = eventTarget;
state.pressTarget = target;
dispatchPressStartEvents(context, props, state);
context.addRootEventTypes(rootEventTypes);
context.addRootEventTypes(target.ownerDocument, rootEventTypes);
}
break;
}
@@ -326,17 +338,17 @@ const PressResponder = {
dispatchPressEndEvents(context, props, state);
if (eventType !== 'touchcancel' && props.onPress) {
if (type !== 'touchcancel' && props.onPress) {
// Find if the X/Y of the end touch is still that of the original target
const changedTouch = (event: any).changedTouches[0];
const doc = (eventTarget: any).ownerDocument;
const target = doc.elementFromPoint(
const changedTouch = (nativeEvent: any).changedTouches[0];
const doc = (target: any).ownerDocument;
const fromTarget = doc.elementFromPoint(
changedTouch.screenX,
changedTouch.screenY,
);
if (
target !== null &&
context.isTargetWithinEventComponent(target)
fromTarget !== null &&
context.isTargetWithinEventComponent(fromTarget)
) {
if (
!(
@@ -363,21 +375,21 @@ const PressResponder = {
if (
!state.isPressed &&
!state.isLongPressed &&
!context.isTargetOwned(eventTarget) &&
isValidKeyPress((event: any).key)
!context.hasOwnership() &&
isValidKeyPress((nativeEvent: any).key)
) {
// Prevent spacebar press from scrolling the window
if ((event: any).key === ' ') {
(event: any).preventDefault();
if ((nativeEvent: any).key === ' ') {
(nativeEvent: any).preventDefault();
}
state.pressTarget = eventTarget;
state.pressTarget = target;
dispatchPressStartEvents(context, props, state);
context.addRootEventTypes(rootEventTypes);
context.addRootEventTypes(target.ownerDocument, rootEventTypes);
}
break;
}
case 'keyup': {
if (state.isPressed && isValidKeyPress((event: any).key)) {
if (state.isPressed && isValidKeyPress((nativeEvent: any).key)) {
const wasLongPressed = state.isLongPressed;
dispatchPressEndEvents(context, props, state);
if (state.pressTarget !== null && props.onPress) {
@@ -410,12 +422,24 @@ const PressResponder = {
case 'click': {
if (state.defaultPrevented) {
(event: any).preventDefault();
(nativeEvent: any).preventDefault();
state.defaultPrevented = false;
}
}
}
},
// TODO This method doesn't work as of yet
onUnmount(context: ResponderContext, props: PressProps, state: PressState) {
unmountResponder(context, props, state);
},
// TODO This method doesn't work as of yet
onOwnershipChange(
context: ResponderContext,
props: PressProps,
state: PressState,
) {
unmountResponder(context, props, state);
},
};
export default {
+19 -18
View File
@@ -7,7 +7,7 @@
* @flow
*/
import type {EventResponderContext} from 'events/EventTypes';
import type {ResponderEvent, ResponderContext} from 'events/EventTypes';
import {REACT_EVENT_COMPONENT_TYPE} from 'shared/ReactSymbols';
const targetEventTypes = ['pointerdown', 'pointercancel'];
@@ -52,7 +52,7 @@ function createSwipeEvent(
}
function dispatchSwipeEvent(
context: EventResponderContext,
context: ResponderContext,
name: SwipeEventType,
listener: SwipeEvent => void,
state: SwipeState,
@@ -91,21 +91,22 @@ const SwipeResponder = {
y: 0,
};
},
handleEvent(
context: EventResponderContext,
onEvent(
event: ResponderEvent,
context: ResponderContext,
props: Object,
state: SwipeState,
): void {
const {eventTarget, eventType, event} = context;
const {target, type, nativeEvent} = event;
switch (eventType) {
switch (type) {
case 'touchstart':
case 'mousedown':
case 'pointerdown': {
if (!state.isSwiping && !context.isTargetOwned(eventTarget)) {
if (!state.isSwiping && !context.hasOwnership()) {
let obj = event;
if (eventType === 'touchstart') {
obj = (event: any).targetTouches[0];
if (type === 'touchstart') {
obj = (nativeEvent: any).targetTouches[0];
state.touchId = obj.identifier;
}
const x = (obj: any).screenX;
@@ -114,7 +115,7 @@ const SwipeResponder = {
let shouldEnableSwiping = true;
if (props.onShouldClaimOwnership && props.onShouldClaimOwnership()) {
shouldEnableSwiping = context.requestOwnership(eventTarget);
shouldEnableSwiping = context.requestOwnership();
}
if (shouldEnableSwiping) {
state.isSwiping = true;
@@ -122,8 +123,8 @@ const SwipeResponder = {
state.startY = y;
state.x = x;
state.y = y;
state.swipeTarget = eventTarget;
context.addRootEventTypes(rootEventTypes);
state.swipeTarget = target;
context.addRootEventTypes(target.ownerDocument, rootEventTypes);
} else {
state.touchId = null;
}
@@ -133,13 +134,13 @@ const SwipeResponder = {
case 'touchmove':
case 'mousemove':
case 'pointermove': {
if (context.isPassive()) {
if (event.passive) {
return;
}
if (state.isSwiping) {
let obj = null;
if (eventType === 'touchmove') {
const targetTouches = (event: any).targetTouches;
if (type === 'touchmove') {
const targetTouches = (nativeEvent: any).targetTouches;
for (let i = 0; i < targetTouches.length; i++) {
if (state.touchId === targetTouches[i].identifier) {
obj = targetTouches[i];
@@ -147,7 +148,7 @@ const SwipeResponder = {
}
}
} else {
obj = event;
obj = nativeEvent;
}
if (obj === null) {
state.isSwiping = false;
@@ -178,7 +179,7 @@ const SwipeResponder = {
false,
eventData,
);
(event: any).preventDefault();
(nativeEvent: any).preventDefault();
}
}
break;
@@ -193,7 +194,7 @@ const SwipeResponder = {
return;
}
if (props.onShouldClaimOwnership) {
context.releaseOwnership(state.swipeTarget);
context.releaseOwnership();
}
const direction = state.direction;
const lastDirection = state.lastDirection;
@@ -22,7 +22,7 @@ let TouchHitTarget;
const noOpResponder = {
targetEventTypes: [],
handleEvent() {},
onEvent() {},
};
function createReactEventComponent() {
+1
View File
@@ -624,6 +624,7 @@ export function createFiberFromEventComponent(
fiber.elementType = eventComponent;
fiber.type = eventComponent;
fiber.stateNode = {
context: null,
props: pendingProps,
state: null,
};
+14 -1
View File
@@ -7,6 +7,8 @@
* @flow
*/
import type {ResponderEvent, ResponderContext} from 'events/EventTypes';
export type ReactNode =
| React$Element<any>
| ReactPortal
@@ -88,7 +90,18 @@ export type ReactEventResponderEventType =
export type ReactEventResponder = {
targetEventTypes: Array<ReactEventResponderEventType>,
createInitialState?: (props: Object) => Object,
handleEvent: (context: Object, props: Object, state: Object) => void,
onEvent: (
event: ResponderEvent,
context: ResponderContext,
props: Object,
state: Object,
) => void,
onUnmount: (context: ResponderContext, props: Object, state: Object) => void,
onOwnershipChange: (
context: ResponderContext,
props: Object,
state: Object,
) => void,
};
export type ReactEventComponent = {|