Skip to content

Commit

Permalink
Release Pressable!
Browse files Browse the repository at this point in the history
Summary:
*Pressable* is a component which is intended to replace the Touchable* components such as *TouchableWithoutFeedback* and *TouchableOpacity*. The motivation is to make it easier to create custom visual touch feedback so that React Native apps are not easily identified by the “signature opacity fade” touch feedback.

We see this component as eventually deprecating all of the existing Touchable components.

Changelog:
[Added][General] New <Pressable> Component to make it easier to create touchable elements

Reviewed By: yungsters

Differential Revision: D19674480

fbshipit-source-id: 765d657f023caea459f02da25376e4d5a2efff8b
  • Loading branch information
elicwhite authored and facebook-github-bot committed Feb 21, 2020
1 parent 6239ace commit 3212f7d
Show file tree
Hide file tree
Showing 8 changed files with 852 additions and 0 deletions.
236 changes: 236 additions & 0 deletions Libraries/Components/Pressable/Pressable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
/**
* Copyright (c) Facebook, Inc. and its 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
*/

'use strict';

import * as React from 'react';
import {useMemo, useState, useRef, useImperativeHandle} from 'react';
import useAndroidRippleForView from './useAndroidRippleForView.js';
import type {
AccessibilityActionEvent,
AccessibilityActionInfo,
AccessibilityRole,
AccessibilityState,
AccessibilityValue,
} from '../View/ViewAccessibility.js';
import usePressability from '../../Pressability/usePressability.js';
import {normalizeRect, type RectOrSize} from '../../StyleSheet/Rect.js';
import type {ColorValue} from '../../StyleSheet/StyleSheetTypes.js';
import type {LayoutEvent, PressEvent} from '../../Types/CoreEventTypes.js';
import View from '../View/View';

type ViewStyleProp = $ElementType<React.ElementConfig<typeof View>, 'style'>;

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

type Props = $ReadOnly<{|
/**
* Accessibility.
*/
accessibilityActions?: ?$ReadOnlyArray<AccessibilityActionInfo>,
accessibilityElementsHidden?: ?boolean,
accessibilityHint?: ?Stringish,
accessibilityIgnoresInvertColors?: ?boolean,
accessibilityLabel?: ?Stringish,
accessibilityLiveRegion?: ?('none' | 'polite' | 'assertive'),
accessibilityRole?: ?AccessibilityRole,
accessibilityState?: ?AccessibilityState,
accessibilityValue?: ?AccessibilityValue,
accessibilityViewIsModal?: ?boolean,
accessible?: ?boolean,
focusable?: ?boolean,
importantForAccessibility?: ?('auto' | 'yes' | 'no' | 'no-hide-descendants'),
onAccessibilityAction?: ?(event: AccessibilityActionEvent) => mixed,

/**
* Either children or a render prop that receives a boolean reflecting whether
* the component is currently pressed.
*/
children: React.Node | ((state: StateCallbackType) => React.Node),

/**
* Duration (in milliseconds) from `onPressIn` before `onLongPress` is called.
*/
delayLongPress?: ?number,

/**
* Whether the press behavior is disabled.
*/
disabled?: ?boolean,

/**
* Additional distance outside of this view in which a press is detected.
*/
hitSlop?: ?RectOrSize,

/**
* Additional distance outside of this view in which a touch is considered a
* press before `onPressOut` is triggered.
*/
pressRectOffset?: ?RectOrSize,

/**
* Called when this view's layout changes.
*/
onLayout?: ?(event: LayoutEvent) => void,

/**
* Called when a long-tap gesture is detected.
*/
onLongPress?: ?(event: PressEvent) => void,

/**
* Called when a single tap gesture is detected.
*/
onPress?: ?(event: PressEvent) => void,

/**
* Called when a touch is engaged before `onPress`.
*/
onPressIn?: ?(event: PressEvent) => void,

/**
* Called when a touch is released before `onPress`.
*/
onPressOut?: ?(event: PressEvent) => void,

/**
* Either view styles or a function that receives a boolean reflecting whether
* the component is currently pressed and returns view styles.
*/
style?: ViewStyleProp | ((state: StateCallbackType) => ViewStyleProp),

/**
* Identifier used to find this view in tests.
*/
testID?: ?string,

/**
* If true, doesn't play system sound on touch.
*/
android_disableSound?: ?boolean,

/**
* Enables the Android ripple effect and configures its color.
*/
android_rippleColor?: ?ColorValue,

/**
* Used only for documentation or testing (e.g. snapshot testing).
*/
testOnly_pressed?: ?boolean,
|}>;

