Skip to content

[apple] Handle hover in Touchable #4283

Open
m-bert wants to merge 10 commits into
mainfrom
@mbert/touchable-hover-ios
Open

[apple] Handle hover in Touchable #4283
m-bert wants to merge 10 commits into
mainfrom
@mbert/touchable-hover-ios

Conversation

@m-bert

@m-bert m-bert commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

Description

Follow up for Android PR which brings hover animations to iOS, macOS and tvOS platforms.

Of course docs/jsdoc will have to be unified between them, it will be done after merging either of these.

Test plan

Tested on existing Touchable example and the code below:
import * as React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import {
  GestureHandlerRootView,
  Touchable as GHTouchable,
} from 'react-native-gesture-handler';

const SLOW = 700;

export default function App() {
  const [pressCount, setPressCount] = React.useState(0);
  const [disabled, setDisabled] = React.useState(false);

  // Auto-toggle `disabled` every 2s so hover mask/resume can be tested with a
  // mouse held still over the button — tapping a separate control would move
  // the pointer off first. Hover and watch: the visual masks to default while
  // disabled and resumes the hover look when re-enabled, pointer unmoved.
  React.useEffect(() => {
    const id = setInterval(() => setDisabled((d) => !d), 2000);
    return () => clearInterval(id);
  }, []);

  return (
    <GestureHandlerRootView style={styles.root}>
      <Text style={styles.title}>Slow hover + press</Text>
      <Text style={styles.hint}>
        Use a mouse / stylus. Hover to grow & fade, press to shrink & fade more.
        Transitions should never flicker through the default state.
      </Text>

      <View style={styles.stage}>
        <GHTouchable
          // Press (active) visuals
          defaultOpacity={1}
          defaultScale={1}
          activeOpacity={0.3}
          activeScale={0.8}
          // Hover visuals
          hoverOpacity={0.6}
          hoverScale={1.2}
          // Underlay so the change is extra visible
          underlayColor="black"
          defaultUnderlayOpacity={0}
          hoverUnderlayOpacity={0.15}
          activeUnderlayOpacity={0.35}
          // Slow everything down: in/out for both tap and hover
          animationDuration={{
            tap: { in: SLOW, out: SLOW },
            hover: { in: SLOW, out: SLOW },
          }}
          disabled={disabled}
          style={styles.button}
          onPress={() => setPressCount((c) => c + 1)}>
          <Text style={styles.buttonText}>Hover / Press me</Text>
        </GHTouchable>
      </View>

      <Text style={styles.counter}>
        {disabled ? 'DISABLED' : 'enabled'} · Presses: {pressCount}
      </Text>
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  root: {
    flex: 1,
    backgroundColor: '#ecf0f1',
    alignItems: 'center',
    justifyContent: 'center',
    padding: 24,
  },
  title: {
    fontSize: 22,
    fontWeight: 'bold',
    color: '#2c3e50',
    marginBottom: 8,
  },
  hint: {
    fontSize: 14,
    color: '#7f8c8d',
    textAlign: 'center',
    marginBottom: 40,
  },
  stage: {
    width: 260,
    height: 260,
    alignItems: 'center',
    justifyContent: 'center',
  },
  button: {
    width: 180,
    height: 180,
    borderRadius: 24,
    backgroundColor: '#8e44ad',
    alignItems: 'center',
    justifyContent: 'center',
  },
  buttonText: {
    color: 'white',
    fontSize: 18,
    fontWeight: 'bold',
  },
  counter: {
    marginTop: 40,
    fontSize: 16,
    color: '#2c3e50',
    fontWeight: 'bold',
  },
});

Copilot AI review requested due to automatic review settings June 24, 2026 12:41

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 native hover-driven visual state support to the Touchable/RNGestureHandlerButton implementation on Apple platforms (iOS + macOS), aligning behavior with the existing web hover model and avoiding flicker during hover→press transitions.

