Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions src/adapters/TouchablePreview.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { Platform, TouchableNativeFeedback, TouchableWithoutFeedback } from 'react-native';
import { TouchablePreview } from './TouchablePreview';

describe('TouchablePreview', () => {
const originalPlatform = Platform.OS;

afterEach(() => {
Platform.OS = originalPlatform;
TouchablePreview.peeking = false;
jest.restoreAllMocks();
jest.useRealTimers();
});

it('accepts any React element type as the touchable component', () => {
const MemoizedTouchable = React.memo(() => null);
const consoleError = jest.spyOn(console, 'error').mockImplementation();

PropTypes.checkPropTypes(
TouchablePreview.propTypes,
{ touchableComponent: MemoizedTouchable },
'prop',
'TouchablePreview'
);

expect(consoleError).not.toHaveBeenCalled();
});

it('falls back from TouchableNativeFeedback on iOS', () => {
Platform.OS = 'ios';
const uut = new TouchablePreview({ touchableComponent: TouchableNativeFeedback });

expect(uut.render().type).toBe(TouchableWithoutFeedback);
});

it('clears pending preview work when unmounted', () => {
jest.useFakeTimers();
const onPeekOut = jest.fn();
const uut = new TouchablePreview({ onPeekOut });
uut.onTouchStart({ nativeEvent: { timestamp: 0 } } as any);
uut.onTouchMove({ nativeEvent: { force: 0.5, timestamp: 400 } } as any);

(uut as any).componentWillUnmount();
jest.runOnlyPendingTimers();

expect(onPeekOut).not.toHaveBeenCalled();
expect(TouchablePreview.peeking).toBe(false);
});

it('does not clear another instance\'s active peek when unmounted', () => {
jest.useFakeTimers();
const owner = new TouchablePreview({});
const nonOwner = new TouchablePreview({});
owner.onTouchStart({ nativeEvent: { timestamp: 0 } } as any);
owner.onTouchMove({ nativeEvent: { force: 0.5, timestamp: 400 } } as any);

nonOwner.componentWillUnmount();

expect(TouchablePreview.peeking).toBe(true);

owner.componentWillUnmount();
expect(TouchablePreview.peeking).toBe(false);
});
});
38 changes: 21 additions & 17 deletions src/adapters/TouchablePreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import {
View,
Platform,
findNodeHandle,
TouchableOpacity,
TouchableHighlight,
TouchableNativeFeedback,
TouchableWithoutFeedback,
GestureResponderEvent,
Expand All @@ -21,12 +19,7 @@ interface GestureResponderEventWithForce extends NativeSyntheticEvent<NativeTouc

export interface Props {
children?: React.ReactNode;
touchableComponent?:
| typeof TouchableHighlight
| typeof TouchableOpacity
| TouchableNativeFeedback
| TouchableWithoutFeedback
| React.ReactNode;
touchableComponent?: React.ElementType;
onPress?: () => void;
onPressIn?: (payload: { reactTag: number | null }) => void;
onPeekIn?: () => void;
Expand All @@ -40,7 +33,7 @@ const PREVIEW_TIMEOUT = 1250;
export class TouchablePreview extends React.PureComponent<Props> {
static propTypes = {
children: PropTypes.node,
touchableComponent: PropTypes.func,
touchableComponent: PropTypes.elementType,
onPress: PropTypes.func,
onPressIn: PropTypes.func,
onPeekIn: PropTypes.func,
Expand All @@ -53,10 +46,17 @@ export class TouchablePreview extends React.PureComponent<Props> {
};

static peeking = false;
private static peekOwner: TouchablePreview | undefined;

private timeout: number | undefined;
private touchStartedAt: number = 0;
private onRef = React.createRef<any>();

componentWillUnmount() {
clearTimeout(this.timeout);
this.releasePeekOwnership();
}

onPress = () => {
const { onPress } = this.props;

Expand Down Expand Up @@ -96,6 +96,7 @@ export class TouchablePreview extends React.PureComponent<Props> {

if (force > PREVIEW_MIN_FORCE && diff > PREVIEW_DELAY) {
TouchablePreview.peeking = true;
TouchablePreview.peekOwner = this;

if (typeof this.props.onPeekIn === 'function') {
this.props.onPeekIn();
Expand All @@ -107,7 +108,7 @@ export class TouchablePreview extends React.PureComponent<Props> {

onTouchEnd = () => {
clearTimeout(this.timeout);
TouchablePreview.peeking = false;
this.releasePeekOwnership();

if (typeof this.props.onPeekOut === 'function') {
this.props.onPeekOut();
Expand All @@ -116,21 +117,17 @@ export class TouchablePreview extends React.PureComponent<Props> {

render() {
const { children, touchableComponent, ...props } = this.props;
const requestedTouchable = touchableComponent ?? TouchableWithoutFeedback;

// Default to TouchableWithoutFeedback for iOS if set to TouchableNativeFeedback
const Touchable =
Platform.OS === 'ios' && touchableComponent instanceof TouchableNativeFeedback
Platform.OS === 'ios' && requestedTouchable === TouchableNativeFeedback
? TouchableWithoutFeedback
: (touchableComponent as React.Component);
: requestedTouchable;

// Wrap component with Touchable for handling platform touches
// and a single react View for detecting force and timing.
return (
/**
* @TODO (Jin Shin 25 June 2020)
* Ignoring this for now so that it builds.
*/
// @ts-ignore
<Touchable {...props} ref={this.onRef} onPress={this.onPress} onPressIn={this.onPressIn}>
<View
onTouchStart={this.onTouchStart}
Expand All @@ -142,4 +139,11 @@ export class TouchablePreview extends React.PureComponent<Props> {
</Touchable>
);
}

private releasePeekOwnership() {
if (TouchablePreview.peekOwner === this) {
TouchablePreview.peekOwner = undefined;
TouchablePreview.peeking = false;
}
}
}