mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Experimental Event API: adds stopPropagation by default to Press (#15384)
This commit is contained in:
+117
-56
@@ -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,
|
||||
};
|
||||
|
||||
@@ -53,6 +55,29 @@ 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;
|
||||
@@ -60,9 +85,8 @@ 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) {
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
@@ -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>));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -475,6 +534,7 @@ export function runResponderEventsInBatch(
|
||||
}
|
||||
}
|
||||
processEventQueue();
|
||||
currentTimers = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -518,6 +578,7 @@ export function unmountEventResponder(
|
||||
} finally {
|
||||
currentEventQueue = previousEventQueue;
|
||||
currentInstance = previousInstance;
|
||||
currentTimers = null;
|
||||
}
|
||||
}
|
||||
if (currentOwner === eventComponentInstance) {
|
||||
|
||||
Vendored
+4
-4
@@ -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';
|
||||
@@ -97,7 +97,7 @@ function dispatchHoverStartEvents(
|
||||
state.isHovered = true;
|
||||
|
||||
if (state.hoverEndTimeout !== null) {
|
||||
clearTimeout(state.hoverEndTimeout);
|
||||
context.clearTimeout(state.hoverEndTimeout);
|
||||
state.hoverEndTimeout = null;
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ function dispatchHoverEndEvents(
|
||||
state.isHovered = false;
|
||||
|
||||
if (state.hoverStartTimeout !== null) {
|
||||
clearTimeout(state.hoverStartTimeout);
|
||||
context.clearTimeout(state.hoverStartTimeout);
|
||||
state.hoverStartTimeout = null;
|
||||
}
|
||||
|
||||
|
||||
Vendored
+29
-7
@@ -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,
|
||||
@@ -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(
|
||||
@@ -185,7 +190,7 @@ function dispatchPressStartEvents(
|
||||
state.isPressed = true;
|
||||
|
||||
if (state.pressEndTimeout !== null) {
|
||||
clearTimeout(state.pressEndTimeout);
|
||||
context.clearTimeout(state.pressEndTimeout);
|
||||
state.pressEndTimeout = null;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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) {
|
||||
@@ -356,6 +369,7 @@ const PressResponder = {
|
||||
targetEventTypes,
|
||||
createInitialState(): PressState {
|
||||
return {
|
||||
didDispatchEvent: false,
|
||||
isActivePressed: false,
|
||||
isActivePressStart: false,
|
||||
isAnchorTouched: false,
|
||||
@@ -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,
|
||||
|
||||
@@ -381,6 +381,38 @@ describe('Event responder: Press', () => {
|
||||
expect(onPressChange).toHaveBeenCalledTimes(2);
|
||||
expect(onPressChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('is called but does not bubble', () => {
|
||||
const element = (
|
||||
<Press onPressChange={onPressChange}>
|
||||
<Press onPressChange={onPressChange}>
|
||||
<div ref={ref} />
|
||||
</Press>
|
||||
</Press>
|
||||
);
|
||||
ReactDOM.render(element, container);
|
||||
|
||||
ref.current.dispatchEvent(createPointerEvent('pointerdown'));
|
||||
expect(onPressChange).toHaveBeenCalledTimes(1);
|
||||
ref.current.dispatchEvent(createPointerEvent('pointerup'));
|
||||
expect(onPressChange).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('is called and bubbles correctly with stopPropagation set to false', () => {
|
||||
const element = (
|
||||
<Press onPressChange={onPressChange}>
|
||||
<Press onPressChange={onPressChange} stopPropagation={false}>
|
||||
<div ref={ref} />
|
||||
</Press>
|
||||
</Press>
|
||||
);
|
||||
ReactDOM.render(element, container);
|
||||
|
||||
ref.current.dispatchEvent(createPointerEvent('pointerdown'));
|
||||
expect(onPressChange).toHaveBeenCalledTimes(2);
|
||||
ref.current.dispatchEvent(createPointerEvent('pointerup'));
|
||||
expect(onPressChange).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onPress', () => {
|
||||
@@ -429,6 +461,36 @@ describe('Event responder: Press', () => {
|
||||
// ref.current.dispatchEvent(createPointerEvent('touchend'));
|
||||
// expect(onPress).toHaveBeenCalledTimes(1);
|
||||
// });
|
||||
|
||||
it('is called but does not bubble', () => {
|
||||
const element = (
|
||||
<Press onPress={onPress}>
|
||||
<Press onPress={onPress}>
|
||||
<div ref={ref} />
|
||||
</Press>
|
||||
</Press>
|
||||
);
|
||||
ReactDOM.render(element, container);
|
||||
|
||||
ref.current.dispatchEvent(createPointerEvent('pointerdown'));
|
||||
ref.current.dispatchEvent(createPointerEvent('pointerup'));
|
||||
expect(onPress).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('is called and bubbles correctly with stopPropagation set to false', () => {
|
||||
const element = (
|
||||
<Press onPress={onPress}>
|
||||
<Press onPress={onPress} stopPropagation={false}>
|
||||
<div ref={ref} />
|
||||
</Press>
|
||||
</Press>
|
||||
);
|
||||
ReactDOM.render(element, container);
|
||||
|
||||
ref.current.dispatchEvent(createPointerEvent('pointerdown'));
|
||||
ref.current.dispatchEvent(createPointerEvent('pointerup'));
|
||||
expect(onPress).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onLongPress', () => {
|
||||
@@ -477,6 +539,38 @@ describe('Event responder: Press', () => {
|
||||
expect(onLongPress).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('is called but does not bubble', () => {
|
||||
const element = (
|
||||
<Press onLongPress={onLongPress}>
|
||||
<Press onLongPress={onLongPress}>
|
||||
<div ref={ref} />
|
||||
</Press>
|
||||
</Press>
|
||||
);
|
||||
ReactDOM.render(element, container);
|
||||
|
||||
ref.current.dispatchEvent(createPointerEvent('pointerdown'));
|
||||
jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY);
|
||||
ref.current.dispatchEvent(createPointerEvent('pointerup'));
|
||||
expect(onLongPress).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('is called and bubbles correctly with stopPropagation set to false', () => {
|
||||
const element = (
|
||||
<Press onLongPress={onLongPress}>
|
||||
<Press onLongPress={onLongPress} stopPropagation={false}>
|
||||
<div ref={ref} />
|
||||
</Press>
|
||||
</Press>
|
||||
);
|
||||
ReactDOM.render(element, container);
|
||||
|
||||
ref.current.dispatchEvent(createPointerEvent('pointerdown'));
|
||||
jest.advanceTimersByTime(DEFAULT_LONG_PRESS_DELAY);
|
||||
ref.current.dispatchEvent(createPointerEvent('pointerup'));
|
||||
expect(onLongPress).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
describe('delayLongPress', () => {
|
||||
it('can be configured', () => {
|
||||
const element = (
|
||||
@@ -914,7 +1008,8 @@ describe('Event responder: Press', () => {
|
||||
onPress={createEventHandler('inner: onPress')}
|
||||
onPressChange={createEventHandler('inner: onPressChange')}
|
||||
onPressStart={createEventHandler('inner: onPressStart')}
|
||||
onPressEnd={createEventHandler('inner: onPressEnd')}>
|
||||
onPressEnd={createEventHandler('inner: onPressEnd')}
|
||||
stopPropagation={false}>
|
||||
<div
|
||||
ref={ref}
|
||||
onPointerDown={createEventHandler('pointerdown')}
|
||||
|
||||
@@ -140,7 +140,6 @@ export type ReactResponderEvent = {
|
||||
export type ReactResponderDispatchEventOptions = {
|
||||
capture?: boolean,
|
||||
discrete?: boolean,
|
||||
stopPropagation?: boolean,
|
||||
};
|
||||
|
||||
export type ReactResponderContext = {
|
||||
@@ -148,6 +147,7 @@ export type ReactResponderContext = {
|
||||
eventObject: Object,
|
||||
otpions: ReactResponderDispatchEventOptions,
|
||||
) => void,
|
||||
dispatchStopPropagation: (passive?: boolean) => void,
|
||||
isTargetWithinElement: (
|
||||
childTarget: Element | Document,
|
||||
parentTarget: Element | Document,
|
||||
@@ -168,7 +168,8 @@ export type ReactResponderContext = {
|
||||
hasOwnership: () => boolean,
|
||||
requestOwnership: () => boolean,
|
||||
releaseOwnership: () => boolean,
|
||||
setTimeout: (func: () => void, timeout: number) => TimeoutID,
|
||||
setTimeout: (func: () => void, timeout: number) => Symbol,
|
||||
clearTimeout: (timerId: Symbol) => void,
|
||||
getEventTargetsFromTarget: (
|
||||
target: Element | Document,
|
||||
queryType?: Symbol | number,
|
||||
|
||||
Reference in New Issue
Block a user