Skip to content

Commit 9427cb3

Browse files
committed
feat: implement visionos_hoverStyle prop
1 parent 65a7224 commit 9427cb3

File tree

9 files changed

+123
-2
lines changed

9 files changed

+123
-2
lines changed

packages/react-native/Libraries/Components/Pressable/Pressable.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import type {
2020
AccessibilityState,
2121
AccessibilityValue,
2222
} from '../View/ViewAccessibility';
23+
import type {HoverStyle} from '../View/ViewPropTypes';
2324

2425
import {PressabilityDebugView} from '../../Pressability/PressabilityDebug';
2526
import usePressability from '../../Pressability/usePressability';
@@ -32,12 +33,20 @@ import useAndroidRippleForView, {
3233
import * as React from 'react';
3334
import {useMemo, useRef, useState} from 'react';
3435

36+
const defaultHoverStyle: HoverStyle = {
37+
effectType: 'automatic',
38+
};
39+
3540
type ViewStyleProp = $ElementType<React.ElementConfig<typeof View>, 'style'>;
3641

3742
export type StateCallbackType = $ReadOnly<{|
3843
pressed: boolean,
3944
|}>;
4045

46+
type VisionOSProps = $ReadOnly<{|
47+
visionos_hoverStyle?: ?HoverStyle,
48+
|}>;
49+
4150
type Props = $ReadOnly<{|
4251
/**
4352
* Accessibility.
@@ -193,6 +202,10 @@ type Props = $ReadOnly<{|
193202
* https://github.com/facebook/react-native/issues/34424
194203
*/
195204
'aria-label'?: ?string,
205+
/**
206+
* Props needed for VisionOS.
207+
*/
208+
...VisionOSProps,
196209
|}>;
197210

198211
/**
@@ -232,6 +245,7 @@ function Pressable(props: Props, forwardedRef): React.Node {
232245
style,
233246
testOnly_pressed,
234247
unstable_pressDelay,
248+
visionos_hoverStyle = defaultHoverStyle,
235249
...restProps
236250
} = props;
237251

@@ -341,7 +355,8 @@ function Pressable(props: Props, forwardedRef): React.Node {
341355
{...eventHandlers}
342356
ref={mergedRef}
343357
style={typeof style === 'function' ? style({pressed}) : style}
344-
collapsable={false}>
358+
collapsable={false}
359+
visionos_hoverStyle={visionos_hoverStyle}>
345360
{typeof children === 'function' ? children({pressed}) : children}
346361
{__DEV__ ? <PressabilityDebugView color="red" hitSlop={hitSlop} /> : null}
347362
</View>

packages/react-native/Libraries/Components/Touchable/TouchableHighlight.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
*/
1010

1111
import type {ColorValue} from '../../StyleSheet/StyleSheet';
12+
import type {HoverStyle} from '../View/ViewPropTypes';
1213
import typeof TouchableWithoutFeedback from './TouchableWithoutFeedback';
1314

1415
import View from '../../Components/View/View';
@@ -32,10 +33,15 @@ type IOSProps = $ReadOnly<{|
3233
hasTVPreferredFocus?: ?boolean,
3334
|}>;
3435

36+
type VisionOSProps = $ReadOnly<{|
37+
hoverStyle?: ?HoverStyle,
38+
|}>;
39+
3540
type Props = $ReadOnly<{|
3641
...React.ElementConfig<TouchableWithoutFeedback>,
3742
...AndroidProps,
3843
...IOSProps,
44+
...VisionOSProps,
3945

4046
activeOpacity?: ?number,
4147
underlayColor?: ?ColorValue,
@@ -341,6 +347,7 @@ class TouchableHighlight extends React.Component<Props, State> {
341347
nextFocusLeft={this.props.nextFocusLeft}
342348
nextFocusRight={this.props.nextFocusRight}
343349
nextFocusUp={this.props.nextFocusUp}
350+
visionos_hoverStyle={this.props.hoverStyle}
344351
focusable={
345352
this.props.focusable !== false && this.props.onPress !== undefined
346353
}

packages/react-native/Libraries/Components/Touchable/TouchableOpacity.d.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type * as React from 'react';
1111
import {Constructor} from '../../../types/private/Utilities';
1212
import {TimerMixin} from '../../../types/private/TimerMixin';
1313
import {NativeMethods} from '../../../types/public/ReactNativeTypes';
14-
import {TVParallaxProperties} from '../View/ViewPropTypes';
14+
import {HoverStyle, TVParallaxProperties} from '../View/ViewPropTypes';
1515
import {TouchableMixin} from './Touchable';
1616
import {TouchableWithoutFeedbackProps} from './TouchableWithoutFeedback';
1717

@@ -86,6 +86,11 @@ export interface TouchableOpacityProps
8686
* @platform android
8787
*/
8888
tvParallaxProperties?: TVParallaxProperties | undefined;
89+
90+
/**
91+
* Hover style to apply to the view. Only supported on VisionOS.
92+
*/
93+
visionos_hoverStyle?: HoverStyle | undefined;
8994
}
9095

9196
/**

packages/react-native/Libraries/Components/Touchable/TouchableOpacity.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
*/
1010

1111
import type {ViewStyleProp} from '../../StyleSheet/StyleSheet';
12+
import type {HoverStyle} from '../View/ViewPropTypes';
1213
import typeof TouchableWithoutFeedback from './TouchableWithoutFeedback';
1314

1415
import Animated from '../../Animated/Animated';
@@ -21,6 +22,10 @@ import flattenStyle from '../../StyleSheet/flattenStyle';
2122
import Platform from '../../Utilities/Platform';
2223
import * as React from 'react';
2324

25+
const defaultHoverStyle: HoverStyle = {
26+
effectType: 'automatic',
27+
};
28+
2429
type TVProps = $ReadOnly<{|
2530
hasTVPreferredFocus?: ?boolean,
2631
nextFocusDown?: ?number,
@@ -30,9 +35,14 @@ type TVProps = $ReadOnly<{|
3035
nextFocusUp?: ?number,
3136
|}>;
3237

38+
type VisionOSProps = $ReadOnly<{|
39+
visionos_hoverStyle?: ?HoverStyle,
40+
|}>;
41+
3342
type Props = $ReadOnly<{|
3443
...React.ElementConfig<TouchableWithoutFeedback>,
3544
...TVProps,
45+
...VisionOSProps,
3646

3747
activeOpacity?: ?number,
3848
style?: ?ViewStyleProp,
@@ -130,6 +140,10 @@ type State = $ReadOnly<{|
130140
*
131141
*/
132142
class TouchableOpacity extends React.Component<Props, State> {
143+
static defaultProps: {|visionos_hoverStyle: HoverStyle|} = {
144+
visionos_hoverStyle: defaultHoverStyle,
145+
};
146+
133147
state: State = {
134148
anim: new Animated.Value(this._getChildStyleOpacityWithDefault()),
135149
pressability: new Pressability(this._createPressabilityConfig()),
@@ -286,6 +300,7 @@ class TouchableOpacity extends React.Component<Props, State> {
286300
nextFocusUp={this.props.nextFocusUp}
287301
hasTVPreferredFocus={this.props.hasTVPreferredFocus}
288302
hitSlop={this.props.hitSlop}
303+
visionos_hoverStyle={this.props.visionos_hoverStyle}
289304
focusable={
290305
this.props.focusable !== false && this.props.onPress !== undefined
291306
}

packages/react-native/Libraries/Components/View/ViewPropTypes.d.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,21 @@ import {LayoutChangeEvent, PointerEvents} from '../../Types/CoreEventTypes';
1616
import {Touchable} from '../Touchable/Touchable';
1717
import {AccessibilityProps} from './ViewAccessibility';
1818

19+
export type HoverStyle = {
20+
/**
21+
* If true the hover effect is enabled. Defaults to true.
22+
*/
23+
enabled: boolean;
24+
/**
25+
* Hover effect type to apply to the view.
26+
*/
27+
effectType: 'automatic' | 'lift' | 'highlight';
28+
/**
29+
* Corner radius of the hover effect.
30+
*/
31+
cornerRadius?: number | undefined;
32+
};
33+
1934
export type TVParallaxProperties = {
2035
/**
2136
* If true, parallax effects are enabled. Defaults to true.
@@ -122,6 +137,10 @@ export interface ViewPropsIOS extends TVViewPropsIOS {
122137
* Test and measure when using this property.
123138
*/
124139
shouldRasterizeIOS?: boolean | undefined;
140+
/**
141+
* Hover style to apply to the view. Only supported on VisionOS.
142+
*/
143+
visionos_hoverStyle?: HoverStyle | undefined;
125144
}
126145

127146
export interface ViewPropsAndroid {

packages/react-native/Libraries/Components/View/ViewPropTypes.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,21 @@ type AndroidDrawableRipple = $ReadOnly<{|
263263
rippleRadius?: ?number,
264264
|}>;
265265

266+
export type HoverStyle = $ReadOnly<{|
267+
/**
268+
* If true the hover effect is enabled. Defaults to true.
269+
*/
270+
enabled?: ?boolean,
271+
/**
272+
* Hover effect type to apply to the view.
273+
*/
274+
effectType: 'automatic' | 'lift' | 'highlight',
275+
/**
276+
* Corner radius of the hover effect.
277+
*/
278+
cornerRadius?: ?number,
279+
|}>;
280+
266281
type AndroidDrawable = AndroidDrawableThemeAttr | AndroidDrawableRipple;
267282

268283
type AndroidViewProps = $ReadOnly<{|
@@ -451,6 +466,11 @@ type IOSViewProps = $ReadOnly<{|
451466
* See https://reactnative.dev/docs/view#shouldrasterizeios
452467
*/
453468
shouldRasterizeIOS?: ?boolean,
469+
470+
/**
471+
* Hover style to apply to the view. Only supported on VisionOS.
472+
*/
473+
visionos_hoverStyle?: ?HoverStyle,
454474
|}>;
455475

456476
export type ViewProps = $ReadOnly<{|

packages/react-native/React/Views/RCTView.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,13 @@ extern const UIAccessibilityTraits SwitchAccessibilityTrait;
120120
*/
121121
@property (nonatomic, assign) UIEdgeInsets hitTestEdgeInsets;
122122

123+
#if TARGET_OS_VISION
124+
/**
125+
* The hover style to apply to a view, including an effect and a shape to use for displaying that effect.
126+
*/
127+
@property (nonatomic, copy) NSDictionary *hoverStyleProperties;
128+
#endif
129+
123130
/**
124131
* (Experimental and unused for Paper) Pointer event handlers.
125132
*/

packages/react-native/React/Views/RCTView.m

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,38 @@ - (UIEdgeInsets)bordersAsInsets
666666
};
667667
}
668668

669+
670+
#if TARGET_OS_VISION
671+
- (void)setHoverStyleProperties:(NSDictionary *)hoverStyleProperties {
672+
_hoverStyleProperties = hoverStyleProperties;
673+
674+
BOOL enabled = _hoverStyleProperties[@"enabled"] != nil ? [_hoverStyleProperties[@"enabled"] boolValue] : YES;
675+
676+
if (!enabled || hoverStyleProperties == nil) {
677+
self.hoverStyle = nil;
678+
return;
679+
}
680+
681+
NSString *effectType = (NSString *)[_hoverStyleProperties objectForKey:@"effectType"];
682+
NSNumber *cornerRadius = (NSNumber *)[_hoverStyleProperties objectForKey:@"cornerRadius"];
683+
684+
float cornerRadiusFloat = [cornerRadius floatValue];
685+
686+
UIShape *shape = [UIShape rectShapeWithCornerRadius:cornerRadiusFloat];
687+
id<UIHoverEffect> hoverEffect;
688+
689+
if ([effectType isEqualToString:@"lift"]) {
690+
hoverEffect = [UIHoverLiftEffect effect];
691+
} else if ([effectType isEqualToString:@"highlight"]) {
692+
hoverEffect = [UIHoverHighlightEffect effect];
693+
} else if ([effectType isEqualToString:@"automatic"]) {
694+
hoverEffect = [UIHoverAutomaticEffect effect];
695+
}
696+
697+
self.hoverStyle = [UIHoverStyle styleWithEffect:hoverEffect shape:shape];
698+
}
699+
#endif
700+
669701
- (RCTCornerRadii)cornerRadii
670702
{
671703
const BOOL isRTL = _reactLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft;

packages/react-native/React/Views/RCTViewManager.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ - (RCTShadowView *)shadowView
194194
RCT_REMAP_VIEW_PROPERTY(testID, reactAccessibilityElement.accessibilityIdentifier, NSString)
195195

196196
RCT_EXPORT_VIEW_PROPERTY(backgroundColor, UIColor)
197+
RCT_REMAP_VISIONOS_VIEW_PROPERTY(visionos_hoverStyle, hoverStyleProperties, NSDictionary)
197198
RCT_REMAP_VIEW_PROPERTY(backfaceVisibility, layer.doubleSided, css_backface_visibility_t)
198199
RCT_REMAP_VIEW_PROPERTY(opacity, alpha, CGFloat)
199200
RCT_REMAP_VIEW_PROPERTY(shadowColor, layer.shadowColor, CGColor)

0 commit comments

Comments
 (0)