Skip to content

Commit

Permalink
Merge branch 'closes_issue_77'
Browse files Browse the repository at this point in the history
  • Loading branch information
Marek Rozmus committed Jan 30, 2020
2 parents 7f77cbc + 57d7eda commit 3a28164
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 50 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import '@sandstreamdev/react-swipeable-list/dist/styles.css';
content: <div>Revealed content during swipe</div>,
action: () => console.info('swipe action triggered')
}}
onSwipeProgress={progress => console.info(`Swipe progress: ${progress}%`)}
>
<div>Item name</div>
</SwipeableListItem>
Expand Down Expand Up @@ -132,6 +133,24 @@ Type: `number` (default: `0.5`)

It can be set for the whole list or for every item. See `threshold` for `SwipeableList`. Value from the `SwipeableListItem` takes precedence.

### onSwipeStart

Type: `() => void`

Fired after swipe has started (after drag gesture passes the `swipeStartThreshold` distance in pixels).

### onSwipeEnd

Type: `() => void`

Fired after swipe has ended.

### onSwipeProgress

Type: `(progress: number) => void`

Fired every time swipe progress changes. The reported `progress` value is always an integer in range 0 to 100 inclusive.

## Contributors ✨

Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
Expand Down
32 changes: 30 additions & 2 deletions examples/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import { MailIcon, ReplyIcon, DeleteIcon } from '../images/icons';
import styles from './app.module.css';

