Slider in react js: How to set width to fill outer div correctly?

365 views Asked by At

I mostly used the following code to create a range slider: https://codesandbox.io/s/multi-range-slider-react-js-forked-3b5n0?file=/src/component/multiRangeSlider/MultiRangeSlider.js

In the CSS in that example, the width of the slider and thumb are set to 200px. However, in my code, I want to use the slider-component inside a grid element where the slider should span over the full width of that element. But when I set the width of .slider and .thumb to 100%, the thumbs are not displayed in the right position anymore.

-> See image The left thumb is off the screen, the right thumb too far to the right.

You can recreate this effect in the codesandbox I linked above by changing the code to this:

App.js

import MultiRangeSlider from "./component/multiRangeSlider/MultiRangeSlider";
import "./styles.css";

const App = () => {
  return (
    <div className="grid-container">
      <div className="main1">
        <MultiRangeSlider
          min={0}
          max={1000}
          onChange={({ min, max }) => console.log(`min = ${min}, max = ${max}`)}
        />
      </div>
      <div className="main2">
        <p>main 2</p>
      </div>
    </div>
  );
};

export default App;

styles.css

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  background-color: #242526;
  font-family: sans-serif;
}

.grid-container {
  display: grid;
  grid-template-areas:
    "main1 main2";
  grid-template-columns: 1fr 1fr;
  gap: 40px;
  margin: auto;
  margin-bottom: 40px;
}

.main1 {grid-area: main1;}
.main2 {grid-area: main2;}

MultiRangeSlider.css

.container {
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

.slider {
  position: relative;
  width: 100%;
}

.slider__track,
.slider__range,
.slider__left-value,
.slider__right-value {
  position: absolute;
}

.slider__track,
.slider__range {
  border-radius: 3px;
  height: 5px;
}

.slider__track {
  background-color: #ced4da;
  width: 100%;
  z-index: 1;
}

.slider__range {
  background-color: #9fe5e1;
  z-index: 2;
}

.slider__left-value,
.slider__right-value {
  color: #dee2e6;
  font-size: 12px;
  margin-top: 20px;
}

.slider__left-value {
  left: 6px;
}

.slider__right-value {
  right: -4px;
}

/* Removing the default appearance */
.thumb,
.thumb::-webkit-slider-thumb {
  -webkit-appearance: none;
  -webkit-tap-highlight-color: transparent;
}

.thumb {
  pointer-events: none;
  position: absolute;
  height: 0;
  width: 100%;
  outline: none;
}

.thumb--left {
  z-index: 3;
}

.thumb--right {
  z-index: 4;
}

/* For Chrome browsers */
.thumb::-webkit-slider-thumb {
  background-color: #f1f5f7;
  border: none;
  border-radius: 50%;
  box-shadow: 0 0 1px 1px #ced4da;
  cursor: pointer;
  height: 18px;
  width: 18px;
  margin-top: 4px;
  pointer-events: all;
  position: relative;
}

/* For Firefox browsers */
.thumb::-moz-range-thumb {
  background-color: #f1f5f7;
  border: none;
  border-radius: 50%;
  box-shadow: 0 0 1px 1px #ced4da;
  cursor: pointer;
  height: 18px;
  width: 18px;
  margin-top: 4px;
  pointer-events: all;
  position: relative;
}

MultiRangeSlider.js (stays the same, no changes)

import React, { useCallback, useEffect, useState, useRef } from "react";
import PropTypes from "prop-types";
import "./multiRangeSlider.css";

const MultiRangeSlider = ({ min, max, onChange }) => {
  const [minVal, setMinVal] = useState(min);
  const [maxVal, setMaxVal] = useState(max);
  const minValRef = useRef(min);
  const maxValRef = useRef(max);
  const range = useRef(null);

  // Convert to percentage
  const getPercent = useCallback(
    (value) => Math.round(((value - min) / (max - min)) * 100),
    [min, max]
  );

  // Set width of the range to decrease from the left side
  useEffect(() => {
    const minPercent = getPercent(minVal);
    const maxPercent = getPercent(maxValRef.current);

    if (range.current) {
      range.current.style.left = `${minPercent}%`;
      range.current.style.width = `${maxPercent - minPercent}%`;
    }
  }, [minVal, getPercent]);

  // Set width of the range to decrease from the right side
  useEffect(() => {
    const minPercent = getPercent(minValRef.current);
    const maxPercent = getPercent(maxVal);

    if (range.current) {
      range.current.style.width = `${maxPercent - minPercent}%`;
    }
  }, [maxVal, getPercent]);

  // Get min and max values when their state changes
  useEffect(() => {
    onChange({ min: minVal, max: maxVal });
  }, [minVal, maxVal, onChange]);

  return (
    <div className="container">
      <input
        type="range"
        min={min}
        max={max}
        value={minVal}
        onChange={(event) => {
          const value = Math.min(Number(event.target.value), maxVal - 1);
          setMinVal(value);
          minValRef.current = value;
        }}
        className="thumb thumb--left"
        style={{ zIndex: minVal > max - 100 && "5" }}
      />
      <input
        type="range"
        min={min}
        max={max}
        value={maxVal}
        onChange={(event) => {
          const value = Math.max(Number(event.target.value), minVal + 1);
          setMaxVal(value);
          maxValRef.current = value;
        }}
        className="thumb thumb--right"
      />

      <div className="slider">
        <div className="slider__track" />
        <div ref={range} className="slider__range" />
        <div className="slider__left-value">{minVal}</div>
        <div className="slider__right-value">{maxVal}</div>
      </div>
    </div>
  );
};

MultiRangeSlider.propTypes = {
  min: PropTypes.number.isRequired,
  max: PropTypes.number.isRequired,
  onChange: PropTypes.func.isRequired
};

export default MultiRangeSlider;

I tried to change the position: absolute/relative values, but I couldn't figure out a solution. Thanks for any help!

1

There are 1 answers

0
Sarah On

Simply add a position: relative (and align-items: top) to the outer container. This way, the thumbs orient their position to that container.