-
Notifications
You must be signed in to change notification settings - Fork 24.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
1 parent
6239ace
commit 3212f7d
Showing
8 changed files
with
852 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
34
Libraries/Components/Pressable/__tests__/Pressable-test.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}, | ||
); | ||
}); | ||
}); |
49 changes: 49 additions & 0 deletions
49
Libraries/Components/Pressable/__tests__/__snapshots__/Pressable-test.js.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)> | ||
`; |
Oops, something went wrong.
3212f7d
There was a problem hiding this comment.
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? Thepressed
prop is only true after a long press for me... See: #29321