Changes:

  • Extends the native button codegen spec with hover-related props (opacity/scale/underlay + hover in/out durations).
  • Implements hover tracking + animation orchestration in the Apple native button (iOS hover recognizer; macOS tracking area).
  • Updates public TS/JSDoc and docs to reflect hover availability (partial).

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
packages/react-native-gesture-handler/src/specs/RNGestureHandlerButtonNativeComponent.ts Adds hover-related native props to the shared codegen spec.
packages/react-native-gesture-handler/src/components/GestureHandlerButton.tsx Updates JSDoc platform notes for hover props.
packages/react-native-gesture-handler/apple/RNGestureHandlerButtonComponentView.mm Wires new hover props from Fabric props into the native button view.
packages/react-native-gesture-handler/apple/RNGestureHandlerButton.mm Implements hover state tracking + hover/press animation coordination for iOS/macOS.
packages/react-native-gesture-handler/apple/RNGestureHandlerButton.h Exposes hover properties/durations on the native button.
packages/docs-gesture-handler/docs/components/touchable.mdx Updates docs to mention hover on iOS (and hover timing text).

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

Comment thread packages/docs-gesture-handler/docs/components/touchable.mdx

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

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

@m-bert m-bert requested a review from j-piasecki June 24, 2026 13:18
Comment on lines +358 to +363
// Only default* changes warrant re-applying the start state, because that's
// the only visual applyStartAnimationState writes. hover*/active* are read
// live by the hover/press animations, so re-running here on a hover change
// would strand a currently-hovering button at the default visual (no
// enter/exit event follows to restore it) and could interrupt an in-flight
// press.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Claude may disagree, but this seems obvious, no? It's the same case with active* which existed before.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Actually he agrees 😅 (daa80ee)

image

return;
}

_userEnabled = userEnabled;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Shouldn't we also disable/enable the hover recognizer here? Or is that by design so the below works?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Or is that by design so the below works?

This 😅

[self animateHoverIn];
break;
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Shouldn't this also include Failed?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Comment on lines +611 to +615
[self animateTarget:target toOpacity:self.hoverOpacity scale:self.hoverScale duration:_hoverAnimationInDuration];

if ([self hasUnderlayAnimation]) {
[self animateUnderlayToOpacity:self.hoverUnderlayOpacity duration:_hoverAnimationInDuration];
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is repeated a number of times. Maybe it would make sense to update applyHoverState to instead be a generic applyState (or a better name) accepting state (resting, hovered, pressed) and optional duration, defaulting to the relevant (in/out) one for the state.

It would likely require tracking which state is displayed currently, besides isPressed/isHovered.

What do you think?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Maybe not exactly what you've suggested, but I've extracted part of it into a helper. Let me know if you prefer something else (165e15f)

Comment on lines +658 to +662
// A pointer press is bracketed by a hover-out just before touch-down (e.g.
// Apple Pencil). Defer the hover-out so an immediately following press
// (which cancels it in handleAnimatePressIn) wins, keeping the hover state
// for a flicker-free hover -> press -> hover transition. A real pointer
// leave has no press following, so the block runs and settles to default.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why do we need to schedule? If the hover-out is immediately followed by a press-down, doesn't the second animation cancel the first one before it's able to render the first frame?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Similar to Android (comment). On the other hand, I've not tested it on the physical iPad with Apple Pencil so I'm not sure. Simulator seems to handle it in a way that we can safely remove this defer, but I'm not 100% sure without physical device so I'd leave that.


[self.layer insertSublayer:_underlayLayer atIndex:0];

#if !TARGET_OS_TV && !TARGET_OS_OSX

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we make it work also on tvOS, where "hover" would be applied to the focused item?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Hmm, probably we could. I'd leave that investigation for a follow-up. Do you agree?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Okay, turns out that it was easier - b9c7207

m-bert and others added 5 commits June 29, 2026 08:40
## Description

This PR removes obsolete version checks from our `iOS` codebase.

Minimal targets for iOS and macOS were chosen based on supported
versions. tvOS was changed to match iOS

## Test plan

Checked that example apps (basic and macos) are built correctly,
…ion/react-native-gesture-handler into @mbert/touchable-hover-ios
@m-bert m-bert requested a review from j-piasecki June 29, 2026 11:22
@m-bert m-bert changed the title [iOS | macOS] Handle hover in Touchable [apple] Handle hover in Touchable Jun 29, 2026
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