UNPKG

@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
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