[apple] Handle hover in Touchable #4283
Conversation
There was a problem hiding this comment.
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.
| // 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. |
There was a problem hiding this comment.
Claude may disagree, but this seems obvious, no? It's the same case with active* which existed before.
| return; | ||
| } | ||
|
|
||
| _userEnabled = userEnabled; |
There was a problem hiding this comment.
Shouldn't we also disable/enable the hover recognizer here? Or is that by design so the below works?
There was a problem hiding this comment.
Or is that by design so the below works?
This 😅
| [self animateHoverIn]; | ||
| break; | ||
| case UIGestureRecognizerStateEnded: | ||
| case UIGestureRecognizerStateCancelled: |
There was a problem hiding this comment.
Shouldn't this also include Failed?
There was a problem hiding this comment.
| [self animateTarget:target toOpacity:self.hoverOpacity scale:self.hoverScale duration:_hoverAnimationInDuration]; | ||
|
|
||
| if ([self hasUnderlayAnimation]) { | ||
| [self animateUnderlayToOpacity:self.hoverUnderlayOpacity duration:_hoverAnimationInDuration]; | ||
| } |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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)
| // 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. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Can we make it work also on tvOS, where "hover" would be applied to the focused item?
There was a problem hiding this comment.
Hmm, probably we could. I'd leave that investigation for a follow-up. Do you agree?
There was a problem hiding this comment.
Okay, turns out that it was easier - b9c7207
## 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
Touchable Touchable

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: