skeletonlabs / skeleton

A complete design system and component solution, built on Tailwind.
https://skeleton.dev
MIT License
4.82k stars 300 forks source link

Dual Range Slider #2224

Closed Silvallis closed 7 months ago

Silvallis commented 9 months ago

Describe the feature in detail (code, mocks, or screenshots encouraged)

A new type of range slider with two pips commonly used for price ranges (for example). image

This feature could either be an enhancement of the existing RangeSlider component or be a new component.

I found this implementation of a dual range slider in pure HTML/CSS:

<div class="range_container">
    <div class="sliders_control">
        <input id="fromSlider" type="range" value="10" min="0" max="100"/>
        <input id="toSlider" type="range" value="40" min="0" max="100"/>
    </div>
    <div class="form_control">
        <div class="form_control_container">
            <div class="form_control_container__time">Min</div>
            <input class="form_control_container__time__input" type="number" id="fromInput" value="10" min="0" max="100"/>
        </div>
        <div class="form_control_container">
            <div class="form_control_container__time">Max</div>
            <input class="form_control_container__time__input" type="number" id="toInput" value="40" min="0" max="100"/>
        </div>
    </div>
</div>
.range_container {
  display: flex;
  flex-direction: column;
  width: 80%;
  margin: 35% auto;
}

.sliders_control {
  position: relative;
  min-height: 50px;
}

.form_control {
  position: relative;
  display: flex;
  justify-content: space-between;
  font-size: 24px;
  color: #635a5a;
}

input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none;
  pointer-events: all;
  width: 24px;
  height: 24px;
  background-color: #fff;
  border-radius: 50%;
  box-shadow: 0 0 0 1px #C6C6C6;
  cursor: pointer;
}

input[type=range]::-moz-range-thumb {
  -webkit-appearance: none;
  pointer-events: all;
  width: 24px;
  height: 24px;
  background-color: #fff;
  border-radius: 50%;
  box-shadow: 0 0 0 1px #C6C6C6;
  cursor: pointer;  
}

input[type=range]::-webkit-slider-thumb:hover {
  background: #f7f7f7;
}

input[type=range]::-webkit-slider-thumb:active {
  box-shadow: inset 0 0 3px #387bbe, 0 0 9px #387bbe;
  -webkit-box-shadow: inset 0 0 3px #387bbe, 0 0 9px #387bbe;
}

input[type="number"] {
  color: #8a8383;
  width: 50px;
  height: 30px;
  font-size: 20px;
  border: none;
}

input[type=number]::-webkit-inner-spin-button, 
input[type=number]::-webkit-outer-spin-button {  
   opacity: 1;
}

input[type="range"] {
  -webkit-appearance: none; 
  appearance: none;
  height: 2px;
  width: 100%;
  position: absolute;
  background-color: #C6C6C6;
  pointer-events: none;
}

#fromSlider {
  height: 0;
  z-index: 1;
}

image

The biggest problem will be that there is no native option in the range input element, therefore it would need to be a custom option like the one provided.

What type of pull request would this be?

New Feature

Provide relevant links or additional information.

https://w3collective.com/double-range-slider-html-css-js/ https://medium.com/@predragdavidovic10/native-dual-range-slider-html-css-javascript-91e778134816 https://svelte.dev/repl/75d34e46cbe64bb68b7c2ac2c61931ce?version=4.2.2 https://codepen.io/predragdavidovic/pen/mdpMoWo

Silvallis commented 9 months ago

Forgot to paste the JS portion of the example:

function controlFromInput(fromSlider, fromInput, toInput, controlSlider) {
    const [from, to] = getParsed(fromInput, toInput);
    fillSlider(fromInput, toInput, '#C6C6C6', '#25daa5', controlSlider);
    if (from > to) {
        fromSlider.value = to;
        fromInput.value = to;
    } else {
        fromSlider.value = from;
    }
}

function controlToInput(toSlider, fromInput, toInput, controlSlider) {
    const [from, to] = getParsed(fromInput, toInput);
    fillSlider(fromInput, toInput, '#C6C6C6', '#25daa5', controlSlider);
    setToggleAccessible(toInput);
    if (from <= to) {
        toSlider.value = to;
        toInput.value = to;
    } else {
        toInput.value = from;
    }
}

