[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
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