Skip to content

Commit 9ebb325

Browse files
sammy-SCmeta-codesync[bot]
authored andcommitted
Add getDefinedEventHandlers API (facebook#55861)
Summary: Pull Request resolved: facebook#55861 Add `Fantom.getDefinedEventHandlers(element)` which returns the names of event handlers registered on a component. It Reads committed memoizedProps from the React fiber, so it works for any component type — including component-specific events like ScrollView's onScroll. The alternative we considered was reading the ViewEvents bitmap from `BaseViewProps` in the ShadowNode (C++). That approach only covers View-specific touch/pointer events and onLayout; it misses component-specific events dispatched imperatively (e.g. ScrollView's onScroll), requiring a separate codepath per component type. Changelog: [Internal] Reviewed By: javache Differential Revision: D94936370 fbshipit-source-id: f48b6e37a9e7cf252f396db69e1a43ecca82db96
1 parent 7f1a1e6 commit 9ebb325

3 files changed

Lines changed: 114 additions & 4 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
12+
13+
import * as Fantom from '@react-native/fantom';
14+
import * as React from 'react';
15+
import {createRef} from 'react';
16+
import {ScrollView, View} from 'react-native';
17+
18+
describe('Fantom.getDefinedEventHandlers', () => {
19+
it('returns event handler names registered on a view', () => {
20+
const root = Fantom.createRoot();
21+
const ref = createRef<React.ElementRef<typeof View>>();
22+
23+
Fantom.runTask(() => {
24+
root.render(
25+
<View ref={ref} onLayout={() => {}} onTouchStart={() => {}} />,
26+
);
27+
});
28+
29+
const handlers = Fantom.getDefinedEventHandlers(ref);
30+
expect(handlers).toContain('onLayout');
31+
expect(handlers).toContain('onTouchStart');
32+
});
33+
34+
it('returns an empty array when no event handlers are registered', () => {
35+
const root = Fantom.createRoot();
36+
const ref = createRef<React.ElementRef<typeof View>>();
37+
38+
Fantom.runTask(() => {
39+
root.render(<View ref={ref} />);
40+
});
41+
42+
expect(Fantom.getDefinedEventHandlers(ref)).toEqual([]);
43+
});
44+
45+
it('returns multiple event handler names when multiple are registered', () => {
46+
const root = Fantom.createRoot();
47+
const ref = createRef<React.ElementRef<typeof View>>();
48+
49+
Fantom.runTask(() => {
50+
root.render(
51+
<View
52+
ref={ref}
53+
onLayout={() => {}}
54+
onTouchStart={() => {}}
55+
onTouchEnd={() => {}}
56+
onTouchCancel={() => {}}
57+
onClick={() => {}}
58+
/>,
59+
);
60+
});
61+
62+
const handlers = Fantom.getDefinedEventHandlers(ref);
63+
expect(handlers).toContain('onLayout');
64+
expect(handlers).toContain('onTouchStart');
65+
expect(handlers).toContain('onTouchEnd');
66+
expect(handlers).toContain('onTouchCancel');
67+
expect(handlers).toContain('onClick');
68+
});
69+
70+
it('detects onScroll on a ScrollView', () => {
71+
const root = Fantom.createRoot();
72+
const ref = createRef<React.ElementRef<typeof ScrollView>>();
73+
74+
Fantom.runTask(() => {
75+
root.render(<ScrollView ref={ref} onScroll={() => {}} />);
76+
});
77+
78+
expect(Fantom.getDefinedEventHandlers(ref)).toContain('onScroll');
79+
});
80+
});

private/react-native-fantom/src/index.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ import {LogBox} from 'react-native';
2323
import NativeFantom, {
2424
NativeEventCategory,
2525
} from 'react-native/src/private/testing/fantom/specs/NativeFantom';
26-
import {getNativeNodeReference} from 'react-native/src/private/webapis/dom/nodes/internals/NodeInternals';
26+
import {
27+
getInstanceHandle,
28+
getNativeNodeReference,
29+
} from 'react-native/src/private/webapis/dom/nodes/internals/NodeInternals';
2730
import ReadOnlyNode from 'react-native/src/private/webapis/dom/nodes/ReadOnlyNode';
2831

2932
const nativeRuntimeScheduler = global.nativeRuntimeScheduler;
@@ -237,6 +240,32 @@ export function unstable_getFabricUpdateProps(nodeOrRef: NodeOrRef): Readonly<{
237240
return NativeFantom.getFabricUpdateProps(shadowNode);
238241
}
239242

243+
/**
244+
* Returns the names of event handlers registered on a component's shadow node.
245+
*
246+
* @param nodeOrRef - The node or ref for which to retrieve event handlers.
247+
* @returns Array of event handler prop names (e.g. `['onLayout', 'onTouchStart']`)
248+
*/
249+
export function getDefinedEventHandlers(
250+
nodeOrRef: NodeOrRef,
251+
): ReadonlyArray<string> {
252+
const node = getNode(nodeOrRef);
253+
const instanceHandle = getInstanceHandle(node);
254+
if (typeof instanceHandle !== 'object' || instanceHandle == null) {
255+
return [];
256+
}
257+
// WARNING: This uses React private API (fiber internals).
258+
// $FlowExpectedError[incompatible-type]
259+
const memoizedProps = (instanceHandle as {memoizedProps?: {[string]: mixed}})
260+
.memoizedProps;
261+
if (memoizedProps == null) {
262+
return [];
263+
}
264+
return Object.keys(memoizedProps).filter(
265+
key => key.startsWith('on') && typeof memoizedProps[key] === 'function',
266+
);
267+
}
268+
240269
/**
241270
* Simulates running a task on the UI thread and forces side effect to drain
242271
* the event queue, scheduling events to be dispatched to JavaScript.

private/react-native-fantom/tester/src/NativeFantom.cpp

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ std::string NativeFantom::getRenderedOutput(
8686
SurfaceId surfaceId,
8787
NativeFantomGetRenderedOutputRenderFormatOptions options) {
8888
RenderFormatOptions formatOptions{
89-
options.includeRoot, options.includeLayoutMetrics};
89+
.includeRoot = options.includeRoot,
90+
.includeLayoutMetrics = options.includeLayoutMetrics};
9091

9192
auto viewTree = appDelegate_.mountingManager_->getViewTree(surfaceId);
9293
return appDelegate_.mountingManager_->renderer()->render(
@@ -124,10 +125,10 @@ void NativeFantom::enqueueNativeEvent(
124125
std::optional<bool> isUnique) {
125126
if (isUnique.value_or(false)) {
126127
shadowNode->getEventEmitter()->dispatchUniqueEvent(
127-
std::move(type), payload.value_or(folly::dynamic::object()));
128+
type, payload.value_or(folly::dynamic::object()));
128129
} else {
129130
shadowNode->getEventEmitter()->dispatchEvent(
130-
std::move(type),
131+
type,
131132
payload.value_or(folly::dynamic::object()),
132133
category.value_or(RawEvent::Category::Unspecified));
133134
}

0 commit comments

Comments
 (0)