Skip to content

Commit 3a28164

Browse files
author
Marek Rozmus
committed
Merge branch 'closes_issue_77'
2 parents 7f77cbc + 57d7eda commit 3a28164

File tree

6 files changed

+227
-50
lines changed

6 files changed

+227
-50
lines changed

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import '@sandstreamdev/react-swipeable-list/dist/styles.css';
5353
content: <div>Revealed content during swipe</div>,
5454
action: () => console.info('swipe action triggered')
5555
}}
56+
onSwipeProgress={progress => console.info(`Swipe progress: ${progress}%`)}
5657
>
5758
<div>Item name</div>
5859
</SwipeableListItem>
@@ -132,6 +133,24 @@ Type: `number` (default: `0.5`)
132133

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

136+
### onSwipeStart
137+
138+
Type: `() => void`
139+
140+
Fired after swipe has started (after drag gesture passes the `swipeStartThreshold` distance in pixels).
141+
142+
### onSwipeEnd
143+
144+
Type: `() => void`
145+
146+
Fired after swipe has ended.
147+
148+
### onSwipeProgress
149+
150+
Type: `(progress: number) => void`
151+
152+
Fired every time swipe progress changes. The reported `progress` value is always an integer in range 0 to 100 inclusive.
153+
135154
## Contributors ✨
136155

137156
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):

examples/src/App.js

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import { MailIcon, ReplyIcon, DeleteIcon } from '../images/icons';
1111
import styles from './app.module.css';
1212

