Skip to content
Merged
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
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
34 changes: 32 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,27 +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}>
<h3>react-swipeable-list example</h3>
<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}</span>
<span className={styles.actionInfo}>
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
Expand Up @@ -3,7 +3,7 @@ html {
}

h3 {
margin-bottom: 0;
margin-bottom: 8px;
text-align: center;
}

Expand Down Expand Up @@ -95,8 +95,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