Skip to content

Commit 729e634

Browse files
authored
fix: support touch :hover on opacity:0 views on iOS (#9872)
## Description ### The issue On iOS, UIKit's hit-testing ignores views with `alpha < 0.01`, so an `opacity: 0` view could never receive touch `:hover`, unlike web and Android, where it stays hit-testable. ### The fix The `:hover` coordinator now briefly lifts each near-zero registered view above that threshold for the single hit-test and restores it before the next compositing pass, so `opacity: 0` views become hoverable with no visual change.
1 parent 57cc5e7 commit 729e634

2 files changed

Lines changed: 34 additions & 9 deletions

File tree

apps/common-app/src/apps/css/examples/transitions/screens/pseudoSelectors/Planets.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,14 @@ export default function Planets() {
6666
}}>
6767
<Animated.View
6868
style={{
69-
opacity: { default: 0.02, ':hover': 1 },
69+
opacity: { default: 0, ':hover': 1 },
7070
transitionDuration: '200ms',
71-
}}
72-
onStartShouldSetResponder={() => true}>
71+
}}>
7372
<Text>{name}</Text>
7473
</Animated.View>
7574
</Animated.View>`}
7675
collapsedCode={`opacity: {
77-
default: 0.02,
76+
default: 0,
7877
':hover': 1,
7978
},
8079
transform: {
@@ -118,11 +117,10 @@ boxShadow: {
118117
style={[
119118
styles.nameWrapper,
120119
{
121-
opacity: { ':hover': 1, default: 0.02 },
120+
opacity: { ':hover': 1, default: 0 },
122121
transitionDuration: '200ms',
123122
},
124-
]}
125-
onStartShouldSetResponder={() => true}>
123+
]}>
126124
<Text style={styles.name}>{planet.name}</Text>
127125
</Animated.View>
128126
</Animated.View>

packages/react-native-reanimated/apple/reanimated/apple/pseudoSelectors/REATouchHoverCoordinator.mm

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,15 +214,42 @@ - (void)setEntry:(REATouchHoverEntry *)entry hovered:(BOOL)hovered
214214
}
215215
}
216216

217+
// UIKit's -hitTest: skips views with alpha below ~0.01, so an `opacity: 0` view is unreachable. Bump each
218+
// near-zero registered view over the threshold for the hit-test, restoring before compositing (never drawn).
219+
- (UIView *)hitTestInWindow:(UIWindow *)window atPoint:(CGPoint)point
220+
{
221+
static const CGFloat kHitTestableAlpha = 0.02;
222+
NSMutableArray<UIView *> *lifted = nil;
223+
NSMutableArray<NSNumber *> *savedAlphas = nil;
224+
for (REATouchHoverEntry *entry in _entries) {
225+
UIView *view = entry->view;
226+
if (view == nil || view.alpha >= kHitTestableAlpha || [lifted containsObject:view]) {
227+
continue;
228+
}
229+
if (lifted == nil) {
230+
lifted = [NSMutableArray array];
231+
savedAlphas = [NSMutableArray array];
232+
}
233+
[lifted addObject:view];
234+
[savedAlphas addObject:@(view.alpha)];
235+
view.alpha = kHitTestableAlpha;
236+
}
237+
UIView *hit = [window hitTest:point withEvent:nil];
238+
[lifted enumerateObjectsUsingBlock:^(UIView *view, NSUInteger index, BOOL *stop) {
239+
view.alpha = savedAlphas[index].floatValue;
240+
}];
241+
return hit;
242+
}
243+
217244
- (void)recomputeAtWindowPoint:(CGPoint)inWindow
218245
{
219246
// Hit-test the topmost view: :hover applies to it and its ancestors only (matching CSS), so views
220247
// that merely overlap the hit branch no longer all activate. hitTest already honors z-order,
221-
// userInteractionEnabled, hidden and alpha, and returns nil over blank space (clears all :hover).
248+
// userInteractionEnabled and hidden, and returns nil over blank space (clears all :hover).
222249
UIView *hit = nil;
223250
UIWindow *window = _observedWindow;
224251
if (window != nil) {
225-
hit = [window hitTest:inWindow withEvent:nil];
252+
hit = [self hitTestInWindow:window atPoint:inWindow];
226253
}
227254
NSMutableArray<REATouchHoverEntry *> *dead = nil;
228255
for (REATouchHoverEntry *entry in _entries) {

0 commit comments

Comments
 (0)