@chayns-components/swipeable-wrapper
Version:
A set of beautiful React components for developing your own applications with chayns.
190 lines (185 loc) • 7.01 kB
JavaScript
import { vibrate } from 'chayns-api';
import { animate, useMotionValue } from 'motion/react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { calcThreshold } from '../../utils/threshold';
import SwipeableAction, { SWIPEABLE_ACTION_WIDTH } from './swipeable-action/SwipeableAction';
import { StyledMotionSwipeableWrapper, StyledSwipeableWrapperContent } from './SwipeableWrapper.styles';
const SwipeableWrapper = ({
children,
leftActions = [],
rightActions = [],
shouldUseOpacityAnimation,
isDisabled = false,
onSwipeEnd,
onSwipeStart
}) => {
const [leftThreshold, setLeftThreshold] = useState(calcThreshold({
actionCount: leftActions.length,
direction: 'left',
width: window.innerWidth
}));
const [rightThreshold, setRightThreshold] = useState(calcThreshold({
actionCount: rightActions.length,
direction: 'right',
width: window.innerWidth
}));
const swipeableWrapperRef = useRef(null);
const isSwipingRef = useRef(false);
const listItemXOffset = useMotionValue(0);
const close = useCallback(() => {
void animate(listItemXOffset, 0);
}, [listItemXOffset]);
const open = useCallback(direction => {
switch (direction) {
case 'left':
void animate(listItemXOffset, SWIPEABLE_ACTION_WIDTH * leftActions.length);
break;
case 'right':
void animate(listItemXOffset, -SWIPEABLE_ACTION_WIDTH * rightActions.length);
break;
default:
break;
}
}, [leftActions.length, listItemXOffset, rightActions.length]);
useEffect(() => {
const width = swipeableWrapperRef.current?.parentElement?.offsetWidth;
// This check was deliberately chosen because a width of 0 is also not permitted.
if (width) {
setLeftThreshold(calcThreshold({
actionCount: leftActions.length,
direction: 'left',
width
}));
setRightThreshold(calcThreshold({
actionCount: rightActions.length,
direction: 'right',
width
}));
}
}, [leftActions.length, rightActions.length]);
// Close an opened menu when anything outside it is tapped
useEffect(() => {
function closeCallback(event) {
const eventTarget = event.target;
// @ts-expect-error: Pretty sure that the event target is always a Node.
if (eventTarget && !swipeableWrapperRef.current?.contains(eventTarget)) {
close();
}
}
document.addEventListener('mousedown', closeCallback);
document.addEventListener('touchstart', closeCallback);
return () => {
document.removeEventListener('mousedown', closeCallback);
document.removeEventListener('touchstart', closeCallback);
};
}, [close]);
// Vibrate when the threshold is passed
useEffect(() => listItemXOffset.on('change', newValue => {
const previous = listItemXOffset.getPrevious();
if (!previous) {
return;
}
const hasCrossedLeftThreshold = previous < leftThreshold && newValue >= leftThreshold || previous > leftThreshold && newValue <= leftThreshold;
const hasCrossedRightThreshold = previous < rightThreshold && newValue >= rightThreshold || previous > rightThreshold && newValue <= rightThreshold;
if (hasCrossedLeftThreshold || hasCrossedRightThreshold) {
void vibrate({
iOSFeedbackVibration: 6,
pattern: [150]
});
}
}), [leftThreshold, listItemXOffset, rightThreshold]);
const handlePan = useCallback((_, info) => {
if (!isSwipingRef.current) {
isSwipingRef.current = true;
if (typeof onSwipeStart === 'function') {
onSwipeStart();
}
}
const currentXOffset = listItemXOffset.get();
const dampingFactor = info.offset.x > 0 && leftActions.length > 0 || info.offset.x < 0 && rightActions.length > 0 || currentXOffset > 0 && info.delta.x < 0 || currentXOffset < 0 && info.delta.x > 0 ? 1 : 0.75 / (Math.abs(info.offset.x) / 9);
if (Math.abs(info.offset.x) > 30 || currentXOffset > 0) {
listItemXOffset.set(currentXOffset + info.delta.x * dampingFactor);
}
}, [leftActions.length, listItemXOffset, onSwipeStart, rightActions.length]);
const handlePanEnd = useCallback(() => {
if (typeof onSwipeEnd === 'function') {
onSwipeEnd();
}
isSwipingRef.current = false;
const offset = listItemXOffset.get();
if (offset > leftThreshold) {
leftActions[0]?.action();
close();
} else if (offset < rightThreshold) {
rightActions[rightActions.length - 1]?.action();
close();
} else {
let state;
if (offset > 2) {
state = 'left-open';
} else if (offset < -2) {
state = 'right-open';
} else {
state = 'closed';
}
// eslint-disable-next-line default-case
switch (state) {
case 'left-open':
if (offset < SWIPEABLE_ACTION_WIDTH) {
close();
} else {
open('left');
}
break;
case 'right-open':
if (offset > -SWIPEABLE_ACTION_WIDTH) {
close();
} else {
open('right');
}
break;
case 'closed':
if (offset > SWIPEABLE_ACTION_WIDTH) {
open('left');
} else if (offset < -SWIPEABLE_ACTION_WIDTH) {
open('right');
} else {
close();
}
}
}
}, [close, leftActions, leftThreshold, listItemXOffset, onSwipeEnd, open, rightActions, rightThreshold]);
const leftActionElements = useMemo(() => Array.from(leftActions).reverse().map((item, index) => /*#__PURE__*/React.createElement(SwipeableAction, {
activationThreshold: leftThreshold,
close: close,
index: index,
item: item,
key: item.key,
listItemXOffset: listItemXOffset,
position: "left",
shouldUseOpacityAnimation: shouldUseOpacityAnimation,
totalActionCount: leftActions.length
})), [close, leftActions, leftThreshold, listItemXOffset, shouldUseOpacityAnimation]);
const rightActionElements = useMemo(() => rightActions.map((item, index) => /*#__PURE__*/React.createElement(SwipeableAction, {
activationThreshold: rightThreshold,
close: close,
index: index,
item: item,
key: item.key,
listItemXOffset: listItemXOffset,
position: "right",
shouldUseOpacityAnimation: shouldUseOpacityAnimation,
totalActionCount: rightActions.length
})), [rightActions, rightThreshold, close, listItemXOffset, shouldUseOpacityAnimation]);
return /*#__PURE__*/React.createElement(StyledMotionSwipeableWrapper, {
onPan: isDisabled ? undefined : handlePan,
onPanEnd: isDisabled ? undefined : handlePanEnd,
ref: swipeableWrapperRef,
style: {
x: listItemXOffset
}
}, leftActionElements, /*#__PURE__*/React.createElement(StyledSwipeableWrapperContent, null, children), rightActionElements);
};
SwipeableWrapper.displayName = 'SwipeableWrapper';
export default SwipeableWrapper;
//# sourceMappingURL=SwipeableWrapper.js.map