Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experimental Event API: adds stopPropagation by default to Press #15384

Merged
merged 3 commits into from
Apr 11, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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