react-native
Version:
A framework for building native apps using React
324 lines (291 loc) • 10.4 kB
JavaScript
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type {HostInstance} from '../../../src/private/types/HostInstance';
import type {ViewStyleProp} from '../../StyleSheet/StyleSheet';
import type {TouchableWithoutFeedbackProps} from './TouchableWithoutFeedback';
import Animated from '../../Animated/Animated';
import Easing from '../../Animated/Easing';
import Pressability, {
type PressabilityConfig,
} from '../../Pressability/Pressability';
import {PressabilityDebugView} from '../../Pressability/PressabilityDebug';
import flattenStyle from '../../StyleSheet/flattenStyle';
import Platform from '../../Utilities/Platform';
import * as React from 'react';
export type TouchableOpacityInstance = HostInstance;
export type TouchableOpacityTVProps = Readonly<{
/**
* *(Apple TV only)* TV preferred focus (see documentation for the View component).
*
* @platform ios
* @deprecated Use `focusable` instead
*/
hasTVPreferredFocus?: ?boolean,
/**
* Designates the next view to receive focus when the user navigates down. See the Android documentation.
*
* @platform android
*/
nextFocusDown?: ?number,
/**
* Designates the next view to receive focus when the user navigates forward. See the Android documentation.
*
* @platform android
*/
nextFocusForward?: ?number,
/**
* Designates the next view to receive focus when the user navigates left. See the Android documentation.
*
* @platform android
*/
nextFocusLeft?: ?number,
/**
* Designates the next view to receive focus when the user navigates right. See the Android documentation.
*
* @platform android
*/
nextFocusRight?: ?number,
/**
* Designates the next view to receive focus when the user navigates up. See the Android documentation.
*
* @platform android
*/
nextFocusUp?: ?number,
}>;
type TouchableOpacityBaseProps = Readonly<{
/**
* Opacity of the wrapped view when touch is active.
*
* @default `0.2`
*/
activeOpacity?: ?number,
style?: ?Animated.WithAnimatedValue<ViewStyleProp>,
hostRef?: ?React.RefSetter<TouchableOpacityInstance>,
}>;
export type TouchableOpacityProps = Readonly<{
...TouchableWithoutFeedbackProps,
...TouchableOpacityTVProps,
...TouchableOpacityBaseProps,
}>;
type TouchableOpacityState = Readonly<{
anim: Animated.Value,
pressability: Pressability,
}>;
/**
* A wrapper for making views respond properly to touches. On press down, the opacity of the wrapped view is decreased, dimming it.
*
* Opacity is controlled by wrapping the children in an `Animated.View`, which is added to the view hierarchy. Be aware that this can affect layout.
*
* If you need more extensive and future-proof touch handling, use `Pressable`.
*
* @see https://reactnative.dev/docs/touchableopacity
*/
class TouchableOpacity extends React.Component<
TouchableOpacityProps,
TouchableOpacityState,
> {
state: TouchableOpacityState = {
anim: new Animated.Value(this._getChildStyleOpacityWithDefault()),
pressability: new Pressability(this._createPressabilityConfig()),
};
_createPressabilityConfig(): PressabilityConfig {
return {
cancelable: !this.props.rejectResponderTermination,
disabled:
this.props.disabled ??
this.props['aria-disabled'] ??
this.props.accessibilityState?.disabled,
hitSlop: this.props.hitSlop,
delayLongPress: this.props.delayLongPress,
delayPressIn: this.props.delayPressIn,
delayPressOut: this.props.delayPressOut,
minPressDuration: 0,
pressRectOffset: this.props.pressRetentionOffset,
onBlur: event => {
if (Platform.isTV) {
this._opacityInactive(250);
}
if (this.props.onBlur != null) {
this.props.onBlur(event);
}
},
onFocus: event => {
if (Platform.isTV) {
this._opacityActive(150);
}
if (this.props.onFocus != null) {
this.props.onFocus(event);
}
},
onLongPress: this.props.onLongPress,
onPress: this.props.onPress,
onPressIn: event => {
this._opacityActive(
event.dispatchConfig.registrationName === 'onResponderGrant'
? 0
: 150,
);
if (this.props.onPressIn != null) {
this.props.onPressIn(event);
}
},
onPressOut: event => {
this._opacityInactive(250);
if (this.props.onPressOut != null) {
this.props.onPressOut(event);
}
},
};
}
/**
* Animate the touchable to a new opacity.
*/
_setOpacityTo(toValue: number, duration: number): void {
Animated.timing(this.state.anim, {
toValue,
duration,
easing: Easing.inOut(Easing.quad),
useNativeDriver: true,
}).start();
}
_opacityActive(duration: number): void {
this._setOpacityTo(this.props.activeOpacity ?? 0.2, duration);
}
_opacityInactive(duration: number): void {
this._setOpacityTo(this._getChildStyleOpacityWithDefault(), duration);
}
_getChildStyleOpacityWithDefault(): number {
// $FlowFixMe[underconstrained-implicit-instantiation]
// $FlowFixMe[prop-missing]
const opacity = flattenStyle(this.props.style)?.opacity;
return typeof opacity === 'number' ? opacity : 1;
}
render(): React.Node {
// BACKWARD-COMPATIBILITY: Focus and blur events were never supported before
// adopting `Pressability`, so preserve that behavior.
const {onBlur, onFocus, ...eventHandlersWithoutBlurAndFocus} =
this.state.pressability.getEventHandlers();
let _accessibilityState = {
busy: this.props['aria-busy'] ?? this.props.accessibilityState?.busy,
checked:
this.props['aria-checked'] ?? this.props.accessibilityState?.checked,
disabled:
this.props['aria-disabled'] ?? this.props.accessibilityState?.disabled,
expanded:
this.props['aria-expanded'] ?? this.props.accessibilityState?.expanded,
selected:
this.props['aria-selected'] ?? this.props.accessibilityState?.selected,
};
_accessibilityState =
this.props.disabled != null
? {
..._accessibilityState,
disabled: this.props.disabled,
}
: _accessibilityState;
const accessibilityValue = {
max: this.props['aria-valuemax'] ?? this.props.accessibilityValue?.max,
min: this.props['aria-valuemin'] ?? this.props.accessibilityValue?.min,
now: this.props['aria-valuenow'] ?? this.props.accessibilityValue?.now,
text: this.props['aria-valuetext'] ?? this.props.accessibilityValue?.text,
};
const accessibilityLiveRegion =
this.props['aria-live'] === 'off'
? 'none'
: (this.props['aria-live'] ?? this.props.accessibilityLiveRegion);
const accessibilityLabel =
this.props['aria-label'] ?? this.props.accessibilityLabel;
return (
<Animated.View
accessible={this.props.accessible !== false}
accessibilityLabel={accessibilityLabel}
accessibilityHint={this.props.accessibilityHint}
accessibilityLanguage={this.props.accessibilityLanguage}
accessibilityRole={this.props.accessibilityRole}
accessibilityState={_accessibilityState}
accessibilityActions={this.props.accessibilityActions}
onAccessibilityAction={this.props.onAccessibilityAction}
accessibilityValue={accessibilityValue}
importantForAccessibility={
this.props['aria-hidden'] === true
? 'no-hide-descendants'
: // $FlowFixMe[incompatible-type] - AnimatedProps types were made more strict and need refining at this call site
this.props.importantForAccessibility
}
accessibilityViewIsModal={
this.props['aria-modal'] ?? this.props.accessibilityViewIsModal
}
accessibilityLiveRegion={accessibilityLiveRegion}
accessibilityElementsHidden={
this.props['aria-hidden'] ?? this.props.accessibilityElementsHidden
}
style={[this.props.style, {opacity: this.state.anim}]}
nativeID={this.props.id ?? this.props.nativeID}
testID={this.props.testID}
onLayout={this.props.onLayout}
nextFocusDown={this.props.nextFocusDown}
nextFocusForward={this.props.nextFocusForward}
nextFocusLeft={this.props.nextFocusLeft}
nextFocusRight={this.props.nextFocusRight}
nextFocusUp={this.props.nextFocusUp}
hasTVPreferredFocus={this.props.hasTVPreferredFocus}
hitSlop={this.props.hitSlop}
focusable={
this.props.focusable !== false &&
this.props.onPress !== undefined &&
!this.props.disabled
}
// $FlowFixMe[prop-missing]
ref={this.props.hostRef}
{...eventHandlersWithoutBlurAndFocus}>
{this.props.children}
{__DEV__ ? (
<PressabilityDebugView color="cyan" hitSlop={this.props.hitSlop} />
) : null}
</Animated.View>
);
}
componentDidUpdate(
prevProps: TouchableOpacityProps,
prevState: TouchableOpacityState,
) {
this.state.pressability.configure(this._createPressabilityConfig());
if (
this.props.disabled !== prevProps.disabled ||
// $FlowFixMe[underconstrained-implicit-instantiation]
// $FlowFixMe[prop-missing]
flattenStyle(prevProps.style)?.opacity !==
// $FlowFixMe[underconstrained-implicit-instantiation]
// $FlowFixMe[prop-missing]
flattenStyle(this.props.style)?.opacity
) {
this._opacityInactive(250);
}
}
componentDidMount(): void {
this.state.pressability.configure(this._createPressabilityConfig());
}
componentWillUnmount(): void {
this.state.pressability.reset();
this.state.anim.resetAnimation();
}
}
const Touchable: component(
ref?: React.RefSetter<TouchableOpacityInstance>,
...props: TouchableOpacityProps
) = ({
ref,
...props
}: {
ref?: React.RefSetter<TouchableOpacityInstance>,
...TouchableOpacityProps,
}) => <TouchableOpacity {...props} hostRef={ref} />;
Touchable.displayName = 'TouchableOpacity';
export default Touchable;