1313
function App() {
14-
const [triggeredSimpleItemAction, triggerSimpleItemAction] = useState('');
14+
const [triggeredSimpleItemAction, triggerSimpleItemAction] = useState('None');
1515
const [triggeredComplexItemAction, triggerComplexItemAction] = useState('');
16+
const [swipeProgress, handleSwipeProgress] = useState();
17+
const [swipeAction, handleSwipeAction] = useState('None');
1618

1719
const swipeRightDataSimple = name => ({
1820
content: (
@@ -64,29 +66,55 @@ function App() {
6466
triggerComplexItemAction(`Reply action triggered on "${name}" item`)
6567
});
6668

69+
const handleSwipeStart = () => {
70+
triggerSimpleItemAction('None');
71+
handleSwipeAction('Swipe started');
72+
};
73+
74+
const handleSwipeEnd = () => {
75+
handleSwipeAction('Swipe ended');
76+
handleSwipeProgress();
77+
};
78+
6779
return (
6880
<div className={styles.example}>
6981
<h1>react-swipeable-list example</h1>
7082
<h5>(try also mobile view in dev tools for touch events)</h5>
7183
<h3>Simple example (with default 0.5 action trigger threshold)</h3>
7284
<span className={styles.actionInfo}>
73-
{triggeredSimpleItemAction || 'No action triggered yet'}
85+
Triggered action: {triggeredSimpleItemAction}
86+
</span>
87+
<span className={styles.actionInfo}>
88+
Callback swipe action: {swipeAction}
89+
</span>
90+
<span className={styles.actionInfo}>
91+
Callback swipe progress:{' '}
92+
{swipeProgress !== undefined ? swipeProgress : '-'}%
7493
</span>
7594
<div className={styles.listContainer}>
7695
<SwipeableList>
7796
<SwipeableListItem
7897
swipeRight={swipeRightDataSimple('Item with swipe right')}
98+
onSwipeStart={handleSwipeStart}
99+
onSwipeEnd={handleSwipeEnd}
100+
onSwipeProgress={handleSwipeProgress}
79101
>
80102
{itemContentSimple('Item with swipe right')}
81103
</SwipeableListItem>
82104
<SwipeableListItem
83105
swipeLeft={swipeLeftDataSimple('Item with swipe left')}
106+
onSwipeStart={handleSwipeStart}
107+
onSwipeEnd={handleSwipeEnd}
108+
onSwipeProgress={handleSwipeProgress}
84109
>
85110
{itemContentSimple('Item with swipe left')}
86111
</SwipeableListItem>
87112
<SwipeableListItem
88113
swipeRight={swipeRightDataSimple('Item with both swipes')}
89114
swipeLeft={swipeLeftDataSimple('Item with both swipes')}
115+
onSwipeStart={handleSwipeStart}
116+
onSwipeEnd={handleSwipeEnd}
117+
onSwipeProgress={handleSwipeProgress}
90118
>
91119
{itemContentSimple('Item with both swipes')}
92120
</SwipeableListItem>

examples/src/app.module.css

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
h3 {
2-
margin-bottom: 0;
2+
margin-bottom: 8px;
33
text-align: center;
44
}
55

@@ -104,8 +104,8 @@ footer a:hover {
104104
}
105105

106106
.actionInfo {
107-
padding: 16px;
108-
font-weight: 600;
107+
padding: 0 8px 4px 8px;
108+
font-weight: 400;
109109
font-size: 14px;
110110
}
111111

src/SwipeableListItem.js

Lines changed: 87 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,21 @@ class SwipeableListItem extends PureComponent {
2525
this.contentLeft = null;
2626
this.contentRight = null;
2727
this.listElement = null;
28+
this.requestedAnimationFrame = null;
2829
this.wrapper = null;
2930

3031
this.startTime = null;
3132

33+
this.previousSwipeDistancePercent = 0;
34+
3235
this.resetState();
3336
}
3437

3538
resetState = () => {
3639
this.dragStartPoint = { x: -1, y: -1 };
3740
this.dragDirection = DragDirection.UNKNOWN;
3841
this.left = 0;
42+
this.previousSwipeDistancePercent = 0;
3943
};
4044

4145
get dragHorizontalDirectionThreshold() {
@@ -58,6 +62,12 @@ class SwipeableListItem extends PureComponent {
5862
}
5963

6064
componentWillUnmount() {
65+
if (this.requestedAnimationFrame) {
66+
cancelAnimationFrame(this.requestedAnimationFrame);
67+
68+
this.requestedAnimationFrame = null;
69+
}
70+
6171
this.wrapper.removeEventListener('mousedown', this.handleDragStartMouse);
6272

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

92102
this.listElement.className = styles.content;
93-
if (this.contentLeft) {
103+
if (this.contentLeft !== null) {
94104
this.contentLeft.className = styles.contentLeft;
95105
}
96106

97-
if (this.contentRight) {
107+
if (this.contentRight !== null) {
98108
this.contentRight.className = styles.contentRight;
99109
}
100110

101111
this.startTime = Date.now();
102-
requestAnimationFrame(this.updatePosition);
112+
this.scheduleUpdatePosition();
103113
};
104114

105115
handleMouseMove = event => {
@@ -112,11 +122,8 @@ class SwipeableListItem extends PureComponent {
112122
event.stopPropagation();
113123
event.preventDefault();
114124

115-
const delta = clientX - this.dragStartPoint.x;
116-
117-
if (this.shouldMoveItem(delta)) {
118-
this.left = delta;
119-
}
125+
this.left = clientX - this.dragStartPoint.x;
126+
this.scheduleUpdatePosition();
120127
}
121128
}
122129
};
@@ -135,11 +142,8 @@ class SwipeableListItem extends PureComponent {
135142
event.stopPropagation();
136143
event.preventDefault();
137144

138-
const delta = clientX - this.dragStartPoint.x;
139-
140-
if (this.shouldMoveItem(delta)) {
141-
this.left = delta;
142-
}
145+
this.left = clientX - this.dragStartPoint.x;
146+
this.scheduleUpdatePosition();
143147
}
144148
}
145149
};
@@ -148,8 +152,10 @@ class SwipeableListItem extends PureComponent {
148152
window.removeEventListener('mouseup', this.handleDragEndMouse);
149153
window.removeEventListener('mousemove', this.handleMouseMove);
150154

151-
this.wrapper.removeEventListener('mouseup', this.handleDragEndMouse);
152-
this.wrapper.removeEventListener('mousemove', this.handleMouseMove);
155+
if (this.wrapper) {
156+
this.wrapper.removeEventListener('mouseup', this.handleDragEndMouse);
157+
this.wrapper.removeEventListener('mousemove', this.handleMouseMove);
158+
}
153159

154160
this.handleDragEnd();
155161
};
@@ -169,39 +175,31 @@ class SwipeableListItem extends PureComponent {
169175
} else if (this.left > this.listElement.offsetWidth * threshold) {
170176
this.handleSwipedRight();
171177
}
178+
179+
if (this.props.onSwipeEnd) {
180+
this.props.onSwipeEnd();
181+
}
172182
}
173183

174184
this.resetState();
175-
this.listElement.className = styles.contentReturn;
176-
this.listElement.style.transform = `translateX(${this.left}px)`;
185+
186+
if (this.listElement) {
187+
this.listElement.className = styles.contentReturn;
188+
this.listElement.style.transform = `translateX(${this.left}px)`;
189+
}
177190

178191
// hide backgrounds
179-
if (this.contentLeft) {
192+
if (this.contentLeft !== null) {
180193
this.contentLeft.style.opacity = 0;
181194
this.contentLeft.className = styles.contentLeftReturn;
182195
}
183196

184-
if (this.contentRight) {
197+
if (this.contentRight !== null) {
185198
this.contentRight.style.opacity = 0;
186199
this.contentRight.className = styles.contentRightReturn;
187200
}
188201
};
189202

190-
shouldMoveItem = delta => {
191-
const {
192-
swipeLeft: { content: contentLeft } = {},
193-
swipeRight: { content: contentRight } = {},
194-
blockSwipe
195-
} = this.props;
196-
const swipingLeft = delta < 0;
197-
const swipingRight = delta > 0;
198-
199-
return (
200-
!blockSwipe &&
201-
((swipingLeft && contentLeft) || (swipingRight && contentRight))
202-
);
203-
};
204-
205203
dragStartedWithinItem = () => {
206204
const { x, y } = this.dragStartPoint;
207205

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

227225
switch (octant) {
228226
case 0:
229-
if (horizontalDistance > this.dragHorizontalDirectionThreshold) {
227+
if (
228+
this.contentRight !== null &&
229+
horizontalDistance > this.dragHorizontalDirectionThreshold
230+
) {
230231
this.dragDirection = DragDirection.RIGHT;
231232
}
232233
break;
@@ -238,7 +239,10 @@ class SwipeableListItem extends PureComponent {
238239
}
239240
break;
240241
case 4:
241-
if (horizontalDistance > this.dragHorizontalDirectionThreshold) {
242+
if (
243+
this.contentLeft !== null &&
244+
horizontalDistance > this.dragHorizontalDirectionThreshold
245+
) {
242246
this.dragDirection = DragDirection.LEFT;
243247
}
244248
break;
@@ -250,21 +254,35 @@ class SwipeableListItem extends PureComponent {
250254
}
251255
break;
252256
}
257+
258+
if (this.props.onSwipeStart && this.isSwiping()) {
259+
this.props.onSwipeStart();
260+
}
253261
}
254262
};
255263

256-
isSwiping = () =>
257-
this.dragStartedWithinItem() &&
258-
(this.dragDirection === DragDirection.LEFT ||
259-
this.dragDirection === DragDirection.RIGHT);
260-
261-
updatePosition = () => {
264+
isSwiping = () => {
262265
const { blockSwipe } = this.props;
266+
const horizontalDrag =
267+
this.dragDirection === DragDirection.LEFT ||
268+
this.dragDirection === DragDirection.RIGHT;
269+
270+
return !blockSwipe && this.dragStartedWithinItem() && horizontalDrag;
271+
};
263272

264-
if (this.dragStartedWithinItem() && !blockSwipe) {
265-
requestAnimationFrame(this.updatePosition);
273+
scheduleUpdatePosition = () => {
274+
if (this.requestedAnimationFrame) {
275+
return;
266276
}
267277

278+
this.requestedAnimationFrame = requestAnimationFrame(() => {
279+
this.requestedAnimationFrame = null;
280+
281+
this.updatePosition();
282+
});
283+
};
284+
285+
updatePosition = () => {
268286
const now = Date.now();
269287
const elapsed = now - this.startTime;
270288

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

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

300+
if (this.props.onSwipeProgress && this.listElement !== null) {
301+
const listElementWidth = this.listElement.offsetWidth;
302+
let swipeDistancePercent = this.previousSwipeDistancePercent;
303+
304+
if (listElementWidth !== 0) {
305+
const swipeDistance = Math.max(
306+
0,
307+
listElementWidth - Math.abs(this.left)
308+
);
309+
310+
swipeDistancePercent =
311+
100 - Math.round((100 * swipeDistance) / listElementWidth);
312+
}
313+
314+
if (this.previousSwipeDistancePercent !== swipeDistancePercent) {
315+
this.props.onSwipeProgress(swipeDistancePercent);
316+
this.previousSwipeDistancePercent = swipeDistancePercent;
317+
}
318+
}
319+
282320
if (opacity < 1 && opacity.toString() !== contentToShow.style.opacity) {
283321
contentToShow.style.opacity = opacity.toString();
284322

@@ -361,7 +399,11 @@ SwipeableListItem.propTypes = {
361399
swipeRight: SwipeActionPropType,
362400
scrollStartThreshold: PropTypes.number,
363401
swipeStartThreshold: PropTypes.number,
364-
threshold: PropTypes.number
402+
threshold: PropTypes.number,
403+
404+
onSwipeEnd: PropTypes.func,
405+
onSwipeProgress: PropTypes.func,
406+
onSwipeStart: PropTypes.func
365407
};
366408

367409
export default SwipeableListItem;

0 commit comments

Comments
 (0)