Skip to content

Commit

Permalink
Experimental Event API: adds stopPropagation by default to Press (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
trueadm authored Apr 11, 2019
1 parent a9eff32 commit 9672cf6
Show file tree
Hide file tree
Showing 5 changed files with 249 additions and 70 deletions.
173 changes: 117 additions & 56 deletions packages/react-dom/src/events/DOMEventResponderSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,11 @@ export function setListenToResponderEventTypes(
listenToResponderEventTypesImpl = _listenToResponderEventTypesImpl;
}

type EventObjectTypes = {|stopPropagation: true|} | $Shape<PartialEventObject>;

type EventQueue = {
bubble: null | Array<$Shape<PartialEventObject>>,
capture: null | Array<$Shape<PartialEventObject>>,
bubble: null | Array<EventObjectTypes>,
capture: null | Array<EventObjectTypes>,
discrete: boolean,
};

Expand All @@ -53,16 +55,38 @@ type PartialEventObject = {
type: string,
};

type ResponderTimeout = {|
id: TimeoutID,
timers: Map<Symbol, ResponderTimer>,
|};

type ResponderTimer = {|
instance: ReactEventComponentInstance,
func: () => void,
id: Symbol,
|};

const activeTimeouts: Map<Symbol, ResponderTimeout> = new Map();
const rootEventTypesToEventComponentInstances: Map<
DOMTopLevelEventType | string,
Set<ReactEventComponentInstance>,
> = new Map();
const targetEventTypeCached: Map<
Array<ReactEventResponderEventType>,
Set<DOMTopLevelEventType>,
> = new Map();
const ownershipChangeListeners: Set<ReactEventComponentInstance> = new Set();

let currentTimers = new Map();
let currentOwner = null;
let currentInstance: ReactEventComponentInstance;
let currentEventQueue: EventQueue;

const eventResponderContext: ReactResponderContext = {
dispatchEvent(
possibleEventObject: Object,
{capture, discrete, stopPropagation}: ReactResponderDispatchEventOptions,
{capture, discrete}: ReactResponderDispatchEventOptions,
): void {
const eventQueue = currentEventQueue;
const {listener, target, type} = possibleEventObject;

if (listener == null || target == null || type == null) {
Expand All @@ -89,27 +113,15 @@ const eventResponderContext: ReactResponderContext = {
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 = [];
}
}
const events = getEventsFromEventQueue(capture);
if (discrete) {
eventQueue.discrete = true;
currentEventQueue.discrete = true;
}
events.push(eventObject);

if (stopPropagation) {
eventsWithStopPropagation.add(eventObject);
}
},
dispatchStopPropagation(capture?: boolean) {
const events = getEventsFromEventQueue();
events.push({stopPropagation: true});
},
isPositionWithinTouchHitTarget(doc: Document, x: number, y: number): boolean {
// This isn't available in some environments (JSDOM)
Expand Down Expand Up @@ -222,21 +234,42 @@ const eventResponderContext: ReactResponderContext = {
triggerOwnershipListeners();
return false;
},
setTimeout(func: () => void, delay): TimeoutID {
const contextInstance = currentInstance;
return setTimeout(() => {
const previousEventQueue = currentEventQueue;
const previousInstance = currentInstance;
currentEventQueue = createEventQueue();
currentInstance = contextInstance;
try {
func();
batchedUpdates(processEventQueue, currentEventQueue);
} finally {
currentInstance = previousInstance;
currentEventQueue = previousEventQueue;
setTimeout(func: () => void, delay): Symbol {
if (currentTimers === null) {
currentTimers = new Map();
}
let timeout = currentTimers.get(delay);

const timerId = Symbol();
if (timeout === undefined) {
const timers = new Map();
const id = setTimeout(() => {
processTimers(timers);
}, delay);
timeout = {
id,
timers,
};
currentTimers.set(delay, timeout);
}
timeout.timers.set(timerId, {
instance: currentInstance,
func,
id: timerId,
});
activeTimeouts.set(timerId, timeout);
return timerId;
},
clearTimeout(timerId: Symbol): void {
const timeout = activeTimeouts.get(timerId);

if (timeout !== undefined) {
const timers = timeout.timers;
timers.delete(timerId);
if (timers.size === 0) {
clearTimeout(timeout.id);
}
}, delay);
}
},
getEventTargetsFromTarget(
target: Element | Document,
Expand Down Expand Up @@ -292,6 +325,46 @@ const eventResponderContext: ReactResponderContext = {
},
};

function getEventsFromEventQueue(capture?: boolean): Array<EventObjectTypes> {
let events;
if (capture) {
events = currentEventQueue.capture;
if (events === null) {
events = currentEventQueue.capture = [];
}
} else {
events = currentEventQueue.bubble;
if (events === null) {
events = currentEventQueue.bubble = [];
}
}
return events;
}

function processTimers(timers: Map<Symbol, ResponderTimer>): void {
const previousEventQueue = currentEventQueue;
const previousInstance = currentInstance;
currentEventQueue = createEventQueue();

try {
const timersArr = Array.from(timers.values());
for (let i = 0; i < timersArr.length; i++) {
const {instance, func, id} = timersArr[i];
currentInstance = instance;
try {
func();
} finally {
activeTimeouts.delete(id);
}
}
batchedUpdates(processEventQueue, currentEventQueue);
} finally {
currentInstance = previousInstance;
currentEventQueue = previousEventQueue;
currentTimers = null;
}
}

function queryEventTarget(
child: Fiber,
queryType: void | Symbol | number,
Expand All @@ -306,20 +379,6 @@ function queryEventTarget(
return true;
}

const rootEventTypesToEventComponentInstances: Map<
DOMTopLevelEventType | string,
Set<ReactEventComponentInstance>,
> = 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();
const ownershipChangeListeners: Set<ReactEventComponentInstance> = new Set();

function createResponderEvent(
topLevelType: string,
nativeEvent: AnyNativeEvent,
Expand Down Expand Up @@ -350,27 +409,27 @@ function processEvent(event: $Shape<PartialEventObject>): void {
}

function processEvents(
bubble: null | Array<$Shape<PartialEventObject>>,
capture: null | Array<$Shape<PartialEventObject>>,
bubble: null | Array<EventObjectTypes>,
capture: null | Array<EventObjectTypes>,
): void {
let i, length;

if (capture !== null) {
for (i = capture.length; i-- > 0; ) {
const event = capture[i];
processEvent(capture[i]);
if (eventsWithStopPropagation.has(event)) {
if (event.stopPropagation === true) {
return;
}
processEvent(((event: any): $Shape<PartialEventObject>));
}
}
if (bubble !== null) {
for (i = 0, length = bubble.length; i < length; ++i) {
const event = bubble[i];
processEvent(event);
if (eventsWithStopPropagation.has(event)) {
if (event.stopPropagation === true) {
return;
}
processEvent(((event: any): $Shape<PartialEventObject>));
}
}
}
Expand Down Expand Up @@ -475,6 +534,7 @@ export function runResponderEventsInBatch(
}
}
processEventQueue();
currentTimers = null;
}
}

Expand Down Expand Up @@ -518,6 +578,7 @@ export function unmountEventResponder(
} finally {
currentEventQueue = previousEventQueue;
currentInstance = previousInstance;
currentTimers = null;
}
}
if (currentOwner === eventComponentInstance) {
Expand Down
8 changes: 4 additions & 4 deletions packages/react-events/src/Hover.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ type HoverState = {
isHovered: boolean,
isInHitSlop: boolean,
isTouched: boolean,
hoverStartTimeout: null | TimeoutID,
hoverEndTimeout: null | TimeoutID,
hoverStartTimeout: null | Symbol,
hoverEndTimeout: null | Symbol,
};

type HoverEventType = 'hoverstart' | 'hoverend' | 'hoverchange';
Expand Down Expand Up @@ -97,7 +97,7 @@ function dispatchHoverStartEvents(
state.isHovered = true;

if (state.hoverEndTimeout !== null) {
clearTimeout(state.hoverEndTimeout);
context.clearTimeout(state.hoverEndTimeout);
state.hoverEndTimeout = null;
}

Expand Down Expand Up @@ -148,7 +148,7 @@ function dispatchHoverEndEvents(
state.isHovered = false;

if (state.hoverStartTimeout !== null) {
clearTimeout(state.hoverStartTimeout);
context.clearTimeout(state.hoverStartTimeout);
state.hoverStartTimeout = null;
}

Expand Down
36 changes: 29 additions & 7 deletions packages/react-events/src/Press.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,21 @@ type PressProps = {
left: number,
},
preventDefault: boolean,
stopPropagation: boolean,
};

type PressState = {
didDispatchEvent: boolean,
isActivePressed: boolean,
isActivePressStart: boolean,
isAnchorTouched: boolean,
isLongPressed: boolean,
isPressed: boolean,
isPressWithinResponderRegion: boolean,
longPressTimeout: null | TimeoutID,
longPressTimeout: null | Symbol,
pressTarget: null | Element | Document,
pressEndTimeout: null | TimeoutID,
pressStartTimeout: null | TimeoutID,
pressEndTimeout: null | Symbol,
pressStartTimeout: null | Symbol,
responderRegion: null | $ReadOnly<{|
bottom: number,
left: number,
Expand Down Expand Up @@ -124,7 +126,10 @@ function dispatchEvent(
): void {
const target = ((state.pressTarget: any): Element | Document);
const syntheticEvent = createPressEvent(name, target, listener);
context.dispatchEvent(syntheticEvent, {discrete: true});
context.dispatchEvent(syntheticEvent, {
discrete: true,
});
state.didDispatchEvent = true;
}

function dispatchPressChangeEvent(
Expand Down Expand Up @@ -185,7 +190,7 @@ function dispatchPressStartEvents(
state.isPressed = true;

if (state.pressEndTimeout !== null) {
clearTimeout(state.pressEndTimeout);
context.clearTimeout(state.pressEndTimeout);
state.pressEndTimeout = null;
}

Expand All @@ -211,6 +216,14 @@ function dispatchPressStartEvents(
if (props.onLongPressChange) {
dispatchLongPressChangeEvent(context, props, state);
}
if (state.didDispatchEvent) {
const shouldStopPropagation =
props.stopPropagation === undefined ? true : props.stopPropagation;
if (shouldStopPropagation) {
context.dispatchStopPropagation();
}
state.didDispatchEvent = false;
}
}, delayLongPress);
}
};
Expand Down Expand Up @@ -243,12 +256,12 @@ function dispatchPressEndEvents(
state.isPressed = false;

if (state.longPressTimeout !== null) {
clearTimeout(state.longPressTimeout);
context.clearTimeout(state.longPressTimeout);
state.longPressTimeout = null;
}

if (!wasActivePressStart && state.pressStartTimeout !== null) {
clearTimeout(state.pressStartTimeout);
context.clearTimeout(state.pressStartTimeout);
state.pressStartTimeout = null;
// don't activate if a press has moved beyond the responder region
if (state.isPressWithinResponderRegion) {
Expand Down Expand Up @@ -356,6 +369,7 @@ const PressResponder = {
targetEventTypes,
createInitialState(): PressState {
return {
didDispatchEvent: false,
isActivePressed: false,
isActivePressStart: false,
isAnchorTouched: false,
Expand Down Expand Up @@ -602,6 +616,14 @@ const PressResponder = {
}
}
}
if (state.didDispatchEvent) {
const shouldStopPropagation =
props.stopPropagation === undefined ? true : props.stopPropagation;
if (shouldStopPropagation) {
context.dispatchStopPropagation();
}
state.didDispatchEvent = false;
}
},
onUnmount(
context: ReactResponderContext,
Expand Down
Loading

0 comments on commit 9672cf6

Please sign in to comment.