function controlFromSlider(fromSlider, toSlider, fromInput) {
  const [from, to] = getParsed(fromSlider, toSlider);
  fillSlider(fromSlider, toSlider, '#C6C6C6', '#25daa5', toSlider);
  if (from > to) {
    fromSlider.value = to;
    fromInput.value = to;
  } else {
    fromInput.value = from;
  }
}

function controlToSlider(fromSlider, toSlider, toInput) {
  const [from, to] = getParsed(fromSlider, toSlider);
  fillSlider(fromSlider, toSlider, '#C6C6C6', '#25daa5', toSlider);
  setToggleAccessible(toSlider);
  if (from <= to) {
    toSlider.value = to;
    toInput.value = to;
  } else {
    toInput.value = from;
    toSlider.value = from;
  }
}

function getParsed(currentFrom, currentTo) {
  const from = parseInt(currentFrom.value, 10);
  const to = parseInt(currentTo.value, 10);
  return [from, to];
}

function fillSlider(from, to, sliderColor, rangeColor, controlSlider) {
    const rangeDistance = to.max-to.min;
    const fromPosition = from.value - to.min;
    const toPosition = to.value - to.min;
    controlSlider.style.background = `linear-gradient(
      to right,
      ${sliderColor} 0%,
      ${sliderColor} ${(fromPosition)/(rangeDistance)*100}%,
      ${rangeColor} ${((fromPosition)/(rangeDistance))*100}%,
      ${rangeColor} ${(toPosition)/(rangeDistance)*100}%, 
      ${sliderColor} ${(toPosition)/(rangeDistance)*100}%, 
      ${sliderColor} 100%)`;
}

function setToggleAccessible(currentTarget) {
  const toSlider = document.querySelector('#toSlider');
  if (Number(currentTarget.value) <= 0 ) {
    toSlider.style.zIndex = 2;
  } else {
    toSlider.style.zIndex = 0;
  }
}

const fromSlider = document.querySelector('#fromSlider');
const toSlider = document.querySelector('#toSlider');
const fromInput = document.querySelector('#fromInput');
const toInput = document.querySelector('#toInput');
fillSlider(fromSlider, toSlider, '#C6C6C6', '#25daa5', toSlider);
setToggleAccessible(toSlider);

fromSlider.oninput = () => controlFromSlider(fromSlider, toSlider, fromInput);
toSlider.oninput = () => controlToSlider(fromSlider, toSlider, toInput);
fromInput.oninput = () => controlFromInput(fromSlider, fromInput, toInput, toSlider);
toInput.oninput = () => controlToInput(toSlider, fromInput, toInput, toSlider);
endigo9740 commented 9 months ago

Hey @Silvallis, yeah we've heard this request on a number of occasions. Unfortunately it's not just a matter of sitting down and building this, but rather the fact this would go against our standards for handling form components. That being, we actively try to avoid creating new form components whenever possible and instead favor native options instead.

In fact, most of our form component elements (ex: Range Slider) are just wrappers around the native implemented features, but with some nice quality of life added. For example, take the Range Slider. We generate the native datalist required to render tick marks:

https://github.com/skeletonlabs/skeleton/blob/master/packages/skeleton/src/lib/components/RangeSlider/RangeSlider.svelte#L98

Our biggest hang up is introducing elements that are not fully accessible. A11y is a major concern of Skeleton, and we would rather go without a feature than implement one with poor support for accessibility. While your example shared above is likely functional, it doesn't appear to make considerations for a11y.

If you or any other volunteers would like to take on the task of building a really first class a11y friendly dual range slider, we would gladly accept it. Otherwise this will likely remain pending for now. Just want to set clear expectations here.

endigo9740 commented 7 months ago

In an effort to prepare for Skeleton v3, we're consolidate some related issues down to a single ticket. This will ensure that we can see the full context of requests when the time comes to refactor and update this feature going forward. If you wish to add additional feedback or suggestions, please so here: