Skip to content

Commit

Permalink
feat: implement visionos_hoverStyle prop
Browse files Browse the repository at this point in the history
  • Loading branch information
okwasniewski committed Jan 11, 2024
1 parent ad3e3d4 commit 783c2bd
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<React.ElementConfig<typeof View>, 'style'>;

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

type VisionOSProps = $ReadOnly<{|
visionos_hoverStyle?: ?HoverStyle,
|}>;

type Props = $ReadOnly<{|
/**
* Accessibility.
Expand Down Expand Up @@ -193,6 +202,10 @@ type Props = $ReadOnly<{|
* https://github.com/facebook/react-native/issues/34424
*/
'aria-label'?: ?string,
/**
* Props needed for VisionOS.
*/
...VisionOSProps,
|}>;

/**
Expand Down Expand Up @@ -232,6 +245,7 @@ function Pressable(props: Props, forwardedRef): React.Node {
style,
testOnly_pressed,
unstable_pressDelay,
visionos_hoverStyle = defaultHoverStyle,
...restProps
} = props;

Expand Down Expand Up @@ -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__ ? <PressabilityDebugView color="red" hitSlop={hitSlop} /> : null}
</View>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -32,10 +33,15 @@ type IOSProps = $ReadOnly<{|
hasTVPreferredFocus?: ?boolean,
|}>;

type VisionOSProps = $ReadOnly<{|
hoverStyle?: ?HoverStyle,
|}>;

type Props = $ReadOnly<{|
...React.ElementConfig<TouchableWithoutFeedback>,
...AndroidProps,
...IOSProps,
...VisionOSProps,

activeOpacity?: ?number,
underlayColor?: ?ColorValue,
Expand Down Expand Up @@ -341,6 +347,7 @@ class TouchableHighlight extends React.Component<Props, State> {
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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand All @@ -30,9 +35,14 @@ type TVProps = $ReadOnly<{|
nextFocusUp?: ?number,
|}>;

type VisionOSProps = $ReadOnly<{|
visionos_hoverStyle?: ?HoverStyle,
|}>;

type Props = $ReadOnly<{|
...React.ElementConfig<TouchableWithoutFeedback>,
...TVProps,
...VisionOSProps,

activeOpacity?: ?number,
style?: ?ViewStyleProp,
Expand Down Expand Up @@ -130,6 +140,10 @@ type State = $ReadOnly<{|
*
*/
class TouchableOpacity extends React.Component<Props, State> {
static defaultProps: {|visionos_hoverStyle: HoverStyle|} = {
visionos_hoverStyle: defaultHoverStyle,
};

state: State = {
anim: new Animated.Value(this._getChildStyleOpacityWithDefault()),
pressability: new Pressability(this._createPressabilityConfig()),
Expand Down Expand Up @@ -286,6 +300,7 @@ class TouchableOpacity extends React.Component<Props, State> {
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
}
Expand Down
19 changes: 19 additions & 0 deletions packages/react-native/Libraries/Components/View/ViewPropTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down
20 changes: 20 additions & 0 deletions packages/react-native/Libraries/Components/View/ViewPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<{|
Expand Down Expand Up @@ -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<{|
Expand Down
7 changes: 7 additions & 0 deletions packages/react-native/React/Views/RCTView.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
32 changes: 32 additions & 0 deletions packages/react-native/React/Views/RCTView.m
Original file line number Diff line number Diff line change
Expand Up @@ -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<UIHoverEffect> 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;
Expand Down
1 change: 1 addition & 0 deletions packages/react-native/React/Views/RCTViewManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 783c2bd

Please sign in to comment.