Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to drag an element and ensure that the cursor won't faster than it?

I have this slider with two thumbs. The thumbs can be moved all along the slider (line), what is made possible by increasing or decreasing their margin-left, but in order for them to move the state move must be true, it happens when each one of the thumbs triggers the event onClickDown. However, If the event onClickedUp is triggered, the cursor leaves the area of the thumb or the slider, move is set to false, what makes the thumb stop moving. And it's ok, that's the idea.

The problem is that the cursor may be faster than the thumbs movement, as can be seen on the gif below, what makes the cursor leaves the area of the thumb and set move to false, even if that's not what the user wants.

enter image description here

So, in order to make the slider works properly the user would have to be extremelly careful when moving the thumbs, what makes it a very annoying UX.

In short, what I need to do is ensure that the cursor doesn't move faster than the thumb, it doesn't matter if I'll have to slow the cursor or increase the speed of the thumbs.

How could I do that?

Here is my code with some notes:

import React, { Fragment } from 'react'

import './Filter.css'


const Filter = props => {

    const sliderRef = React.useRef() // => main parent div

    const initial_position = 0 
    const end_position = 200 

    const initial_min_value = 5 // => Initial price 
    const initial_max_value = 1290 // => Final price

    let [thumb1_position, setValueThumb1] =  React.useState(0)
    let [thumb2_position, setValueThumb2] =  React.useState(0)
    let [min_value, setMinValue] =  React.useState(initial_min_value)
    let [max_value, setMaxValue] =  React.useState(initial_max_value)
    let [move, setMove] =  React.useState(false) // => Enable thumbs to move
    
    // Ensure that the thumb_2 will be in the end of the slider at first
    React.useEffect(() => {
        setValueThumb2(sliderRef.current.offsetWidth - 5)
    }, [])


    // Here I get the position of the cursor within the element (slider) and move the thumbs based on it.
    const handleChange = e => {

        let thumb_class = e.target.className

        var rect = sliderRef.current.getBoundingClientRect();
        const current_position = e.clientX - rect.left; // X position within the element.

        // Only moves if 'move' is true
        if (move === true) {

            // Get the className to ensure that only the clicked thumb is moved
            if (thumb_class.includes('left-thumb')) {

                // Ensure that the thumb_1 will always be on the left and the thumb_2 on the right
                // Ensure that neither of the thumbs exceed the limits of the slider
                if (current_position >= initial_position && current_position < thumb2_position - 25) {
                    setValueThumb1(current_position)
                } else if (current_position >= initial_position && current_position >= thumb2_position - 25) {
                    setValueThumb1(thumb2_position - 25)
                    setMove(false)
                } else {
                    setValueThumb1(initial_position)
                    setMove(false)
                }

                if (thumb1_position - initial_position < 1) {
                    setMinValue(initial_min_value)
                } else {
                    setMinValue((thumb1_position - initial_position) * 6.46)
                }

            } else if (thumb_class.includes('right-thumb')) {
                
                if (current_position >= thumb1_position + 25 && current_position <= end_position) {
                    setValueThumb2(current_position)
                } else if (current_position >= thumb1_position + 25 && current_position >= end_position) {
                    setValueThumb2(end_position)
                    setMove(false)
                } else {
                    setValueThumb2(thumb1_position + 25)
                    setMove(false)
                }

                if (thumb2_position > end_position - 1) {
                    setMaxValue(initial_max_value)
                } else {
                    setMaxValue((thumb2_position - initial_position) * 6.48)
                }
            }
        }

    }

    const moveOn = e => {
        setMove(true)
    }

    const moveOff = () => {
        setMove(false)
    }



    return (
        <Fragment>
            <div>
                <h6 style={{marginBottom: '35px'}}>PRICE FILTER</h6>
                <div className="range-container"
                    onMouseMove={(e) => handleChange(e)}
                    onMouseDown={(e) => moveOn(e)}
                    onMouseUp={() => moveOff()}
                    onMouseLeave={() => moveOff()}

                    ref={sliderRef}
                >
                    <div className="range">
                        <span className="rounded-circle left-thumb"
                            style={{
                                width:'15px',
                                height: '15px',
                                backgroundColor: 'red',
                                marginTop: '-6px',
                                marginLeft: thumb1_position - 7 + 'px'
                            }}
                        ></span>
                        <span className="rounded-circle right-thumb"
                            style={{
                                width:'15px',
                                height: '15px',
                                backgroundColor: 'black',
                                marginTop: '-6px',
                                marginLeft: thumb2_position - 7 + 'px'
                            }}

                        ></span>
                        <p style={{
                            marginLeft: thumb1_position - 15 + 'px',
                            position: 'absolute',
                            marginTop: '15px'}}
                        > {Math.floor(min_value)}
                        </p>
                        <p style={{
                            marginLeft: thumb2_position - 15 + 'px',
                            position: 'absolute',
                            marginTop: '15px'}}
                        > {Math.floor(max_value)}
                        </p>
                    </div>
                </div>
            </div>
        </Fragment>
    )
}

export default Filter
like image 531
Berg_Durden Avatar asked Sep 18 '25 02:09

Berg_Durden


1 Answers

I kept digging and stumbled over the setPointerCapture() method, what solved my problem.

https://developer.mozilla.org/en-US/docs/Web/API/Element/setPointerCapture

