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.
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
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With