Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SwipeableRow: Remove PropTypes, convert to ES6 class #21386

Closed
wants to merge 2 commits into from
Closed
Changes from 1 commit
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
205 changes: 94 additions & 111 deletions Libraries/Experimental/SwipeableRow/SwipeableRow.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,8 @@ const PanResponder = require('PanResponder');
const React = require('React');
const PropTypes = require('prop-types');
const StyleSheet = require('StyleSheet');
/* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses an error
* found when Flow v0.54 was deployed. To see the error delete this comment and
* run Flow. */
const TimerMixin = require('react-timer-mixin');
const View = require('View');

const createReactClass = require('create-react-class');
const emptyFunction = require('fbjs/lib/emptyFunction');

import type {LayoutEvent, PressEvent} from 'CoreEventTypes';
Expand Down Expand Up @@ -63,117 +58,110 @@ const RIGHT_SWIPE_THRESHOLD = 30 * SLOW_SPEED_SWIPE_FACTOR;
type Props = $ReadOnly<{|
children?: ?React.Node,
isOpen?: ?boolean,
maxSwipeDistance?: ?number,
maxSwipeDistance: number,
onClose?: ?Function,
onOpen?: ?Function,
onSwipeEnd?: ?Function,
onSwipeStart?: ?Function,
preventSwipeRight?: ?boolean,

/**
* Should bounce the row on mount
*/
shouldBounceOnMount?: ?boolean,

/**
* A ReactElement that is unveiled when the user swipes
*/
slideoutView?: ?React.Node,
swipeThreshold?: ?number,

/**
* The minimum swipe distance required before fully animating the swipe. If
* the user swipes less than this distance, the item will return to its
* previous (open/close) position.
*/
swipeThreshold: number,
|}>;

type State = {|
currentLeft: Animated.Value,
isSwipeableViewRendered: boolean,
rowHeight: ?number,
|};

