Skip to content

Commit 7e25db3

Browse files
fix: deprecate bubbling of direct events in fireEvent (#1941)
1 parent b80a762 commit 7e25db3

21 files changed

Lines changed: 724 additions & 161 deletions

‎CHANGELOG.md‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,23 @@ with v14.
88
### Features
99

1010
- Added `fireEvent.layout()` to simulate the layout engine measuring an element, invoking the
11-
`onLayout` handler with a synthetic layout event. Layout events do not bubble to parent
12-
elements.
11+
`onLayout` handler with a synthetic layout event. Unlike `fireEvent(element, 'layout')`, it
12+
does not bubble to parent elements.
1313
- `fireEvent.scroll()` and `userEvent.scrollTo()` use the size from the last layout event on the
1414
same `ScrollView` as the default `layoutMeasurement`.
1515
- Added `userEvent.accessibilityAction()` to dispatch a named accessibility action to an
1616
element, invoking its `onAccessibilityAction` handler.
1717
- Added `userEvent.pullToRefresh()` to simulate the pull-to-refresh gesture on a host
1818
`ScrollView` element, invoking the `onRefresh` handler of its `refreshControl` prop.
1919

20+
### Deprecations
21+
22+
- `fireEvent` warns when a direct event bubbles from a nested element to the host element that
23+
emits it, e.g. `scroll` from `ScrollView` content to the `ScrollView`. These events will stop
24+
bubbling in the next major version. See the
25+
[`fireEvent` docs](./website/docs/14.x/docs/api/events/fire-event.mdx) for the list of direct
26+
events.
27+
2028
## 14.0.0
2129

2230
### Migration guide

‎contributing/event-dispatch.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Both are built on the shared event subsystem in `src/events/`, which also holds
2222
`src/events/fire-event.ts` is the public API. It calls a single handler for a single event, found with `findEventHandler()` from `src/events/propagation.ts`. The work is in finding the right handler:
2323

2424
- It starts at the target and moves up the tree until it finds a handler. It also checks props of composite components, not only host elements.
25-
- Direct events (see [Native event propagation](native-events.md)) only check the target.
25+
- Direct events (see [Native event propagation](native-events.md)) still bubble, with a warning when they reach an ancestor that emits them. `fireEvent.layout()` only checks the target.
2626
- It mimics cases where a device would not deliver the event, like `pointerEvents`, a non-editable `TextInput`, or a touch responder that declines.
2727

2828
## `userEvent`

‎contributing/native-events.md‎

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
In React Native, some events **bubble** up to parent elements and others are **direct**, meaning only the element that emitted them receives them. `fireEvent` should behave the same way.
44

5-
Today, `fireEvent` treats every event as bubbling except `layout`. The list of direct events lives in `isDirectEvent()` in `src/events/propagation.ts`.
5+
Today, `fireEvent` still bubbles direct events, with a warning (see [Known gaps](#known-gaps)). Only `fireEvent.layout()` does not bubble. The rules live in `isDirectEvent()` in `src/events/propagation.ts`.
66

77
## Which events are which
88

@@ -12,21 +12,29 @@ There is no simple rule for which events bubble. Coming from user input doesn't
1212

1313
**Direct:**
1414

15-
| Component | Direct events |
16-
| ---------------- | ---------------------------------------------------------------------------------------- |
17-
| All components | `layout`, accessibility actions |
18-
| `ScrollView` | `scroll`, `scrollBeginDrag`, `scrollEndDrag`, `momentumScrollBegin`, `momentumScrollEnd` |
19-
| `TextInput` | `scroll`, `selectionChange`, `contentSizeChange` |
20-
| `Text` | `textLayout` |
21-
| `Image` | `loadStart`, `progress`, `load`, `error`, `loadEnd` |
22-
| `Modal` | `requestClose`, `show`, `dismiss`, `orientationChange` |
23-
| `RefreshControl` | `refresh` |
15+
| Component | Direct events |
16+
| ---------------- | ------------------------------------------------------------------------------------------------------- |
17+
| All components | `layout`, `accessibilityAction`, `accessibilityTap`, `magicTap`, `accessibilityEscape` |
18+
| `ScrollView` | `scroll`, `scrollBeginDrag`, `scrollEndDrag`, `momentumScrollBegin`, `momentumScrollEnd`, `scrollToTop` |
19+
| `TextInput` | `scroll`, `selectionChange`, `contentSizeChange` |
20+
| `Text` | `textLayout` |
21+
| `Image` | `loadStart`, `progress`, `partialLoad`, `load`, `error`, `loadEnd` |
22+
| `Modal` | `requestClose`, `show`, `dismiss`, `orientationChange` |
23+
| `RefreshControl` | `refresh` |
2424

2525
This is simplified. A few events differ between iOS and Android. Check the sources below for the exact details.
2626

2727
## Known gaps
2828

29-
All the direct events above except `layout` still bubble in `fireEvent`. Fixing that is a breaking change: tests that fire these events on a child element would stop reaching the parent's handler.
29+
`fireEvent` still bubbles the direct events above for backward compatibility, as tests fire them on nested elements, e.g. `scroll` on `ScrollView` content. Making them direct is a breaking change, planned for the next major release, which should also remove the warning.
30+
31+
Until then, `fireEvent` logs a warning when a direct event bubbles from a nested element to the handler of an ancestor that emits it, based on the host element type, e.g. `scroll` from `ScrollView` content to the `ScrollView`'s `onScroll`. Handlers with the same name elsewhere, like an `onLoad` prop of a custom composite component, receive bubbled events without a warning. Only the type of the element with the handler is checked, so a handler further up on an element that doesn't emit the event gets no warning, although it will stop receiving the event too.
32+
33+
`refresh` is emitted by `RefreshControl`. The Jest `ScrollView` mock renders the `refreshControl` element next to the content view, not around it, so it is never an ancestor of list items. `FlatList` and `SectionList` also pass `onRefresh` to the host `ScrollView`, so the rule uses `ScrollView` as the emitting element.
34+
35+
`contentSizeChange` is not a native `ScrollView` event, so the table above doesn't list it. The `ScrollView` component calls `onContentSizeChange` from the `onLayout` of its content view and passes `onContentSizeChange: null` to the host element. The Jest `ScrollView` mock passes the prop to the host element instead, so the rule uses `ScrollView` as the emitting element. `FlatList` and `SectionList` always set this handler, and tests fire the event on list items, so making it direct will break more tests than other events.
36+
37+
Both rules depend on the Jest mock. The `FlatList` cases in `src/events/__tests__/fire-event.test.tsx` cover both, so a mock change that moves these handlers fails them.
3038

3139
## Sources
3240

‎docs/api/fire-event.md‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,25 @@
88
> Use Fire Event for cases not supported by User Event and for triggering event handlers on composite components.
99
1010
```ts
11-
function fireEvent(instance: TestInstance, eventName: string, ...data: unknown[]): Promise<unknown>;
11+
function fireEvent(instance: TestInstance, eventType: string, ...data: unknown[]): Promise<unknown>;
1212
```
1313

1414
The `fireEvent` API triggers event handlers on both host and composite components. It traverses the component tree bottom-up from the passed element to find an enabled event handler named `onXxx` where `xxx` is the event name.
1515

16+
Some events are direct in React Native: they are delivered only to the host element that emitted them. `fireEvent` still bubbles them for backward compatibility, but logs a warning when they bubble from a nested element to the handler of an ancestor that emits them, e.g. `scroll` from `ScrollView` content to the `ScrollView`. They will stop bubbling in the next major version, so fire them on the element that has the handler. These events are:
17+
18+
- `layout`, `accessibilityAction`, `accessibilityTap`, `magicTap` and `accessibilityEscape` on all elements
19+
- `textLayout` on `Text`
20+
- `scroll`, `selectionChange` and `contentSizeChange` on `TextInput`
21+
- `loadStart`, `progress`, `partialLoad`, `load`, `error` and `loadEnd` on `Image`
22+
- `scroll`, `scrollBeginDrag`, `scrollEndDrag`, `momentumScrollBegin`, `momentumScrollEnd`, `scrollToTop`, `refresh` and `contentSizeChange` on `ScrollView`
23+
- `requestClose`, `show`, `dismiss` and `orientationChange` on `Modal`
24+
25+
Events with these names bubble without a warning to other handlers, such as an `onLoad` prop of your own composite component.
26+
1627
Unlike User Event, this API does not automatically pass event object to event handler, this is responsibility of the user to construct such object.
1728

18-
The base `fireEvent(instance, eventName, ...data)` API can pass multiple custom arguments to the handler. Convenience helpers such as `fireEvent.press` and `fireEvent.scroll` are different: they create a default event object and accept one optional object to merge into it.
29+
The base `fireEvent(instance, eventType, ...data)` API can pass multiple custom arguments to the handler. Convenience helpers such as `fireEvent.press` and `fireEvent.scroll` are different: they create a default event object and accept one optional object to merge into it.
1930

2031
This function uses async `act` internally to execute all pending React updates during event handling.
2132

@@ -180,7 +191,7 @@ fireEvent.layout: (
180191

181192
Builds a layout event carrying the given `layout` rectangle and invokes the `onLayout` handler of the given element. Use it to simulate the layout engine measuring an element, e.g. to test components that adapt to a measured size.
182193

183-
Unlike other `fireEvent` calls, layout events do not bubble: React Native delivers them only to the measured element, so the handler is not looked up on parent elements.
194+
Layout events fired with this helper do not bubble: React Native delivers them only to the measured element, so only that element's own `onLayout` prop is called, not handlers on its parent elements or composite components. This is the intended behavior. `fireEvent(element, 'layout')` still bubbles for backward compatibility, with a deprecation warning, and will stop bubbling like `fireEvent.layout()` in the next major version.
184195

185196
The `layout` values are merged onto a zeroed rectangle (`{ x: 0, y: 0, width: 0, height: 0 }`), so pass only the fields your component reads.
186197

‎docs/api/user-event.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## Comparison with Fire Event API
44

5-
Fire Event is our original event simulation API. It can invoke **any event handler** declared on **either host or composite elements**. Suppose the element does not have `onEventName` event handler for the passed `eventName` event, or the element is disabled. In that case, Fire Event will traverse up the component tree, looking for an event handler on both host and composite elements along the way. By default, it will **not pass any event data**, but the user might provide it in the last argument.
5+
Fire Event is our original event simulation API. It can invoke **any event handler** declared on **either host or composite elements**. Suppose the element does not have `onEventName` event handler for the passed `eventType` event, or the element is disabled. In that case, Fire Event will traverse up the component tree, looking for an event handler on both host and composite elements along the way. By default, it will **not pass any event data**, but the user might provide it in the last argument.
66

77
In contrast, User Event provides realistic event simulation for user interactions like `press` or `type`. Each interaction will trigger a **sequence of events** corresponding to React Native runtime behavior. These events will be invoked **only on host elements**, and **will automatically receive event data** corresponding to each event.
88

‎docs/guides/llm-guidelines.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ Use only when `userEvent` doesn't support the event or when you need direct cont
113113
114114
| Method | Description |
115115
| ---------------------------------------- | --------------------------------------------- |
116-
| `fireEvent(element, eventName, ...data)` | Fire any event by name |
116+
| `fireEvent(element, eventType, ...data)` | Fire any event by name |
117117
| `fireEvent.press(element)` | Fire `onPress` only (no `pressIn`/`pressOut`) |
118118
| `fireEvent.changeText(element, text)` | Fire `onChangeText` directly |
119119
| `fireEvent.scroll(element, eventData)` | Fire `onScroll` with event data |

0 commit comments

Comments
 (0)