1

I'm building a react carousel and have the basic app set up. It's a slider that follows your finger on mobile, basically a flexbox with overflow: scroll. The buttons work by scrolling the container by (item index * the width of the item) times, so if I scroll to item #3, it will scroll by 300% the width of the item from the starting position.

Here's the code:

function Carousel(props) {
    const {children} = props
    const [index, setIndex] = useState(0)
    const [length, setLength] = useState(children.length)
    const [touchPosition, setTouchPosition] = useState(null)
    const [movement, setMovement] = useState(0) // saves the distance swiped


    useEffect(() => {
        setLength(children.length)
    }, [children])


    const next = () => {
        if (index < (length-1))  {
            setIndex(prevState => prevState + 1)
        }
        // sets the index to next if you are not on the last slide
    }

    const previous = () => {
        if (index > 0) {
            setIndex(prevState => prevState - 1)
        }
        // sets the index to be the previous if you are further than first slide
    }

    const handleTouchStart = (e) => {
        const touchDown = e.touches[0].clientX
        setTouchPosition(touchDown)
        // saves the touch position on touch start
    }

    const handleTouchEnd = (e) => {
        const touchDown = touchPosition

        if (touchDown === null) {
            return
        }

        const currentTouch = e.changedTouches[0].clientX
        const diff = touchDown - currentTouch

        console.log(diff) // for testing
        setMovement(diff)

        if (diff > 5) {
            setTimeout(() =>  next(), 100)
        } 
        if (diff < -5) {
            setTimeout(() =>  previous(), 100)
        }
        setTouchPosition(null)
        // compares the starting and ending touch positions, calculates the difference
    }

    

    // to test the distance swiped
    const transformMultiplier =() => {
        let mu = (movement / window.innerWidth) * 100
        console.log(`m = ${mu}`)
    }

    useEffect(()=>{
        transformMultiplier()
    }, [movement])

  

    return (
        <div className="carousel-container">
            <div className="carousel-wrapper" 
            onTouchStart={handleTouchStart}
            onTouchEnd={handleTouchEnd}

            > 
               { index > 0 && <button className="left-arrow" onClick={previous}>
                &lt;
                </button> }
                <div className="carousel-content-wrapper">
               { index < children.length -1 &&    <button className="right-arrow" onClick={next}>
                    &gt;
                </button>}
                {/* the sliding is done by the translateX below. it translates by (100% of the slides * the index of the slide) from the starting position.  */}
                    <div className="carousel-content"
                    style={{transform: `translateX(-${index * (window.innerWidth > 480 ? 100 : (100 - (movement / window.innerWidth) * 100))}%)`}}
                    >
                        { children}
                    </div>
                </div>
            </div>
        </div>
    )
}
.carousel-container {
    width: 100vw;
    display: flex;
    flex-direction: column;
}

.carousel-wrapper {
    display: flex;
    width: 100%;
    position: relative;
}

.carousel-content-wrapper {
    overflow: scroll;
    -webkit-overflow-scrolling: auto;
    width: 100%;
    height: 100%;
    -ms-overflow-style: none;  /* hide scrollbar in IE and Edge */
    scrollbar-width: none;  /* hide scrollbar in Firefox */
}

.carousel-content {
    display: flex;
    transition: all 250ms linear;
    -ms-overflow-style: none;  /* hide scrollbar in IE and Edge */
    scrollbar-width: none;  /* hide scrollbar in Firefox */
}

.carousel-content::-webkit-scrollbar, .carousel-content::-webkit-scrollbar {
    display: none;
}
.carousel-content-wrapper::-webkit-scrollbar, .carousel-content::-webkit-scrollbar {
    display: none;
}

.carousel-content > * {
    width: 100%;
    flex-shrink: 0;
    flex-grow: 1;
}

.left-arrow, .right-arrow {
    position: absolute;
    z-index: 1;
    top: 50%;
    transform: translateY(-50%);
    width: 48px;
    height: 48px;
    border-radius: 24px;
    background-color: white;
    border: 1px solid #ddd;
}
.left-arrow {
    left: 24px;
}

.right-arrow {
    right: 24px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

When I used to swipe on mobile, the slider transitions by the swipe amount + the default transition amount. I tried to fix that by trying to subtract the percentage of the screen the swipe took from the transition amount, so if I swipe for 15% of the screen, the slide should transition the remaining 85%.

The problems:

  1. I try to set the amount of swipe movement as a state value "movement", which seems to be causing trouble and doesn't update properly during every swipe. So sometimes the slider uses the "movement" state from the previous swipe.
  2. The inertial scrolling on mobile makes the swipes unpredictable. I couldn't turn it off with some methods I found online.

If anyone could take a look at the code, and maybe point me toward how I should try and fix these problems, that would be great. Or better yet, if someone knows a less jank way of making such a carousel snap to the next/previous item while maintaining the finger-following scroll that would be perfect.

I know this isn't ideal, but the SO snippet editor is giving me an unexplainable error so here's codesandbox if anyone wants to play around with it in action: https://codesandbox.io/s/hardcore-goodall-usspz?file=/src/carousel.js&resolutionWidth=320&resolutionHeight=675

Lursmani
  • 159
  • 1
  • 10

1 Answers1

1

for scrolling use on parent container where you have all images next styles >>

 scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  display: flex;
  overflow-x: scroll;

and then for each slide, child of parent container each this style

scroll-snap-align: start;
n1koloza
  • 539
  • 5
  • 11
  • Holy cow this is such an easy workaround. Instead of relying on a single function to do the transitions, now I handle the button transitions with my original method and swipe transitions with this method. It works perfectly unless you have to change between mobile and desktop versions of the slider on the fly, but that doesn't really happen a lot in practice. მადლობა! – Lursmani Mar 30 '21 at 12:45