Skip to content

[iOS] Add hover callbacks to Touchable - #4397

Merged
j-piasecki merged 1 commit into
jpiasecki/touchable-hover-androidfrom
jpiasecki/touchable-hover-ios
Aug 10, 2026
Merged

[iOS] Add hover callbacks to Touchable#4397
j-piasecki merged 1 commit into
jpiasecki/touchable-hover-androidfrom
jpiasecki/touchable-hover-ios

Conversation

@j-piasecki

Copy link
Copy Markdown
Member

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.

Example code
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' },
});

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added hover-in and hover-out events for buttons on Apple platforms.
    • Hover events now include pointer position, absolute coordinates, and pointer type.
    • Improved hover behavior across mouse, touch, Pencil, focus, drag, and release interactions.
  • Bug Fixes

    • Prevented duplicate or unbalanced hover events during state changes, recycling, and view transitions.
    • Improved hover handling for disabled buttons and hit-slop boundaries.

Walkthrough

Changes

The 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

Layer / File(s) Summary
Generalize the button event contract
packages/react-native-gesture-handler/apple/RNGestureHandlerButton.h
The event enum adds hover-in and hover-out values. The delegate protocol and property now support press and hover events.
Track and balance hover state
packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm
The button tracks effective hover, coordinates, pointer type, lifecycle resets, enabled-state transitions, animations, and balanced event dispatch.
Sample platform hover input
packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm
iOS, macOS, and touch handlers record hover samples and dispatch transitions during entry, dragging, release, and cancellation.
Forward hover events to Fabric
packages/react-native-gesture-handler/apple/RNGestureHandlerButtonComponentView.mm
The component view uses the generalized delegate and emits Fabric hover-in and hover-out events with event data.

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
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the main change, adding hover callbacks to Touchable, although it names iOS while the implementation also covers tvOS and macOS.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI review requested due to automatic review settings August 7, 2026 08:11
@j-piasecki
j-piasecki force-pushed the jpiasecki/touchable-hover-ios branch from 4a471bb to 2dcad4a Compare August 7, 2026 08:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 (RNGHButtonPressEventDelegateRNGHButtonEventDelegate).
  • 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.

@j-piasecki
j-piasecki force-pushed the jpiasecki/touchable-hover-ios branch from 2dcad4a to 73aa555 Compare August 7, 2026 09:52
@j-piasecki
j-piasecki force-pushed the jpiasecki/touchable-hover-ios branch from 73aa555 to c277136 Compare August 7, 2026 12:45
@j-piasecki
j-piasecki force-pushed the jpiasecki/touchable-hover-ios branch from c277136 to 8c91c68 Compare August 7, 2026 12:54

@m-bert m-bert left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@j-piasecki
j-piasecki force-pushed the jpiasecki/touchable-hover-ios branch from 8c91c68 to 9af3c7b Compare August 10, 2026 06:46
@j-piasecki
j-piasecki merged commit 34f5e48 into main Aug 10, 2026
4 of 7 checks passed
@j-piasecki
j-piasecki deleted the jpiasecki/touchable-hover-ios branch August 10, 2026 07:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants