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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ with v14.
- `toBeDisabled()` and `*ByRole` queries prefer `disabled` over `aria-disabled` and
`accessibilityState.disabled` on `Text`, as React Native does. A `Text` with
`disabled={false}` and `aria-disabled` is enabled.
- `fireEvent` delivers raw touch events (`touchStart`, `touchMove`, `touchEnd`, `touchCancel`)
to a handler even when the nearest touch responder declines the touch, as a device does. Before,
a child with `onStartShouldSetResponder` returning `false` stopped these events from reaching
the `onTouchStart` of its ancestors. Responder-driven events like `press` still respect the
responder.

## 14.1.0

Expand Down
33 changes: 33 additions & 0 deletions src/events/__tests__/fire-event.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1812,4 +1812,37 @@ describe('responder system', () => {
});
expect(onDrag).toHaveBeenCalled();
});

test.each([
['touchStart', 'onTouchStart'],
['touchMove', 'onTouchMove'],
['touchEnd', 'onTouchEnd'],
['touchCancel', 'onTouchCancel'],
])('fires %s on parent when child declines the responder', async (eventName, handlerName) => {
const onTouch = jest.fn();
await render(
<View {...{ [handlerName]: onTouch }}>
<View
testID="child"
onStartShouldSetResponder={() => false}
onMoveShouldSetResponder={() => false}
/>
</View>,
);

await fireEvent(screen.getByTestId('child'), eventName);

expect(onTouch).toHaveBeenCalledTimes(1);
});

test('fires touchStart on element that declines the responder', async () => {
const onTouchStart = jest.fn();
await render(
<View testID="view" onTouchStart={onTouchStart} onStartShouldSetResponder={() => false} />,
);

await fireEvent(screen.getByTestId('view'), 'touchStart');

expect(onTouchStart).toHaveBeenCalledTimes(1);
});
});
10 changes: 10 additions & 0 deletions src/events/is-enabled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ export function isEventBlockableByPointerEvents(eventType: string): boolean {
*/
const TEXT_INPUT_EVENTS_IGNORING_EDITABLE = ['contentSizeChange', 'layout', 'scroll'];

/**
* List of raw touch events. A device delivers them to every ancestor of the touched view, whether
* or not a touch responder accepts the touch.
*/
const TOUCH_EVENTS_IGNORING_RESPONDER = ['touchStart', 'touchMove', 'touchEnd', 'touchCancel'];

/**
* Checks whether a device would deliver the event to the instance, taking into account
* `pointerEvents`, non-editable `TextInput` and touch responders that decline the touch.
Expand All @@ -65,6 +71,10 @@ export function isEventEnabled(
return false;
}

if (TOUCH_EVENTS_IGNORING_RESPONDER.includes(eventType)) {
return true;
}

const touchStart = nearestTouchResponder?.props.onStartShouldSetResponder?.();
const touchMove = nearestTouchResponder?.props.onMoveShouldSetResponder?.();
if (touchStart || touchMove) {
Expand Down