diff --git a/packages/react-native/Libraries/Components/Pressable/Pressable.js b/packages/react-native/Libraries/Components/Pressable/Pressable.js index 4873850d1e3ac4..36e095a7347e77 100644 --- a/packages/react-native/Libraries/Components/Pressable/Pressable.js +++ b/packages/react-native/Libraries/Components/Pressable/Pressable.js @@ -20,7 +20,7 @@ import type { AccessibilityState, AccessibilityValue, } from '../View/ViewAccessibility'; -import type {HoverStyle} from '../View/ViewPropTypes'; +import type {HoverEffect} from '../View/ViewPropTypes'; import {PressabilityDebugView} from '../../Pressability/PressabilityDebug'; import usePressability from '../../Pressability/usePressability'; @@ -33,9 +33,7 @@ import useAndroidRippleForView, { import * as React from 'react'; import {useImperativeHandle, useMemo, useRef, useState} from 'react'; -const defaultHoverStyle: HoverStyle = { - effectType: 'automatic', -}; +const defaultHoverEffect: HoverEffect = 'highlight'; type ViewStyleProp = $ElementType, 'style'>; @@ -44,7 +42,7 @@ export type StateCallbackType = $ReadOnly<{| |}>; type VisionOSProps = $ReadOnly<{| - visionos_hoverStyle?: ?HoverStyle, + visionos_hoverEffect?: ?HoverEffect, |}>; type Props = $ReadOnly<{| @@ -245,7 +243,7 @@ function Pressable(props: Props, forwardedRef): React.Node { style, testOnly_pressed, unstable_pressDelay, - visionos_hoverStyle = defaultHoverStyle, + visionos_hoverEffect = defaultHoverEffect, ...restProps } = props; @@ -356,7 +354,7 @@ function Pressable(props: Props, forwardedRef): React.Node { ref={mergedRef} style={typeof style === 'function' ? style({pressed}) : style} collapsable={false} - visionos_hoverStyle={visionos_hoverStyle}> + visionos_hoverEffect={visionos_hoverEffect}> {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 ad4a0663a739b8..ceebee7470a93b 100644 --- a/packages/react-native/Libraries/Components/Touchable/TouchableHighlight.js +++ b/packages/react-native/Libraries/Components/Touchable/TouchableHighlight.js @@ -9,7 +9,7 @@ */ import type {ColorValue} from '../../StyleSheet/StyleSheet'; -import type {HoverStyle} from '../View/ViewPropTypes'; +import type {HoverEffect} from '../View/ViewPropTypes'; import typeof TouchableWithoutFeedback from './TouchableWithoutFeedback'; import View from '../../Components/View/View'; @@ -34,7 +34,7 @@ type IOSProps = $ReadOnly<{| |}>; type VisionOSProps = $ReadOnly<{| - hoverStyle?: ?HoverStyle, + hoverEffect?: ?HoverEffect, |}>; type Props = $ReadOnly<{| @@ -347,7 +347,7 @@ class TouchableHighlight extends React.Component { nextFocusLeft={this.props.nextFocusLeft} nextFocusRight={this.props.nextFocusRight} nextFocusUp={this.props.nextFocusUp} - visionos_hoverStyle={this.props.hoverStyle} + visionos_hoverEffect={this.props.hoverEffect} 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 919e90dbfe9246..ec8485306a8050 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 {HoverStyle, TVParallaxProperties} from '../View/ViewPropTypes'; +import {HoverEffect, TVParallaxProperties} from '../View/ViewPropTypes'; import {TouchableMixin} from './Touchable'; import {TouchableWithoutFeedbackProps} from './TouchableWithoutFeedback'; @@ -90,7 +90,7 @@ export interface TouchableOpacityProps /** * Hover style to apply to the view. Only supported on VisionOS. */ - visionos_hoverStyle?: HoverStyle | undefined; + visionos_hoverEffect?: HoverEffect | undefined; } /** diff --git a/packages/react-native/Libraries/Components/Touchable/TouchableOpacity.js b/packages/react-native/Libraries/Components/Touchable/TouchableOpacity.js index 35a3897cd1d305..6fab99b54514b0 100644 --- a/packages/react-native/Libraries/Components/Touchable/TouchableOpacity.js +++ b/packages/react-native/Libraries/Components/Touchable/TouchableOpacity.js @@ -9,7 +9,7 @@ */ import type {ViewStyleProp} from '../../StyleSheet/StyleSheet'; -import type {HoverStyle} from '../View/ViewPropTypes'; +import type {HoverEffect} from '../View/ViewPropTypes'; import typeof TouchableWithoutFeedback from './TouchableWithoutFeedback'; import Animated from '../../Animated/Animated'; @@ -22,9 +22,7 @@ import flattenStyle from '../../StyleSheet/flattenStyle'; import Platform from '../../Utilities/Platform'; import * as React from 'react'; -const defaultHoverStyle: HoverStyle = { - effectType: 'automatic', -}; +const defaultHoverEffect: HoverEffect = 'highlight'; type TVProps = $ReadOnly<{| hasTVPreferredFocus?: ?boolean, @@ -36,7 +34,7 @@ type TVProps = $ReadOnly<{| |}>; type VisionOSProps = $ReadOnly<{| - visionos_hoverStyle?: ?HoverStyle, + visionos_hoverEffect?: ?HoverEffect, |}>; type Props = $ReadOnly<{| @@ -140,8 +138,8 @@ type State = $ReadOnly<{| * */ class TouchableOpacity extends React.Component { - static defaultProps: {|visionos_hoverStyle: HoverStyle|} = { - visionos_hoverStyle: defaultHoverStyle, + static defaultProps: {|visionos_hoverEffect: HoverEffect|} = { + visionos_hoverEffect: defaultHoverEffect, }; state: State = { @@ -300,7 +298,7 @@ class TouchableOpacity extends React.Component { nextFocusUp={this.props.nextFocusUp} hasTVPreferredFocus={this.props.hasTVPreferredFocus} hitSlop={this.props.hitSlop} - visionos_hoverStyle={this.props.visionos_hoverStyle} + visionos_hoverEffect={this.props.visionos_hoverEffect} 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 660e6e76a82bf4..91de6db091ddc5 100644 --- a/packages/react-native/Libraries/Components/View/ViewPropTypes.d.ts +++ b/packages/react-native/Libraries/Components/View/ViewPropTypes.d.ts @@ -16,20 +16,7 @@ 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 HoverEffect = 'lift' | 'highlight'; export type TVParallaxProperties = { /** @@ -140,7 +127,7 @@ export interface ViewPropsIOS extends TVViewPropsIOS { /** * Hover style to apply to the view. Only supported on VisionOS. */ - visionos_hoverStyle?: HoverStyle | undefined; + visionos_hoverEffect?: HoverEffect | 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 379b469c8af943..3c51edae052360 100644 --- a/packages/react-native/Libraries/Components/View/ViewPropTypes.js +++ b/packages/react-native/Libraries/Components/View/ViewPropTypes.js @@ -263,20 +263,7 @@ 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, -|}>; +export type HoverEffect = 'lift' | 'highlight'; type AndroidDrawable = AndroidDrawableThemeAttr | AndroidDrawableRipple; @@ -455,7 +442,7 @@ type IOSViewProps = $ReadOnly<{| /** * Hover style to apply to the view. Only supported on VisionOS. */ - visionos_hoverStyle?: ?HoverStyle, + visionos_hoverEffect?: ?HoverEffect, |}>; export type ViewProps = $ReadOnly<{| diff --git a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm index cb2afd402f0a63..0acb8d5e4af6b4 100644 --- a/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm +++ b/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm @@ -293,6 +293,12 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & oldViewProps.borderColors != newViewProps.borderColors) { needsInvalidateLayer = YES; } + +#if TARGET_OS_VISION + if (oldViewProps.visionos_hoverEffect != newViewProps.visionos_hoverEffect) { + [self updateHoverEffect:[NSString stringWithUTF8String:newViewProps.visionos_hoverEffect.c_str()]]; + } +#endif // `nativeId` if (oldViewProps.nativeId != newViewProps.nativeId) { @@ -513,6 +519,28 @@ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event } } +#if TARGET_OS_VISION +- (void) updateHoverEffect:(NSString*)hoverEffect { + if (hoverEffect == nil || [hoverEffect isEqualToString:@""]) { + self.hoverStyle = nil; + return; + } + + UIShape *shape = [UIShape rectShapeWithCornerRadius:self.layer.cornerRadius]; + id effect; + + if ([hoverEffect isEqualToString:@"lift"]) { + effect = [UIHoverLiftEffect effect]; + } else if ([hoverEffect isEqualToString:@"highlight"]) { + effect = [UIHoverHighlightEffect effect]; + } + + if (hoverEffect != nil) { + self.hoverStyle = [UIHoverStyle styleWithEffect:effect shape:shape]; + } +} +#endif + static RCTCornerRadii RCTCornerRadiiFromBorderRadii(BorderRadii borderRadii) { return RCTCornerRadii{ diff --git a/packages/react-native/React/Views/RCTView.h b/packages/react-native/React/Views/RCTView.h index ace9840f5c59d5..100cde3079f5c9 100644 --- a/packages/react-native/React/Views/RCTView.h +++ b/packages/react-native/React/Views/RCTView.h @@ -124,7 +124,7 @@ extern const UIAccessibilityTraits SwitchAccessibilityTrait; /** * 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; +@property (nonatomic, copy) NSString *hoverEffect; #endif /** diff --git a/packages/react-native/React/Views/RCTView.m b/packages/react-native/React/Views/RCTView.m index 3b4fb6076aab62..821a5b1a1981a4 100644 --- a/packages/react-native/React/Views/RCTView.m +++ b/packages/react-native/React/Views/RCTView.m @@ -668,33 +668,26 @@ - (UIEdgeInsets)bordersAsInsets #if TARGET_OS_VISION -- (void)setHoverStyleProperties:(NSDictionary *)hoverStyleProperties { - _hoverStyleProperties = hoverStyleProperties; +- (void)setHoverEffect:(NSString *)hoverEffect { + _hoverEffect = hoverEffect; - BOOL enabled = _hoverStyleProperties[@"enabled"] != nil ? [_hoverStyleProperties[@"enabled"] boolValue] : YES; - - if (!enabled || hoverStyleProperties == nil) { + if (hoverEffect == 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:_borderRadius]; + id effect; - 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]; + if ([hoverEffect isEqualToString:@"lift"]) { + effect = [UIHoverLiftEffect effect]; + } else if ([hoverEffect isEqualToString:@"highlight"]) { + effect = [UIHoverHighlightEffect effect]; } - self.hoverStyle = [UIHoverStyle styleWithEffect:hoverEffect shape:shape]; + if (hoverEffect != nil) { + self.hoverStyle = [UIHoverStyle styleWithEffect:effect shape:shape]; + } } #endif diff --git a/packages/react-native/React/Views/RCTViewManager.m b/packages/react-native/React/Views/RCTViewManager.m index a0a09439d0e4f9..a25c0cd498b4a7 100644 --- a/packages/react-native/React/Views/RCTViewManager.m +++ b/packages/react-native/React/Views/RCTViewManager.m @@ -194,7 +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_VISIONOS_VIEW_PROPERTY(visionos_hoverEffect, hoverEffect, NSString) 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) diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp index 58bb38b20385c1..22fa01d7170cae 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.cpp @@ -71,6 +71,17 @@ BaseViewProps::BaseViewProps( "backgroundColor", sourceProps.backgroundColor, {})), +#if TARGET_OS_VISION + visionos_hoverEffect( + CoreFeatures::enablePropIteratorSetter + ? sourceProps.visionos_hoverEffect + : convertRawProp( + context, + rawProps, + "visionos_hoverEffect", + sourceProps.visionos_hoverEffect, + {})), +#endif borderRadii( CoreFeatures::enablePropIteratorSetter ? sourceProps.borderRadii : convertRawProp( @@ -281,6 +292,9 @@ void BaseViewProps::setProp( RAW_SET_PROP_SWITCH_CASE_BASIC(collapsable); RAW_SET_PROP_SWITCH_CASE_BASIC(removeClippedSubviews); RAW_SET_PROP_SWITCH_CASE_BASIC(experimental_layoutConformance); +#if TARGET_OS_VISION + RAW_SET_PROP_SWITCH_CASE_BASIC(visionos_hoverEffect); +#endif // events field VIEW_EVENT_CASE(PointerEnter); VIEW_EVENT_CASE(PointerEnterCapture); diff --git a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h index b7bdd9afc7eb55..a70e8c578e8b9a 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h +++ b/packages/react-native/ReactCommon/react/renderer/components/view/BaseViewProps.h @@ -63,6 +63,10 @@ class BaseViewProps : public YogaStylableProps, public AccessibilityProps { PointerEventsMode pointerEvents{}; EdgeInsets hitSlop{}; bool onLayout{}; + +#if TARGET_OS_VISION + std::string visionos_hoverEffect{}; +#endif ViewEvents events{};