diff --git a/README.md b/README.md
index 11a7a0d..04011d8 100644
--- a/README.md
+++ b/README.md
@@ -53,6 +53,7 @@ import '@sandstreamdev/react-swipeable-list/dist/styles.css';
content:
react-swipeable-list example
(try also mobile view in dev tools for touch events)
Simple example (with default 0.5 action trigger threshold)
-
{triggeredSimpleItemAction}
+
+ Triggered action: {triggeredSimpleItemAction}
+
+
+ Callback swipe action: {swipeAction}
+
+
+ Callback swipe progress:{' '}
+ {swipeProgress !== undefined ? swipeProgress : '-'}%
+
{itemContentSimple('Item with swipe right')}
{itemContentSimple('Item with swipe left')}
{itemContentSimple('Item with both swipes')}
diff --git a/examples/src/app.module.css b/examples/src/app.module.css
index 4d5a5cf..17bdcbc 100644
--- a/examples/src/app.module.css
+++ b/examples/src/app.module.css
@@ -3,7 +3,7 @@ html {
}
h3 {
- margin-bottom: 0;
+ margin-bottom: 8px;
text-align: center;
}
@@ -95,8 +95,8 @@ footer a:hover {
}
.actionInfo {
- padding: 16px;
- font-weight: 600;
+ padding: 0 8px 4px 8px;
+ font-weight: 400;
font-size: 14px;
}
diff --git a/src/SwipeableListItem.js b/src/SwipeableListItem.js
index 67c1a26..9efa472 100644
--- a/src/SwipeableListItem.js
+++ b/src/SwipeableListItem.js
@@ -25,10 +25,13 @@ 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();
}
@@ -36,6 +39,7 @@ class SwipeableListItem extends PureComponent {
this.dragStartPoint = { x: -1, y: -1 };
this.dragDirection = DragDirection.UNKNOWN;
this.left = 0;
+ this.previousSwipeDistancePercent = 0;
};
get dragHorizontalDirectionThreshold() {
@@ -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);
@@ -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 => {
@@ -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();
}
}
};
@@ -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();
}
}
};
@@ -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();
};
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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();
@@ -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;
diff --git a/src/__tests__/SwipeableListItem.test.js b/src/__tests__/SwipeableListItem.test.js
index 9d4d9d9..c4b23c0 100644
--- a/src/__tests__/SwipeableListItem.test.js
+++ b/src/__tests__/SwipeableListItem.test.js
@@ -242,3 +242,88 @@ test('swipe actions triggering if block swipe prop is set', () => {
expect(callbackLeft).toHaveBeenCalledTimes(0);
expect(callbackRight).toHaveBeenCalledTimes(0);
});
+
+test('start and end callbacks not triggered if swipe content not defined', () => {
+ const callbackSwipeStart = jest.fn();
+ const callbackSwipeEnd = jest.fn();
+
+ const { getByTestId } = render(
+
+ Item content
+
+ );
+
+ const contentContainer = getByTestId('content');
+ swipeLeftMouse(contentContainer);
+ swipeLeftTouch(contentContainer);
+ swipeRightMouse(contentContainer);
+ swipeRightTouch(contentContainer);
+
+ expect(callbackSwipeStart).toHaveBeenCalledTimes(0);
+ expect(callbackSwipeEnd).toHaveBeenCalledTimes(0);
+});
+
+test('start and end callbacks not triggered if blockSwipe is set', () => {
+ const callbackSwipeStart = jest.fn();
+ const callbackSwipeEnd = jest.fn();
+ const callbackLeft = jest.fn();
+
+ const { getByTestId } = render(
+ Left swipe content,
+ action: callbackLeft
+ }}
+ onSwipeStart={callbackSwipeStart}
+ onSwipeEnd={callbackSwipeEnd}
+ >
+ Item content
+
+ );
+
+ const contentContainer = getByTestId('content');
+ swipeLeftMouse(contentContainer);
+ swipeLeftTouch(contentContainer);
+ swipeRightMouse(contentContainer);
+ swipeRightTouch(contentContainer);
+
+ expect(callbackSwipeStart).toHaveBeenCalledTimes(0);
+ expect(callbackSwipeEnd).toHaveBeenCalledTimes(0);
+});
+
+test('start and end callbacks triggered if swipe content is defined', () => {
+ const callbackSwipeStart = jest.fn();
+ const callbackSwipeEnd = jest.fn();
+ const callbackLeft = jest.fn();
+ const callbackRight = jest.fn();
+
+ const { getByTestId } = render(
+ Left swipe content,
+ action: callbackLeft
+ }}
+ swipeRight={{
+ content: Right swipe content,
+ action: callbackRight
+ }}
+ onSwipeStart={callbackSwipeStart}
+ onSwipeEnd={callbackSwipeEnd}
+ >
+ Item content
+
+ );
+
+ const contentContainer = getByTestId('content');
+ swipeLeftMouse(contentContainer);
+ swipeLeftTouch(contentContainer);
+ swipeRightMouse(contentContainer);
+ swipeRightTouch(contentContainer);
+
+ expect(callbackSwipeStart).toHaveBeenCalledTimes(4);
+ expect(callbackSwipeEnd).toHaveBeenCalledTimes(4);
+});
diff --git a/src/module.d.ts b/src/module.d.ts
index e0284c5..18b57c9 100644
--- a/src/module.d.ts
+++ b/src/module.d.ts
@@ -20,6 +20,9 @@ interface ISwipeableListItemProps {
scrollStartThreshold?: number;
swipeStartThreshold?: number;
threshold?: number;
+ onSwipeStart?: () => void;
+ onSwipeEnd?: () => void;
+ onSwipeProgress?: (progress: number) => void;
}
export class SwipeableListItem extends React.Component<