Its function is to do exactly what I was doing with my code but using pointer events. It makes an element move on pointerdown event and make it stop moving on pointerup event.

Since it makes almost all I need to do I could get rid of some functions that I was using. In fact, the only function kept is handleChange. Besides that I also removed the move state, but I had to add some refs to get each element.

This is the new code:

import React, { Fragment } from 'react'

import './Filter.css'


const Filter = props => {

    const sliderRef = React.useRef() // => main parent div

    const sliderRef = React.useRef() // => Div que engloba o slider
    const thumb_1_Ref = React.useRef() // => Div que engloba o slider
    const thumb_2_Ref = React.useRef() // => Div que engloba o slider
    const price_thumb_1_Ref = React.useRef() // => Div que engloba o slider
    const price_thumb_2_Ref = React.useRef() // => Div que engloba o slider
    
    const initial_position = 0 
    
    const initial_min_value = 5 // => Initial price 
    const initial_max_value = 1290 // => Final price

    let [thumb1_position, setValueThumb1] =  React.useState(0)
    let [thumb2_position, setValueThumb2] =  React.useState(0)
    let [mobile_thumb1_position, setValueMobileThumb1] =  React.useState(0)
    let [mobile_thumb2_position, setValueMobileThumb2] =  React.useState(0)
    let [min_value, setMinValue] =  React.useState(initial_min_value)
    let [max_value, setMaxValue] =  React.useState(initial_max_value)
    
    // Ensure that the thumb_2 will be in the end of the slider at first
    React.useEffect(() => {
        setValueThumb2(sliderRef.current.offsetWidth - 5)
    }, [])



    let slider
    let slider_price

    const beginSliding = e => {
        slider.onpointermove = slide
        slider.setPointerCapture(e.pointerId)
    }
    
    const stopSliding = e => {
        slider.onpointermove = null
        slider.releasePointerCapture(e.pointerId)
    }
    
    const slide = e => {
        const thumb_class = e.target.className

        let rect = sliderRef.current.getBoundingClientRect()
        let current_position = e.clientX - rect.left 
        
        if (thumb_class.includes('right-thumb')) {

            current_position = current_position - sliderRef.current.offsetWidth

            if (current_position >= initial_position) {
                current_position = initial_position
            }

            if (current_position <= mobile_thumb1_position - 175) {
                current_position = mobile_thumb1_position - 175
            }
            
            setValueMobileThumb2(current_position)
        } 
        
        if (thumb_class.includes('left-thumb')) {

            if (current_position <= initial_position) {
                current_position = initial_position
            }

            if (current_position >= mobile_thumb2_position + 175) {
                current_position = mobile_thumb2_position + 175
            }

            setValueMobileThumb1(current_position)
        }
        
        slider.style.transform = `translate(${current_position}px)`
        slider_price.style.transform = `translate(${current_position}px)`
    }
        
    
    const handleChange = e => {

        const thumb_class = e.target.className

        if (thumb_class.includes('left-thumb')) {

            slider = thumb_1_Ref.current;
            slider_price = price_thumb_1_Ref.current;

            slider.onpointerdown = beginSliding;
            slider.onpointerup = stopSliding;

            if (mobile_thumb1_position - initial_position < 1) {
                setMinValue(initial_min_value)
            } else {
                setMinValue((mobile_thumb1_position - initial_position) * 6.45)
            }

        } else if (thumb_class.includes('right-thumb')) {
            
            slider = thumb_2_Ref.current;
            slider_price = price_thumb_2_Ref.current;

            slider.onpointerdown = beginSliding;
            slider.onpointerup = stopSliding;

            if (mobile_thumb2_position > -1) {
                setMaxValue(initial_max_value)
            } else {
                setMaxValue((mobile_thumb2_position + 200) * 6.45)
            }
        }

    }



    return (
        <Fragment>
            <div>
                <h6 style={{marginBottom: '35px'}}>PRICE FILTER</h6>
                <div className="range-container"
                    onMouseMove={(e) => handleChange(e)}
                    ref={sliderRef}
                >
                    <div className="range"

                    >
                        <span 
                            className="rounded-circle left-thumb"
                            style={{
                                width:'15px',
                                height: '15px',
                                backgroundColor: 'red',
                                marginTop: '-6px',
                                marginLeft: thumb1_position - 7 + 'px'
                            }}
                            ref={thumb_1_Ref}
                        ></span>
                        <span 
                            className="rounded-circle right-thumb"
                            style={{
                                width:'15px',
                                height: '15px',
                                backgroundColor: 'black',
                                marginTop: '-6px',
                                marginLeft: thumb2_position - 7 + 'px'
                            }}
                            ref={thumb_2_Ref}
                        ></span>
                        <p style={{
                            marginLeft: thumb1_position - 15 + 'px',
                            position: 'absolute',
                            marginTop: '15px'}}
                            ref={price_thumb_1_Ref}
                        > {Math.floor(min_value)}
                        </p>
                        <p style={{
                            marginLeft: thumb2_position - 15 + 'px',
                            position: 'absolute',
                            marginTop: '15px'}}
                            ref={price_thumb_2_Ref}
                        > {Math.floor(max_value)}
                        </p>
                    </div>
                </div>
            </div>
        </Fragment>
    )
}

export default Filter 
like image 194
Berg_Durden Avatar answered Sep 19 '25 17:09

Berg_Durden