/**
* Creates a swipable row that allows taps on the main item and a custom View
* on the item hidden behind the row. Typically this should be used in
* conjunction with SwipeableListView for additional functionality, but can be
* used in a normal ListView. See the renderRow for SwipeableListView to see how
* to use this component separately.
*/
const SwipeableRow = createReactClass({
displayName: 'SwipeableRow',
_panResponder: {},
_previousLeft: CLOSED_LEFT_POSITION,

mixins: [TimerMixin],

propTypes: {
children: PropTypes.any,
isOpen: PropTypes.bool,
preventSwipeRight: PropTypes.bool,
maxSwipeDistance: PropTypes.number.isRequired,
onOpen: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
onSwipeEnd: PropTypes.func.isRequired,
onSwipeStart: PropTypes.func.isRequired,
// Should bounce the row on mount
shouldBounceOnMount: PropTypes.bool,
/**
* A ReactElement that is unveiled when the user swipes
*/
slideoutView: PropTypes.node.isRequired,
class SwipeableRow extends React.Component<Props, State> {
_timerID: ?TimeoutID = null;

static defaultProps = {
isOpen: false,
preventSwipeRight: false,
maxSwipeDistance: 0,
onOpen: emptyFunction,
onClose: emptyFunction,
onSwipeEnd: emptyFunction,
onSwipeStart: emptyFunction,
swipeThreshold: 30,
};

state = {
currentLeft: new Animated.Value(this._previousLeft),
/**
* The minimum swipe distance required before fully animating the swipe. If
* the user swipes less than this distance, the item will return to its
* previous (open/close) position.
* In order to render component A beneath component B, A must be rendered
* before B. However, this will cause "flickering", aka we see A briefly
* then B. To counter this, _isSwipeableViewRendered flag is used to set
* component A to be transparent until component B is loaded.
*/
swipeThreshold: PropTypes.number.isRequired,
},

getInitialState(): Object {
return {
currentLeft: new Animated.Value(this._previousLeft),
/**
* In order to render component A beneath component B, A must be rendered
* before B. However, this will cause "flickering", aka we see A briefly
* then B. To counter this, _isSwipeableViewRendered flag is used to set
* component A to be transparent until component B is loaded.
*/
isSwipeableViewRendered: false,
rowHeight: (null: ?number),
};
},

getDefaultProps(): Object {
return {
isOpen: false,
preventSwipeRight: false,
maxSwipeDistance: 0,
onOpen: emptyFunction,
onClose: emptyFunction,
onSwipeEnd: emptyFunction,
onSwipeStart: emptyFunction,
swipeThreshold: 30,
};
},

UNSAFE_componentWillMount(): void {
this._panResponder = PanResponder.create({
onMoveShouldSetPanResponderCapture: this
._handleMoveShouldSetPanResponderCapture,
onPanResponderGrant: this._handlePanResponderGrant,
onPanResponderMove: this._handlePanResponderMove,
onPanResponderRelease: this._handlePanResponderEnd,
onPanResponderTerminationRequest: this._onPanResponderTerminationRequest,
onPanResponderTerminate: this._handlePanResponderEnd,
onShouldBlockNativeResponder: (event, gestureState) => false,
});
},
isSwipeableViewRendered: false,
rowHeight: null,
};

_panResponder = PanResponder.create({
onMoveShouldSetPanResponderCapture: this
._handleMoveShouldSetPanResponderCapture,
onPanResponderGrant: this._handlePanResponderGrant,
onPanResponderMove: this._handlePanResponderMove,
onPanResponderRelease: this._handlePanResponderEnd,
onPanResponderTerminationRequest: this._onPanResponderTerminationRequest,
onPanResponderTerminate: this._handlePanResponderEnd,
onShouldBlockNativeResponder: (event, gestureState) => false,
});

_previousLeft = CLOSED_LEFT_POSITION;

componentDidMount(): void {
if (this.props.shouldBounceOnMount) {
/**
* Do the on mount bounce after a delay because if we animate when other
* components are loading, the animation will be laggy
*/
this.setTimeout(() => {
this._timerID = setTimeout(() => {
this._animateBounceBack(ON_MOUNT_BOUNCE_DURATION);
}, ON_MOUNT_BOUNCE_DELAY);
}
},
}

componentWillUnmount(): void {
this._timerID && clearTimeout(this._timerID);
}

UNSAFE_componentWillReceiveProps(nextProps: Object): void {
UNSAFE_componentWillReceiveProps(nextProps: Props): void {
/**
* We do not need an "animateOpen(noCallback)" because this animation is
* handled internally by this component.
*/
if (this.props.isOpen && !nextProps.isOpen) {
this._animateToClosedPosition();
}
},
}

render(): React.Element<any> {
render(): React.Node {
// The view hidden behind the main view
let slideOutView;
if (this.state.isSwipeableViewRendered && this.state.rowHeight) {
Expand All @@ -200,61 +188,61 @@ const SwipeableRow = createReactClass({
{swipeableView}
</View>
);
},
}

close(): void {
this.props.onClose();
this.props.onClose && this.props.onClose();
Copy link
Member

Choose a reason for hiding this comment

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

onClose is required based on the propTypes, you shouldn't need this check. If you do, then the flow types for onClose aren't properly showing that it is required.

this._animateToClosedPosition();
},
}

_onSwipeableViewLayout(event: LayoutEvent): void {
this.setState({
isSwipeableViewRendered: true,
rowHeight: event.nativeEvent.layout.height,
});
},
}

_handleMoveShouldSetPanResponderCapture(
event: PressEvent,
gestureState: GestureState,
): boolean {
// Decides whether a swipe is responded to by this component or its child
return gestureState.dy < 10 && this._isValidSwipe(gestureState);
},
}

_handlePanResponderGrant(
event: PressEvent,
gestureState: GestureState,
): void {},
): void {}

_handlePanResponderMove(event: PressEvent, gestureState: GestureState): void {
if (this._isSwipingExcessivelyRightFromClosedPosition(gestureState)) {
return;
}

this.props.onSwipeStart();
this.props.onSwipeStart && this.props.onSwipeStart();
Copy link
Member

Choose a reason for hiding this comment

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

same with onSwipeStart

Copy link
Member

Choose a reason for hiding this comment

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

If you could check the other ones in this file too that would be great.


if (this._isSwipingRightFromClosed(gestureState)) {
this._swipeSlowSpeed(gestureState);
} else {
this._swipeFullSpeed(gestureState);
}
},
}

_isSwipingRightFromClosed(gestureState: GestureState): boolean {
const gestureStateDx = IS_RTL ? -gestureState.dx : gestureState.dx;
return this._previousLeft === CLOSED_LEFT_POSITION && gestureStateDx > 0;
},
}

