[iOS] Add hover callbacks to Touchable - #4397
Conversation
📝 WalkthroughSummary by CodeRabbit
WalkthroughChangesThe Apple button API now defines hover-in and hover-out events and uses a generalized event delegate. Native iOS, macOS, and touch paths track hover samples and dispatch balanced events. Fabric forwards the events through hover callbacks. Apple button hover events
Sequence Diagram(s)sequenceDiagram
participant Pointer as iOS/macOS/touch input
participant Button as RNGestureHandlerButton
participant View as RNGestureHandlerButtonComponentView
participant Fabric as Fabric event emitter
Pointer->>Button: record hover position and pointer type
Button->>Button: derive effective hover transition
Button->>View: dispatch button hover event
View->>Fabric: emit onButtonHoverIn or onButtonHoverOut
Possibly related PRs
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
4a471bb to
2dcad4a
Compare
There was a problem hiding this comment.
Pull request overview
Adds Apple-platform support (iOS/tvOS/macOS) for Touchable hover callbacks (onHoverIn/onHoverOut) by extending the native button’s interaction state machine to emit hover events with pointer metadata, aligning behavior with Android where possible.
Changes:
- Renames the button delegate from press-only to a generalized interaction event delegate (
RNGHButtonPressEventDelegate→RNGHButtonEventDelegate). - Emits new hover-in/hover-out events from the Fabric component view to the codegen event emitter.
- Implements Apple-side hover tracking/sampling (UIHoverGestureRecognizer, NSTrackingArea, and press-touch-derived hover maintenance) and dispatches hover events gated to the v3 managed button.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| packages/react-native-gesture-handler/apple/RNGestureHandlerButtonComponentView.mm | Routes new native hover event types through the Fabric event emitter and updates delegate wiring. |
| packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm | Adds hover state bookkeeping, pointer sampling, and hover event dispatch logic across iOS/macOS/tvOS paths. |
| packages/react-native-gesture-handler/apple/RNGestureHandlerButton.h | Extends event types with HoverIn/HoverOut and renames the delegate/properties to cover non-press interactions. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
2dcad4a to
73aa555
Compare
73aa555 to
c277136
Compare
c277136 to
8c91c68
Compare
m-bert
left a comment
There was a problem hiding this comment.
Doesn't it have the same bug with cancel action as Android? Other than that it's fine
## Description
The Apple side of `onHoverIn`/`onHoverOut`, covering iOS, tvOS and
macOS. Mirrors the Android state machine, with the pointer sampled from
`UIHoverGestureRecognizer` on iOS, the `NSTrackingArea` on macOS, and
the touch stream during a press.
- `RNGHButtonPressEventDelegate` becomes `RNGHButtonEventDelegate` now
that it carries more than presses.
- The touch-derived path is gated on a press-start latch. Without it a
plain pencil tap on an iPad that doesn't report pencil hover would open
a hover on touch-up that nothing could ever close.
- The pointer type is latched for the duration of a hover — a pencil's
`zOffset` collapses to zero as it approaches contact, which would
otherwise make the hover-out claim a different type than the hover-in.
- tvOS drives hover from focus rather than a pointer, so there's no
sample to report; the payload falls back to the button's centre.
## Test plan
Not covered by the JS tests. Needs an iPad with a trackpad or a
hover-capable pencil, a Mac, and a tvOS device for the focus path.
<details>
<summary>Example code</summary>
```tsx
import React, { useState } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import {
GestureHandlerRootView,
Touchable,
} from 'react-native-gesture-handler';
export default function Example() {
const [log, setLog] = useState<string[]>([]);
const callbacks = (source: string) => ({
onHoverIn: () => setLog((l) => [`${source} onHoverIn`, ...l]),
onHoverOut: () => setLog((l) => [`${source} onHoverOut`, ...l]),
onPressIn: () => setLog((l) => [`${source} onPressIn`, ...l]),
onPressOut: () => setLog((l) => [`${source} onPressOut`, ...l]),
});
return (
<GestureHandlerRootView style={styles.container}>
<View style={styles.row}>
<Touchable style={styles.box} {...callbacks('Touchable')}>
<Text style={styles.text}>Touchable</Text>
</Touchable>
<Pressable style={styles.box} {...callbacks('Pressable')}>
<Text style={styles.text}>Pressable</Text>
</Pressable>
</View>
{log.slice(0, 12).map((entry, i) => (
<Text key={i}>{entry}</Text>
))}
</GestureHandlerRootView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 24 },
row: { flexDirection: 'row', gap: 24, marginBottom: 24 },
box: {
width: 120,
height: 120,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#6941C6',
},
text: { color: 'white' },
});
```
</details>
8c91c68 to
9af3c7b
Compare
…867) Bumps \[react-native-gesture-handler\](https://github.com/software-mansion/react-native-gesture-handler) from 2.32.0 to 3.2.1. Release notes _Sourced from [react-native-gesture-handler's releases](https://github.com/software-mansion/react-native-gesture-handler/releases)._ > v3.2.1 > ------ > > 🐛 Bug fixes > ------------ > > * Forward press handlers as `testOnly_*` in `PressableWithTouchable` by [`@huextrat`](https://github.com/huextrat) in [software-mansion/react-native-gesture-handler#4416](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4416) > > 🔢 Miscellaneous > ---------------- > > * Update `Pressable` props by [`@m-bert`](https://github.com/m-bert) in [software-mansion/react-native-gesture-handler#4421](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4421) > > **Full Changelog**: [https://github.com/software-mansion/react-native-gesture-handler/compare/v3.2.0...v3.2.1](https://github.com/software-mansion/react-native-gesture-handler/compare/v3.2.0...v3.2.1) > > v3.2.0 > ------ > > ❗ Important changes > ------------------- > > * feat: Adopt AGP v9 by [`@hurali97`](https://github.com/hurali97) in [software-mansion/react-native-gesture-handler#4263](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4263) > * Implement `Pressable` based on `Touchable` by [`@m-bert`](https://github.com/m-bert) in [software-mansion/react-native-gesture-handler#4411](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4411) > * \[Android\] Add hover callbacks to Touchable by [`@j-piasecki`](https://github.com/j-piasecki) in [software-mansion/react-native-gesture-handler#4396](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4396) > * \[iOS\] Add hover callbacks to Touchable by [`@j-piasecki`](https://github.com/j-piasecki) in [software-mansion/react-native-gesture-handler#4397](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4397) > * \[Web\] Add hover callbacks to Touchable by [`@j-piasecki`](https://github.com/j-piasecki) in [software-mansion/react-native-gesture-handler#4398](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4398) > * \[Web\] Refactor `Touchable` not to rely on `GestureDetector` by [`@j-piasecki`](https://github.com/j-piasecki) in [software-mansion/react-native-gesture-handler#4344](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4344) > * \[iOS\] Refactor `Touchable` not to rely on `GestureDetector` by [`@j-piasecki`](https://github.com/j-piasecki) in [software-mansion/react-native-gesture-handler#4343](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4343) > * \[Android\] Refactor `Touchable` not to rely on `GestureDetector` by [`@j-piasecki`](https://github.com/j-piasecki) in [software-mansion/react-native-gesture-handler#4342](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4342) > * Fix fatal crash `Cannot read property 'translationX' of undefined` when a touch event is serialized without `allTouches` by [`@huextrat`](https://github.com/huextrat) in [software-mansion/react-native-gesture-handler#4316](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4316) > > 👍 Improvements > --------------- > > * \[Android\] Skip the underlay drawable when it can never be visible by [`@j-piasecki`](https://github.com/j-piasecki) in [software-mansion/react-native-gesture-handler#4359](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4359) > * \[Android\] Apply the button's managed handler config once per prop transaction by [`@j-piasecki`](https://github.com/j-piasecki) in [software-mansion/react-native-gesture-handler#4357](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4357) > * \[Android\] Configure the button's handler directly instead of through a `ReadableMap` by [`@j-piasecki`](https://github.com/j-piasecki) in [software-mansion/react-native-gesture-handler#4358](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4358) > > 🐛 Bug fixes > ------------ > > * \[Android\] Guard update events to only be dispatched in ACTIVE state by [`@j-piasecki`](https://github.com/j-piasecki) in [software-mansion/react-native-gesture-handler#4332](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4332) > * Pass empty callbacks to UI when `runOnJS` is `true` by [`@m-bert`](https://github.com/m-bert) in [software-mansion/react-native-gesture-handler#4326](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4326) > * \[Web\] Fix incorrectly calculated `timeDelta` by [`@m-bert`](https://github.com/m-bert) in [software-mansion/react-native-gesture-handler#4329](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4329) > * \[Web\] Fix incorrect `Tap` offset by [`@m-bert`](https://github.com/m-bert) in [software-mansion/react-native-gesture-handler#4330](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4330) > * Fix `minVelocity` props behavior by [`@m-bert`](https://github.com/m-bert) in [software-mansion/react-native-gesture-handler#4327](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4327) > * \[Android\] Fix `minDistance` being reset by partial config updates by [`@m-bert`](https://github.com/m-bert) in [software-mansion/react-native-gesture-handler#4347](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4347) > * Move Interceptor on `ScrollView`, not its content by [`@m-bert`](https://github.com/m-bert) in [software-mansion/react-native-gesture-handler#4331](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4331) > * \[iOS\] Re-sync layer opacity and transform from retained props when recycling buttons by [`@m-bert`](https://github.com/m-bert) in [software-mansion/react-native-gesture-handler#4360](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4360) > * \[Android\] Properly handle `requestDisallowInterceptTouchEvent` for v3 by [`@j-piasecki`](https://github.com/j-piasecki) in [software-mansion/react-native-gesture-handler#4367](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4367) > * Fix `Touchable` not respecting `keyboardShouldPersistTaps="handled"` by [`@m-bert`](https://github.com/m-bert) in [software-mansion/react-native-gesture-handler#4372](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4372) > * \[macOS\] Fix touch events never being delivered by [`@m-bert`](https://github.com/m-bert) in [software-mansion/react-native-gesture-handler#4390](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4390) > * fix: crash when mount listener fires after GestureDetector unmount by [`@kosmydel`](https://github.com/kosmydel) in [software-mansion/react-native-gesture-handler#4268](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4268) > * \[macOS\] Fix `Pan` activation criteria being ignored by [`@m-bert`](https://github.com/m-bert) in [software-mansion/react-native-gesture-handler#4387](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4387) > * \[macOS\] Fix `manualActivation` never blocking gesture activation by [`@m-bert`](https://github.com/m-bert) in [software-mansion/react-native-gesture-handler#4389](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4389) > * \[iOS\] Fix touch events never being delivered to `VirtualDetector` handlers by [`@m-bert`](https://github.com/m-bert) in [software-mansion/react-native-gesture-handler#4392](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4392) > * \[iOS\] Fix gestures attached via `VirtualGestureDetector` never recognizing continuous gestures by [`@m-bert`](https://github.com/m-bert) in [software-mansion/react-native-gesture-handler#4393](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4393) > * \[macOS\] Fix `Fling` not sending touch events and begin/end states consistently by [`@m-bert`](https://github.com/m-bert) in [software-mansion/react-native-gesture-handler#4395](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4395) > * \[Android\] Fix handlers cancelled while awaiting leaking in the orchestrator by [`@m-bert`](https://github.com/m-bert) in [software-mansion/react-native-gesture-handler#4402](https://redirect.github.com/software-mansion/react-native-gesture-handler/pull/4402) ... (truncated) Commits * [`62f0f7d`](software-mansion/react-native-gesture-handler@62f0f7d) Release v3.2.1 * [`4716425`](software-mansion/react-native-gesture-handler@4716425) Merge branch '3.2-stable' of github.com:software-mansion/react-native-gesture... * [`f0ae48c`](software-mansion/react-native-gesture-handler@f0ae48c) Update `Pressable` props ([#4421](https://redirect.github.com/software-mansion/react-native-gesture-handler/issues/4421)) * [`5f0f0d8`](software-mansion/react-native-gesture-handler@5f0f0d8) Forward press handlers as `testOnly_*` in `PressableWithTouchable` ([#4416](https://redirect.github.com/software-mansion/react-native-gesture-handler/issues/4416)) * [`0a91db7`](software-mansion/react-native-gesture-handler@0a91db7) Release v3.2.0 * [`44046a6`](software-mansion/react-native-gesture-handler@44046a6) \[Android\] Resolve the button event dispatcher by react tag ([#4415](https://redirect.github.com/software-mansion/react-native-gesture-handler/issues/4415)) * [`2469c1d`](software-mansion/react-native-gesture-handler@2469c1d) Derive `Pressable` pressed state from `testOnly_pressed` ([#4414](https://redirect.github.com/software-mansion/react-native-gesture-handler/issues/4414)) * [`3f1bf74`](software-mansion/react-native-gesture-handler@3f1bf74) Clear pending timers on unmount in StatefulPressable ([#4413](https://redirect.github.com/software-mansion/react-native-gesture-handler/issues/4413)) * [`50ae6a1`](software-mansion/react-native-gesture-handler@50ae6a1) \[General\] Default GestureDetector moduleId to -1 ([#4412](https://redirect.github.com/software-mansion/react-native-gesture-handler/issues/4412)) * [`8b661c9`](software-mansion/react-native-gesture-handler@8b661c9) Implement `Pressable` based on `Touchable` ([#4411](https://redirect.github.com/software-mansion/react-native-gesture-handler/issues/4411)) * Additional commits viewable in [compare view](software-mansion/react-native-gesture-handler@v2.32.0...v3.2.1)
Description
The Apple side of
onHoverIn/onHoverOut, covering iOS, tvOS andmacOS. Mirrors the Android state machine, with the pointer sampled from
UIHoverGestureRecognizeron iOS, theNSTrackingAreaon macOS, andthe touch stream during a press.
RNGHButtonPressEventDelegatebecomesRNGHButtonEventDelegatenowthat it carries more than presses.
plain pencil tap on an iPad that doesn't report pencil hover would open
a hover on touch-up that nothing could ever close.
zOffsetcollapses to zero as it approaches contact, which wouldotherwise make the hover-out claim a different type than the hover-in.
sample to report; the payload falls back to the button's centre.
Test plan
Not covered by the JS tests. Needs an iPad with a trackpad or a
hover-capable pencil, a Mac, and a tvOS device for the focus path.
Example code