Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
import * as Fantom from '@react-native/fantom';
import * as React from 'react';
import {View} from 'react-native';
import {ReactNativeFeatureFlags} from 'react-native/react-private-interface';

// Helpers for common touch event payloads
function touchStart(identifier: number = 0): {
Expand Down Expand Up @@ -132,6 +133,68 @@ describe('Responder System', () => {
expect(onResponderRelease).toHaveBeenCalledTimes(1);
});

// TODO(react/react#38006): run this with the flag on too once the synced
// renderer stops dropping events whose target is null.
(ReactNativeFeatureFlags.enableNativeEventTargetEventDispatching()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the behavior should be the same as before, this test should run for both versions of the feature flag and behave the same way

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left this as is for now because the flag-on run can't pass in this PR alone. With the flag off, a touch end with a null target already goes through the legacy responder plugin and releases the responder. With the flag on, ReactFabric's dispatchEvent drops events with a null target, so they never reach dispatchNativeEvent (react/react#38006). After that guard is removed, the test can run for both flag values with the same expectations. Should I keep it here so it lands with the renderer sync, or add it in a follow-up?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I flipped the gate in 6208d24, so the test now runs with the flag off and is skipped with the flag on. With the flag on, the synced renderer still drops touch events whose target is null before they reach dispatchNativeEvent, so that run can't pass until the renderer sync lands (react/react#38006). I left a TODO on the skip to turn it on after the sync.

? it.skip
: it)(
'releases the responder when the touched view unmounts before the touch ends',
() => {
const root = Fantom.createRoot();
const firstRef = React.createRef<React.ElementRef<typeof View>>();
const secondRef = React.createRef<React.ElementRef<typeof View>>();
const onFirstResponderGrant = jest.fn();
const onSecondResponderGrant = jest.fn();

Fantom.runTask(() => {
root.render(
<View
key="first"
ref={firstRef}
onStartShouldSetResponder={() => true}
onResponderGrant={onFirstResponderGrant}
/>,
);
});

Fantom.dispatchNativeEvent(firstRef, 'onTouchStart', touchStart(), {
category: Fantom.NativeEventCategory.Discrete,
});

expect(onFirstResponderGrant).toHaveBeenCalledTimes(1);

// Queue the touch end without flushing it, then unmount the view, so the
// event reaches JS without a target.
Fantom.enqueueNativeEvent(firstRef, 'onTouchEnd', touchEnd(), {
category: Fantom.NativeEventCategory.Discrete,
});

Fantom.runTask(() => {
root.render(
<View
key="second"
ref={secondRef}
onStartShouldSetResponder={() => true}
onResponderGrant={onSecondResponderGrant}
/>,
);
});

Fantom.flushAllNativeEvents();

// The responder was released, so the next touch is granted
Fantom.dispatchNativeEvent(secondRef, 'onTouchStart', touchStart(), {
category: Fantom.NativeEventCategory.Discrete,
});

expect(onSecondResponderGrant).toHaveBeenCalledTimes(1);

Fantom.dispatchNativeEvent(secondRef, 'onTouchEnd', touchEnd(), {
category: Fantom.NativeEventCategory.Discrete,
});
},
);

it('dispatches responderTerminate on touch cancel', () => {
const root = Fantom.createRoot();
const ref = React.createRef<React.ElementRef<typeof View>>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -591,7 +591,7 @@ function isResponderRelevantTopLevelType(topLevelType: string): boolean {
*/
export function processResponderEvent(
topLevelType: string,
eventTarget: EventTarget | null,
eventTarget: ?EventTarget,
nativeEvent: {[string]: unknown},
): void {
// Fast path: if this event isn't one the responder system cares about and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,23 @@ import {
* enableNativeEventTargetEventDispatching is enabled.
*/
export default function dispatchNativeEvent(
target: EventTarget,
target: ?EventTarget,
type: string,
payload: {[string]: unknown},
): void {
// $FlowFixMe[incompatible-type] The global is backed by this implementation.
const internalTarget = target as InternalEventTarget;
const internalTarget = target as ?InternalEventTarget;

// Process responder events before normal event dispatch.
processResponderEvent(type, internalTarget, payload);

try {
// The target is missing when its view unmounted during the touch, so only
// the responder step above applies.
if (internalTarget == null) {
return;
}

// Normal EventTarget dispatch
const bubbleConfig = customBubblingEventTypes[type];
const directConfig = customDirectEventTypes[type];
Expand Down
Loading