diff --git a/packages/react-native/Libraries/Components/Pressable/Pressable.js b/packages/react-native/Libraries/Components/Pressable/Pressable.js index ef8dccaa830d04..44587a81eb92ae 100644 --- a/packages/react-native/Libraries/Components/Pressable/Pressable.js +++ b/packages/react-native/Libraries/Components/Pressable/Pressable.js @@ -20,6 +20,7 @@ import type { AccessibilityState, AccessibilityValue, } from '../View/ViewAccessibility'; +import type {HoverStyle} from '../View/ViewPropTypes'; import {PressabilityDebugView} from '../../Pressability/PressabilityDebug'; import usePressability from '../../Pressability/usePressability'; @@ -32,12 +33,20 @@ import useAndroidRippleForView, { import * as React from 'react'; import {useMemo, useRef, useState} from 'react'; +const defaultHoverStyle: HoverStyle = { + effectType: 'automatic', +}; + type ViewStyleProp = $ElementType, 'style'>; export type StateCallbackType = $ReadOnly<{| pressed: boolean, |}>; +type VisionOSProps = $ReadOnly<{| + visionos_hoverStyle?: ?HoverStyle, +|}>; + type Props = $ReadOnly<{| /** * Accessibility. @@ -193,6 +202,10 @@ type Props = $ReadOnly<{| * https://github.com/facebook/react-native/issues/34424 */ 'aria-label'?: ?string, + /** + * Props needed for VisionOS. + */ + ...VisionOSProps, |}>; /** @@ -232,6 +245,7 @@ function Pressable(props: Props, forwardedRef): React.Node { style, testOnly_pressed, unstable_pressDelay, + visionos_hoverStyle = defaultHoverStyle, ...restProps } = props; @@ -341,7 +355,8 @@ function Pressable(props: Props, forwardedRef): React.Node { {...eventHandlers} ref={mergedRef} style={typeof style === 'function' ? style({pressed}) : style} - collapsable={false}> + collapsable={false} + visionos_hoverStyle={visionos_hoverStyle}> {typeof children === 'function' ? children({pressed}) : children} {__DEV__ ? : null} diff --git a/packages/react-native/Libraries/Components/Touchable/TouchableHighlight.js b/packages/react-native/Libraries/Components/Touchable/TouchableHighlight.js index f2f9c4e7829a4b..8a1cea8be323cc 100644 --- a/packages/react-native/Libraries/Components/Touchable/TouchableHighlight.js +++ b/packages/react-native/Libraries/Components/Touchable/TouchableHighlight.js @@ -9,6 +9,7 @@ */ import type {ColorValue} from '../../StyleSheet/StyleSheet'; +import type {HoverStyle} from '../View/ViewPropTypes'; import typeof TouchableWithoutFeedback from './TouchableWithoutFeedback'; import View from '../../Components/View/View'; @@ -32,10 +33,15 @@ type IOSProps = $ReadOnly<{| hasTVPreferredFocus?: ?boolean, |}>; +type VisionOSProps = $ReadOnly<{| + hoverStyle?: ?HoverStyle, +|}>; + type Props = $ReadOnly<{| ...React.ElementConfig, ...AndroidProps, ...IOSProps, + ...VisionOSProps, activeOpacity?: ?number, underlayColor?: ?ColorValue, @@ -341,6 +347,7 @@ class TouchableHighlight extends React.Component { nextFocusLeft={this.props.nextFocusLeft} nextFocusRight={this.props.nextFocusRight} nextFocusUp={this.props.nextFocusUp} + visionos_hoverStyle={this.props.hoverStyle} focusable={ this.props.focusable !== false && this.props.onPress !== undefined } diff --git a/packages/react-native/Libraries/Components/Touchable/TouchableOpacity.d.ts b/packages/react-native/Libraries/Components/Touchable/TouchableOpacity.d.ts index 37c4ce6df43e4c..919e90dbfe9246 100644 --- a/packages/react-native/Libraries/Components/Touchable/TouchableOpacity.d.ts +++ b/packages/react-native/Libraries/Components/Touchable/TouchableOpacity.d.ts @@ -11,7 +11,7 @@ import type * as React from 'react'; import {Constructor} from '../../../types/private/Utilities'; import {TimerMixin} from '../../../types/private/TimerMixin'; import {NativeMethods} from '../../../types/public/ReactNativeTypes'; -import {TVParallaxProperties} from '../View/ViewPropTypes'; +import {HoverStyle, TVParallaxProperties} from '../View/ViewPropTypes'; import {TouchableMixin} from './Touchable'; import {TouchableWithoutFeedbackProps} from './TouchableWithoutFeedback'; @@ -86,6 +86,11 @@ export interface TouchableOpacityProps * @platform android */ tvParallaxProperties?: TVParallaxProperties | undefined; + + /** + * Hover style to apply to the view. Only supported on VisionOS. + */ + visionos_hoverStyle?: HoverStyle | undefined; } /** diff --git a/packages/react-native/Libraries/Components/Touchable/TouchableOpacity.js b/packages/react-native/Libraries/Components/Touchable/TouchableOpacity.js index 789f0394343b11..a49eaa83dbbe5c 100644 --- a/packages/react-native/Libraries/Components/Touchable/TouchableOpacity.js +++ b/packages/react-native/Libraries/Components/Touchable/TouchableOpacity.js @@ -9,6 +9,7 @@ */ import type {ViewStyleProp} from '../../StyleSheet/StyleSheet'; +import type {HoverStyle} from '../View/ViewPropTypes'; import typeof TouchableWithoutFeedback from './TouchableWithoutFeedback'; import Animated from '../../Animated/Animated'; @@ -21,6 +22,10 @@ import flattenStyle from '../../StyleSheet/flattenStyle'; import Platform from '../../Utilities/Platform'; import * as React from 'react'; +const defaultHoverStyle: HoverStyle = { + effectType: 'automatic', +}; + type TVProps = $ReadOnly<{| hasTVPreferredFocus?: ?boolean, nextFocusDown?: ?number, @@ -30,9 +35,14 @@ type TVProps = $ReadOnly<{| nextFocusUp?: ?number, |}>; +type VisionOSProps = $ReadOnly<{| + visionos_hoverStyle?: ?HoverStyle, +|}>; + type Props = $ReadOnly<{| ...React.ElementConfig, ...TVProps, + ...VisionOSProps, activeOpacity?: ?number, style?: ?ViewStyleProp, @@ -130,6 +140,10 @@ type State = $ReadOnly<{| * */ class TouchableOpacity extends React.Component { + static defaultProps: {|visionos_hoverStyle: HoverStyle|} = { + visionos_hoverStyle: defaultHoverStyle, + }; + state: State = { anim: new Animated.Value(this._getChildStyleOpacityWithDefault()), pressability: new Pressability(this._createPressabilityConfig()), @@ -286,6 +300,7 @@ class TouchableOpacity extends React.Component { nextFocusUp={this.props.nextFocusUp} hasTVPreferredFocus={this.props.hasTVPreferredFocus} hitSlop={this.props.hitSlop} + visionos_hoverStyle={this.props.visionos_hoverStyle} focusable={ this.props.focusable !== false && this.props.onPress !== undefined } diff --git a/packages/react-native/Libraries/Components/View/ViewPropTypes.d.ts b/packages/react-native/Libraries/Components/View/ViewPropTypes.d.ts index 20ff434f97b879..627e94cd6437da 100644 --- a/packages/react-native/Libraries/Components/View/ViewPropTypes.d.ts +++ b/packages/react-native/Libraries/Components/View/ViewPropTypes.d.ts @@ -16,6 +16,21 @@ import {LayoutChangeEvent, PointerEvents} from '../../Types/CoreEventTypes'; import {Touchable} from '../Touchable/Touchable'; import {AccessibilityProps} from './ViewAccessibility'; +export type HoverStyle = { + /** + * If true the hover effect is enabled. Defaults to true. + */ + enabled: boolean; + /** + * Hover effect type to apply to the view. + */ + effectType: 'automatic' | 'lift' | 'highlight'; + /** + * Corner radius of the hover effect. + */ + cornerRadius?: number | undefined; +}; + export type TVParallaxProperties = { /** * If true, parallax effects are enabled. Defaults to true. @@ -122,6 +137,10 @@ export interface ViewPropsIOS extends TVViewPropsIOS { * Test and measure when using this property. */ shouldRasterizeIOS?: boolean | undefined; + /** + * Hover style to apply to the view. Only supported on VisionOS. + */ + visionos_hoverStyle?: HoverStyle | undefined; } export interface ViewPropsAndroid { diff --git a/packages/react-native/Libraries/Components/View/ViewPropTypes.js b/packages/react-native/Libraries/Components/View/ViewPropTypes.js index 692410cd89d176..0cd084b47cb170 100644 --- a/packages/react-native/Libraries/Components/View/ViewPropTypes.js +++ b/packages/react-native/Libraries/Components/View/ViewPropTypes.js @@ -263,6 +263,21 @@ type AndroidDrawableRipple = $ReadOnly<{| rippleRadius?: ?number, |}>; +export type HoverStyle = $ReadOnly<{| + /** + * If true the hover effect is enabled. Defaults to true. + */ + enabled?: ?boolean, + /** + * Hover effect type to apply to the view. + */ + effectType: 'automatic' | 'lift' | 'highlight', + /** + * Corner radius of the hover effect. + */ + cornerRadius?: ?number, +|}>; + type AndroidDrawable = AndroidDrawableThemeAttr | AndroidDrawableRipple; type AndroidViewProps = $ReadOnly<{| @@ -451,6 +466,11 @@ type IOSViewProps = $ReadOnly<{| * See https://reactnative.dev/docs/view#shouldrasterizeios */ shouldRasterizeIOS?: ?boolean, + + /** + * Hover style to apply to the view. Only supported on VisionOS. + */ + visionos_hoverStyle?: ?HoverStyle, |}>; export type ViewProps = $ReadOnly<{| diff --git a/packages/react-native/React/Views/RCTView.h b/packages/react-native/React/Views/RCTView.h index 200d8b451bf59e..ace9840f5c59d5 100644 --- a/packages/react-native/React/Views/RCTView.h +++ b/packages/react-native/React/Views/RCTView.h @@ -120,6 +120,13 @@ extern const UIAccessibilityTraits SwitchAccessibilityTrait; */ @property (nonatomic, assign) UIEdgeInsets hitTestEdgeInsets; +#if TARGET_OS_VISION +/** + * The hover style to apply to a view, including an effect and a shape to use for displaying that effect. + */ +@property (nonatomic, copy) NSDictionary *hoverStyleProperties; +#endif + /** * (Experimental and unused for Paper) Pointer event handlers. */ diff --git a/packages/react-native/React/Views/RCTView.m b/packages/react-native/React/Views/RCTView.m index 6e6f9cd2761035..e0e7a149a17de9 100644 --- a/packages/react-native/React/Views/RCTView.m +++ b/packages/react-native/React/Views/RCTView.m @@ -666,6 +666,38 @@ - (UIEdgeInsets)bordersAsInsets }; } + +#if TARGET_OS_VISION +- (void)setHoverStyleProperties:(NSDictionary *)hoverStyleProperties { + _hoverStyleProperties = hoverStyleProperties; + + BOOL enabled = _hoverStyleProperties[@"enabled"] != nil ? [_hoverStyleProperties[@"enabled"] boolValue] : YES; + + if (!enabled || hoverStyleProperties == nil) { + self.hoverStyle = nil; + return; + } + + NSString *effectType = (NSString *)[_hoverStyleProperties objectForKey:@"effectType"]; + NSNumber *cornerRadius = (NSNumber *)[_hoverStyleProperties objectForKey:@"cornerRadius"]; + + float cornerRadiusFloat = [cornerRadius floatValue]; + + UIShape *shape = [UIShape rectShapeWithCornerRadius:cornerRadiusFloat]; + id hoverEffect; + + if ([effectType isEqualToString:@"lift"]) { + hoverEffect = [UIHoverLiftEffect effect]; + } else if ([effectType isEqualToString:@"highlight"]) { + hoverEffect = [UIHoverHighlightEffect effect]; + } else if ([effectType isEqualToString:@"automatic"]) { + hoverEffect = [UIHoverAutomaticEffect effect]; + } + + self.hoverStyle = [UIHoverStyle styleWithEffect:hoverEffect shape:shape]; +} +#endif + - (RCTCornerRadii)cornerRadii { const BOOL isRTL = _reactLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft; diff --git a/packages/react-native/React/Views/RCTViewManager.m b/packages/react-native/React/Views/RCTViewManager.m index 66c449d7e81a00..3d07dd11fc5cc0 100644 --- a/packages/react-native/React/Views/RCTViewManager.m +++ b/packages/react-native/React/Views/RCTViewManager.m @@ -194,6 +194,7 @@ - (RCTShadowView *)shadowView RCT_REMAP_VIEW_PROPERTY(testID, reactAccessibilityElement.accessibilityIdentifier, NSString) RCT_EXPORT_VIEW_PROPERTY(backgroundColor, UIColor) +RCT_REMAP_VISIONOS_VIEW_PROPERTY(visionos_hoverStyle, hoverStyleProperties, NSDictionary) RCT_REMAP_VIEW_PROPERTY(backfaceVisibility, layer.doubleSided, css_backface_visibility_t) RCT_REMAP_VIEW_PROPERTY(opacity, alpha, CGFloat) RCT_REMAP_VIEW_PROPERTY(shadowColor, layer.shadowColor, CGColor)