/**
* Component used to build display components that should respond to whether the
* component is currently pressed or not.
*/
function Pressable(props: Props, forwardedRef): React.Node {
const {
accessible,
android_disableSound,
android_rippleColor,
children,
delayLongPress,
disabled,
focusable,
onLongPress,
onPress,
onPressIn,
onPressOut,
pressRectOffset,
style,
testOnly_pressed,
...restProps
} = props;

const viewRef = useRef<React.ElementRef<typeof View> | null>(null);
useImperativeHandle(forwardedRef, () => viewRef.current);

const android_ripple = useAndroidRippleForView(android_rippleColor, viewRef);

const [pressed, setPressed] = usePressState(testOnly_pressed === true);

const hitSlop = normalizeRect(props.hitSlop);

const config = useMemo(
() => ({
disabled,
hitSlop,
pressRectOffset,
android_disableSound,
delayLongPress,
onLongPress,
onPress,
onPressIn(event: PressEvent): void {
if (android_ripple != null) {
android_ripple.onPressIn(event);
}
setPressed(true);
if (onPressIn != null) {
onPressIn(event);
}
},
onPressMove: android_ripple?.onPressMove,
onPressOut(event: PressEvent): void {
if (android_ripple != null) {
android_ripple.onPressOut(event);
}
setPressed(false);
if (onPressOut != null) {
onPressOut(event);
}
},
}),
[
android_disableSound,
android_ripple,
delayLongPress,
disabled,
hitSlop,
onLongPress,
onPress,
onPressIn,
onPressOut,
pressRectOffset,
setPressed,
],
);
const eventHandlers = usePressability(config);

return (
<View
{...restProps}
{...eventHandlers}
{...android_ripple?.viewProps}
accessible={accessible !== false}
focusable={focusable !== false}
hitSlop={hitSlop}
ref={viewRef}
style={typeof style === 'function' ? style({pressed}) : style}>
{typeof children === 'function' ? children({pressed}) : children}
</View>
);
}

function usePressState(forcePressed: boolean): [boolean, (boolean) => void] {
const [pressed, setPressed] = useState(false);
return [pressed || forcePressed, setPressed];
}

const MemodPressable = React.memo(React.forwardRef(Pressable));
MemodPressable.displayName = 'Pressable';

export default (MemodPressable: React.AbstractComponent<
Props,
React.ElementRef<typeof View>,
>);
34 changes: 34 additions & 0 deletions Libraries/Components/Pressable/__tests__/Pressable-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @emails oncall+react_native
* @flow strict-local
*/

'use strict';

import * as React from 'react';

import Pressable from '../Pressable';
import View from '../../View/View';
import {expectRendersMatchingSnapshot} from '../../../Utilities/ReactNativeTestTools';

describe('<Pressable />', () => {
it('should render as expected', () => {
expectRendersMatchingSnapshot(
'Pressable',
() => (
<Pressable>
<View />
</Pressable>
),
() => {
jest.dontMock('../Pressable');
},
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<Pressable /> should render as expected: should deep render when mocked (please verify output manually) 1`] = `
<View
accessible={true}
focusable={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
>
<View />
</View>
`;

exports[`<Pressable /> should render as expected: should deep render when not mocked (please verify output manually) 1`] = `
<View
accessible={true}
focusable={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
>
<View />
</View>
`;

exports[`<Pressable /> should render as expected: should shallow render as <Pressable /> when mocked 1`] = `
<Memo(Pressable)>
<View />
</Memo(Pressable)>
`;

exports[`<Pressable /> should render as expected: should shallow render as <Pressable /> when not mocked 1`] = `
<Memo(Pressable)>
<View />
</Memo(Pressable)>
`;
Loading

1 comment on commit 3212f7d

@mrousavy
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible that this causes delay for the pressIn event? The pressed prop is only true after a long press for me... See: #29321

Please sign in to comment.