function App() {
const [triggeredSimpleItemAction, triggerSimpleItemAction] = useState('');
const [triggeredSimpleItemAction, triggerSimpleItemAction] = useState('None');
const [triggeredComplexItemAction, triggerComplexItemAction] = useState('');
const [swipeProgress, handleSwipeProgress] = useState();
const [swipeAction, handleSwipeAction] = useState('None');

const swipeRightDataSimple = name => ({
content: (
Expand Down Expand Up @@ -64,29 +66,55 @@ function App() {
triggerComplexItemAction(`Reply action triggered on "${name}" item`)
});

const handleSwipeStart = () => {
triggerSimpleItemAction('None');
handleSwipeAction('Swipe started');
};

const handleSwipeEnd = () => {
handleSwipeAction('Swipe ended');
handleSwipeProgress();
};

return (
<div className={styles.example}>
<h1>react-swipeable-list example</h1>
<h5>(try also mobile view in dev tools for touch events)</h5>
<h3>Simple example (with default 0.5 action trigger threshold)</h3>
<span className={styles.actionInfo}>
{triggeredSimpleItemAction || 'No action triggered yet'}
Triggered action: {triggeredSimpleItemAction}
</span>
<span className={styles.actionInfo}>
Callback swipe action: {swipeAction}
</span>
<span className={styles.actionInfo}>
Callback swipe progress:{' '}
{swipeProgress !== undefined ? swipeProgress : '-'}%
</span>
<div className={styles.listContainer}>
<SwipeableList>
<SwipeableListItem
swipeRight={swipeRightDataSimple('Item with swipe right')}
onSwipeStart={handleSwipeStart}
onSwipeEnd={handleSwipeEnd}
onSwipeProgress={handleSwipeProgress}
>
{itemContentSimple('Item with swipe right')}
</SwipeableListItem>
<SwipeableListItem
swipeLeft={swipeLeftDataSimple('Item with swipe left')}
onSwipeStart={handleSwipeStart}
onSwipeEnd={handleSwipeEnd}
onSwipeProgress={handleSwipeProgress}
>
{itemContentSimple('Item with swipe left')}
</SwipeableListItem>
<SwipeableListItem
swipeRight={swipeRightDataSimple('Item with both swipes')}
swipeLeft={swipeLeftDataSimple('Item with both swipes')}
onSwipeStart={handleSwipeStart}
onSwipeEnd={handleSwipeEnd}
onSwipeProgress={handleSwipeProgress}
>
{itemContentSimple('Item with both swipes')}
</SwipeableListItem>
Expand Down
6 changes: 3 additions & 3 deletions examples/src/app.module.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
h3 {
margin-bottom: 0;
margin-bottom: 8px;
text-align: center;
}

Expand Down Expand Up @@ -104,8 +104,8 @@ footer a:hover {
}

.actionInfo {
padding: 16px;
font-weight: 600;
padding: 0 8px 4px 8px;
font-weight: 400;
font-size: 14px;
}

Expand Down
132 changes: 87 additions & 45 deletions src/SwipeableListItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,21 @@ class SwipeableListItem extends PureComponent {
this.contentLeft = null;
this.contentRight = null;
this.listElement = null;
this.requestedAnimationFrame = null;
this.wrapper = null;

this.startTime = null;

this.previousSwipeDistancePercent = 0;

this.resetState();
}

resetState = () => {
this.dragStartPoint = { x: -1, y: -1 };
this.dragDirection = DragDirection.UNKNOWN;
this.left = 0;
this.previousSwipeDistancePercent = 0;
};

get dragHorizontalDirectionThreshold() {
Expand All @@ -58,6 +62,12 @@ class SwipeableListItem extends PureComponent {
}

componentWillUnmount() {
if (this.requestedAnimationFrame) {
cancelAnimationFrame(this.requestedAnimationFrame);

this.requestedAnimationFrame = null;
}

this.wrapper.removeEventListener('mousedown', this.handleDragStartMouse);

this.wrapper.removeEventListener('touchstart', this.handleDragStartTouch);
Expand Down Expand Up @@ -90,16 +100,16 @@ class SwipeableListItem extends PureComponent {
this.dragStartPoint = { x: clientX, y: clientY };

this.listElement.className = styles.content;
if (this.contentLeft) {
if (this.contentLeft !== null) {
this.contentLeft.className = styles.contentLeft;
}

if (this.contentRight) {
if (this.contentRight !== null) {
this.contentRight.className = styles.contentRight;
}

this.startTime = Date.now();
requestAnimationFrame(this.updatePosition);
this.scheduleUpdatePosition();
};

handleMouseMove = event => {
Expand All @@ -112,11 +122,8 @@ class SwipeableListItem extends PureComponent {
event.stopPropagation();
event.preventDefault();

const delta = clientX - this.dragStartPoint.x;

if (this.shouldMoveItem(delta)) {
this.left = delta;
}
this.left = clientX - this.dragStartPoint.x;
this.scheduleUpdatePosition();
}
}
};
Expand All @@ -135,11 +142,8 @@ class SwipeableListItem extends PureComponent {
event.stopPropagation();
event.preventDefault();

const delta = clientX - this.dragStartPoint.x;

if (this.shouldMoveItem(delta)) {
this.left = delta;
}
this.left = clientX - this.dragStartPoint.x;
this.scheduleUpdatePosition();
}
}
};
Expand All @@ -148,8 +152,10 @@ class SwipeableListItem extends PureComponent {
window.removeEventListener('mouseup', this.handleDragEndMouse);
window.removeEventListener('mousemove', this.handleMouseMove);

this.wrapper.removeEventListener('mouseup', this.handleDragEndMouse);
this.wrapper.removeEventListener('mousemove', this.handleMouseMove);
if (this.wrapper) {
this.wrapper.removeEventListener('mouseup', this.handleDragEndMouse);
this.wrapper.removeEventListener('mousemove', this.handleMouseMove);
}

this.handleDragEnd();
};
Expand All @@ -169,39 +175,31 @@ class SwipeableListItem extends PureComponent {
} else if (this.left > this.listElement.offsetWidth * threshold) {
this.handleSwipedRight();
}

if (this.props.onSwipeEnd) {
this.props.onSwipeEnd();
}
}

this.resetState();
this.listElement.className = styles.contentReturn;
this.listElement.style.transform = `translateX(${this.left}px)`;

if (this.listElement) {
this.listElement.className = styles.contentReturn;
this.listElement.style.transform = `translateX(${this.left}px)`;
}

// hide backgrounds
if (this.contentLeft) {
if (this.contentLeft !== null) {
this.contentLeft.style.opacity = 0;
this.contentLeft.className = styles.contentLeftReturn;
}

if (this.contentRight) {
if (this.contentRight !== null) {
this.contentRight.style.opacity = 0;
this.contentRight.className = styles.contentRightReturn;
}
};

shouldMoveItem = delta => {
const {
swipeLeft: { content: contentLeft } = {},
swipeRight: { content: contentRight } = {},
blockSwipe
} = this.props;
const swipingLeft = delta < 0;
const swipingRight = delta > 0;

return (
!blockSwipe &&
((swipingLeft && contentLeft) || (swipingRight && contentRight))
);
};

dragStartedWithinItem = () => {
const { x, y } = this.dragStartPoint;

Expand All @@ -226,7 +224,10 @@ class SwipeableListItem extends PureComponent {

switch (octant) {
case 0:
if (horizontalDistance > this.dragHorizontalDirectionThreshold) {
if (
this.contentRight !== null &&
horizontalDistance > this.dragHorizontalDirectionThreshold
) {
this.dragDirection = DragDirection.RIGHT;
}
break;
Expand All @@ -238,7 +239,10 @@ class SwipeableListItem extends PureComponent {
}
break;
case 4:
if (horizontalDistance > this.dragHorizontalDirectionThreshold) {
if (
this.contentLeft !== null &&
horizontalDistance > this.dragHorizontalDirectionThreshold
) {
this.dragDirection = DragDirection.LEFT;
}
break;
Expand All @@ -250,21 +254,35 @@ class SwipeableListItem extends PureComponent {
}
break;
}

if (this.props.onSwipeStart && this.isSwiping()) {
this.props.onSwipeStart();
}
}
};

isSwiping = () =>
this.dragStartedWithinItem() &&
(this.dragDirection === DragDirection.LEFT ||
this.dragDirection === DragDirection.RIGHT);

updatePosition = () => {
isSwiping = () => {
const { blockSwipe } = this.props;
const horizontalDrag =
this.dragDirection === DragDirection.LEFT ||
this.dragDirection === DragDirection.RIGHT;

return !blockSwipe && this.dragStartedWithinItem() && horizontalDrag;
};

if (this.dragStartedWithinItem() && !blockSwipe) {
requestAnimationFrame(this.updatePosition);
scheduleUpdatePosition = () => {
if (this.requestedAnimationFrame) {
return;
}

this.requestedAnimationFrame = requestAnimationFrame(() => {
this.requestedAnimationFrame = null;

this.updatePosition();
});
};

updatePosition = () => {
const now = Date.now();
const elapsed = now - this.startTime;

Expand All @@ -279,6 +297,26 @@ class SwipeableListItem extends PureComponent {

const opacity = (Math.abs(this.left) / 100).toFixed(2);

if (this.props.onSwipeProgress && this.listElement !== null) {
const listElementWidth = this.listElement.offsetWidth;
let swipeDistancePercent = this.previousSwipeDistancePercent;

if (listElementWidth !== 0) {
const swipeDistance = Math.max(
0,
listElementWidth - Math.abs(this.left)
);

swipeDistancePercent =
100 - Math.round((100 * swipeDistance) / listElementWidth);
}

if (this.previousSwipeDistancePercent !== swipeDistancePercent) {
this.props.onSwipeProgress(swipeDistancePercent);
this.previousSwipeDistancePercent = swipeDistancePercent;
}
}

if (opacity < 1 && opacity.toString() !== contentToShow.style.opacity) {
contentToShow.style.opacity = opacity.toString();

Expand Down Expand Up @@ -361,7 +399,11 @@ SwipeableListItem.propTypes = {
swipeRight: SwipeActionPropType,
scrollStartThreshold: PropTypes.number,
swipeStartThreshold: PropTypes.number,
threshold: PropTypes.number
threshold: PropTypes.number,

onSwipeEnd: PropTypes.func,
onSwipeProgress: PropTypes.func,
onSwipeStart: PropTypes.func
};

export default SwipeableListItem;
Loading

0 comments on commit 3a28164

Please sign in to comment.