_swipeFullSpeed(gestureState: GestureState): void {
this.state.currentLeft.setValue(this._previousLeft + gestureState.dx);
},
}

_swipeSlowSpeed(gestureState: GestureState): void {
this.state.currentLeft.setValue(
this._previousLeft + gestureState.dx / SLOW_SPEED_SWIPE_FACTOR,
);
},
}

_isSwipingExcessivelyRightFromClosedPosition(
gestureState: GestureState,
Expand All @@ -269,14 +257,14 @@ const SwipeableRow = createReactClass({
this._isSwipingRightFromClosed(gestureState) &&
gestureStateDx > RIGHT_SWIPE_THRESHOLD
);
},
}

_onPanResponderTerminationRequest(
event: PressEvent,
gestureState: GestureState,
): boolean {
return false;
},
}

_animateTo(
toValue: number,
Expand All @@ -291,14 +279,14 @@ const SwipeableRow = createReactClass({
this._previousLeft = toValue;
callback();
});
},
}

_animateToOpenPosition(): void {
const maxSwipeDistance = IS_RTL
? -this.props.maxSwipeDistance
: this.props.maxSwipeDistance;
this._animateTo(-maxSwipeDistance);
},
}

_animateToOpenPositionWith(speed: number, distMoved: number): void {
/**
Expand All @@ -320,15 +308,15 @@ const SwipeableRow = createReactClass({
? -this.props.maxSwipeDistance
: this.props.maxSwipeDistance;
this._animateTo(-maxSwipeDistance, duration);
},
}

_animateToClosedPosition(duration: number = SWIPE_DURATION): void {
this._animateTo(CLOSED_LEFT_POSITION, duration);
},
}

_animateToClosedPositionDuringBounce(): void {
this._animateToClosedPosition(RIGHT_SWIPE_BOUNCE_BACK_DURATION);
},
}

_animateBounceBack(duration: number): void {
/**
Expand All @@ -343,7 +331,7 @@ const SwipeableRow = createReactClass({
duration,
this._animateToClosedPositionDuringBounce,
);
},
}

// Ignore swipes due to user's finger moving slightly when tapping
_isValidSwipe(gestureState: GestureState): boolean {
Expand All @@ -356,7 +344,7 @@ const SwipeableRow = createReactClass({
}

return Math.abs(gestureState.dx) > HORIZONTAL_SWIPE_DISTANCE_THRESHOLD;
},
}

_shouldAnimateRemainder(gestureState: GestureState): boolean {
/**
Expand All @@ -367,21 +355,21 @@ const SwipeableRow = createReactClass({
Math.abs(gestureState.dx) > this.props.swipeThreshold ||
gestureState.vx > HORIZONTAL_FULL_SWIPE_SPEED_THRESHOLD
);
},
}

_handlePanResponderEnd(event: PressEvent, gestureState: GestureState): void {
const horizontalDistance = IS_RTL ? -gestureState.dx : gestureState.dx;
if (this._isSwipingRightFromClosed(gestureState)) {
this.props.onOpen();
this.props.onOpen && this.props.onOpen();
this._animateBounceBack(RIGHT_SWIPE_BOUNCE_BACK_DURATION);
} else if (this._shouldAnimateRemainder(gestureState)) {
if (horizontalDistance < 0) {
// Swiped left
this.props.onOpen();
this.props.onOpen && this.props.onOpen();
this._animateToOpenPositionWith(gestureState.vx, horizontalDistance);
} else {
// Swiped right
this.props.onClose();
this.props.onClose && this.props.onClose();
this._animateToClosedPosition();
}
} else {
Expand All @@ -392,13 +380,8 @@ const SwipeableRow = createReactClass({
}
}

this.props.onSwipeEnd();
},
});

// TODO: Delete this when `SwipeableRow` uses class syntax.
class TypedSwipeableRow extends React.Component<Props> {
close() {}
this.props.onSwipeEnd && this.props.onSwipeEnd();
}
}

const styles = StyleSheet.create({
Expand All @@ -411,4 +394,4 @@ const styles = StyleSheet.create({
},
});

module.exports = ((SwipeableRow: any): Class<TypedSwipeableRow>);
module.exports = SwipeableRow;