Skip to content

[Web] Add hover callbacks to Touchable - #4398

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

[Web] Add hover callbacks to Touchable#4398
j-piasecki merged 1 commit into
jpiasecki/touchable-hover-iosfrom
jpiasecki/touchable-hover-web

Conversation

@j-piasecki

Copy link
Copy Markdown
Member

Description

Hover on web comes straight from the onPointerEnter/onPointerLeave
the button already uses for its own animation, rather than through the
press pipeline.

  • Reported from the pointer handlers rather than an effect, so a leave
    and a re-enter batched into one render still produce both events. The
    effect only covers enabled flipping while the pointer is inside.
  • hovered is now tracked regardless of enabled and masked at render,
    so hover resumes on its own when enabled flips back with the pointer
    still inside — matching the native platforms.
  • The payload uses the same coordinate basis as PointerEventManager's
    mapEvent. Both reads force a layout flush, so it's only built when a
    hover callback is actually present.

Test plan

The .web variant isn't resolved by the React Native Jest preset, so
this needs a browser — with a mouse and with a pen.

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

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d3b3f188-54f7-4670-ba55-496f3ff094c4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)

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-web branch from 110fe61 to c24a2eb 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

This PR updates the web implementation of GestureHandlerButton to emit onButtonHoverIn / onButtonHoverOut callbacks directly from onPointerEnter / onPointerLeave, with event payloads aligned to the same coordinate basis used by the web pointer event pipeline.

Changes:

  • Added onButtonHoverIn / onButtonHoverOut props to the web button component and dispatch logic that masks hover by enabled while still tracking hover state.
  • Introduced a lightweight pointer-event-to-ButtonEvent sampler (gated behind the presence of hover callbacks) to avoid unnecessary layout reads.
  • Ensured hover transitions are emitted even when leave/re-enter events are batched, and when enabled toggles while the pointer remains inside.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@j-piasecki
j-piasecki force-pushed the jpiasecki/touchable-hover-web branch from c24a2eb to eab5d27 Compare August 7, 2026 09:52
@j-piasecki
j-piasecki force-pushed the jpiasecki/touchable-hover-web branch 2 times, most recently from 95a1352 to bd37b23 Compare August 7, 2026 12:54
## Description

Hover on web comes straight from the `onPointerEnter`/`onPointerLeave`
the button already uses for its own animation, rather than through the
press pipeline.

- Reported from the pointer handlers rather than an effect, so a leave
and a re-enter batched into one render still produce both events. The
effect only covers `enabled` flipping while the pointer is inside.
- `hovered` is now tracked regardless of `enabled` and masked at render,
so hover resumes on its own when `enabled` flips back with the pointer
still inside — matching the native platforms.
- The payload uses the same coordinate basis as `PointerEventManager`'s
`mapEvent`. Both reads force a layout flush, so it's only built when a
hover callback is actually present.

## Test plan

The `.web` variant isn't resolved by the React Native Jest preset, so
this needs a browser — with a mouse and with a pen.

<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-web branch from bd37b23 to 718880f Compare August 10, 2026 06:46
@j-piasecki
j-piasecki merged commit 1994741 into main Aug 10, 2026
5 of 9 checks passed
@j-piasecki
j-piasecki deleted the jpiasecki/touchable-hover-web 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