Skip to content

Instantly share code, notes, and snippets.

@adbutterfield
Created May 16, 2022 02:36
Show Gist options
  • Save adbutterfield/1913ac62da1f02b4d97009af2e8a4433 to your computer and use it in GitHub Desktop.
Save adbutterfield/1913ac62da1f02b4d97009af2e8a4433 to your computer and use it in GitHub Desktop.
RangeSlider component
import React, { useState, useEffect, useRef } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import clsx from 'clsx';
import useOnMobile from '../../../lib/hooks/useOnMobile';
import { colors, mediaQueries as mq } from '../../../lib/styles';
const useStyles = makeStyles(() => ({
RangeSlider: {
[mq.smOnly]: {
display: 'flex',
},
[mq.mdUp]: {
width: '47%',
},
},
RangeSlider__wrap: {
[mq.smOnly]: {
width: '100%',
},
},
RangeSlider__headingWrap: {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
},
RangeSlider__heading: {
fontWeight: 'bold',
[mq.smOnly]: {
fontSize: 16,
margin: 0,
},
[mq.mdUp]: {
fontSize: 20,
},
[mq.lgOnly]: {
width: 245,
},
},
RangeSlider__result: {
backgroundColor: colors.white,
border: `2px solid ${colors.borderColor}`,
borderRadius: 10,
textAlign: 'center',
lineHeight: 1.1,
padding: '10px 4px',
[mq.smOnly]: {
width: 90,
marginRight: 16,
fontSize: 12,
height: 56,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
},
[mq.mdOnly]: {
fontSize: 14,
width: 110,
},
[mq.lgOnly]: {
fontSize: 18,
width: 'calc(100% - 280px)',
},
},
RangeSlider__resultNumber: {
color: colors.orange,
fontWeight: 'normal',
marginRight: 6,
[mq.smOnly]: {
fontSize: 15,
},
[mq.mdUp]: {
fontSize: 24,
},
},
RangeSlider__inputWrap: {
position: 'relative',
'&::before': {
left: 0,
backgroundColor: colors.orange,
zIndex: 2,
display: 'block',
content: '""',
position: 'absolute',
[mq.smOnly]: {
width: 3,
height: 10,
top: 9,
},
[mq.mdUp]: {
width: 5,
height: 20,
top: 6,
},
},
'&::after': {
right: 0,
backgroundColor: '#d1d1d1',
zIndex: 2,
display: 'block',
content: '""',
position: 'absolute',
[mq.smOnly]: {
width: 3,
height: 10,
top: 9,
},
[mq.mdUp]: {
width: 5,
height: 20,
top: 6,
},
},
},
RangeSlider__input: {
position: 'relative',
zIndex: 10,
appearance: 'none',
background: 'none',
width: 'calc(100% + 18px)',
margin: '0 -12px',
borderRadius: 0,
cursor: 'pointer',
height: 30,
'&::-webkit-slider-thumb': {
position: 'relative',
display: 'block',
border: `3px solid ${colors.orange}`,
boxShadow: '3px 3px 10px rgba(0, 0, 0, 0.4)',
backgroundColor: colors.white,
borderRadius: '50%',
width: 25,
height: 25,
},
'&::-moz-range-thumb': {
border: `3px solid ${colors.orange}`,
borderRadius: 20,
background: colors.white,
width: 25,
height: 25,
},
'&::-ms-track': {
width: '100%',
height: 12,
animate: '0.2s',
background: 'transparent',
borderColor: 'transparent',
borderWidth: '16px 0',
color: 'transparent',
},
'&::-ms-tooltip': {
display: 'none',
},
'&::-ms-thumb': {
border: `3px solid ${colors.orange}`,
borderRadius: 20,
background: colors.white,
width: 25,
height: 25,
},
},
RangeSlider__base: {
content: '""',
backgroundColor: '#d1d1d1',
width: '100%',
position: 'absolute',
left: 0,
top: 12,
zIndex: 1,
[mq.smOnly]: {
height: 4,
},
[mq.mdUp]: {
height: 6,
},
},
RangeSlider__fill: {
content: '""',
backgroundColor: colors.orange,
position: 'absolute',
left: 0,
top: 12,
zIndex: 1,
[mq.smOnly]: {
height: 4,
},
[mq.mdUp]: {
height: 6,
},
},
RangeSlider__ticks: {
display: 'flex',
justifyContent: 'space-between',
marginTop: 0,
},
RangeSlider__tick: {
fontSize: 12,
color: '#bababa',
fontWeight: 'bold',
},
RangeSlider__tickName: {
lineHeight: 1,
fontSize: 11,
color: '#bababa',
textAlign: 'right',
margin: 0,
},
SliderIndicatorArrow: {
pointerEvents: 'none',
position: 'absolute',
display: 'flex',
width: 30,
height: 30,
alignItems: 'center',
justifyContent: 'center',
zIndex: 100,
opacity: 0,
top: -1,
transition: 'opacity 0.2s ease-in',
},
'SliderIndicatorArrow--left': {
transform: 'rotate(180deg)',
top: 0,
},
'SliderIndicatorArrow--visible': {
opacity: 1,
},
SliderIndicatorArrow__inner: {
position: 'relative',
width: 30,
height: 30,
},
SliderIndicatorArrow__firstArrowIcon: {
left: '30%',
position: 'absolute',
marginLeft: 0,
width: 12,
height: 12,
backgroundSize: 'contain',
top: 10,
backgroundImage: 'url(data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiI+PHN0eWxlPi5zdDB7ZmlsbDojNWEzMDg3fTwvc3R5bGU+PHBhdGggY2xhc3M9InN0MCIgZD0iTTMxOS4xIDIxN2MyMC4yIDIwLjIgMTkuOSA1My4yLS42IDczLjdzLTUzLjUgMjAuOC03My43LjZsLTE5MC0xOTBjLTIwLjEtMjAuMi0xOS44LTUzLjIuNy03My43UzEwOSA2LjggMTI5LjEgMjdsMTkwIDE5MHoiLz48cGF0aCBjbGFzcz0ic3QwIiBkPSJNMzE5LjEgMjkwLjVjMjAuMi0yMC4yIDE5LjktNTMuMi0uNi03My43cy01My41LTIwLjgtNzMuNy0uNmwtMTkwIDE5MGMtMjAuMiAyMC4yLTE5LjkgNTMuMi42IDczLjdzNTMuNSAyMC44IDczLjcuNmwxOTAtMTkweiIvPjwvc3ZnPg==)',
animationName: '$bounceAlpha',
animationDuration: '1.4s',
animationIterationCount: 'infinite',
animationTimingFunction: 'linear',
animationDelay: '0.2s',
},
SliderIndicatorArrow__secondArrowIcon: {
left: '30%',
position: 'absolute',
width: 12,
height: 12,
backgroundSize: 'contain',
top: 10,
backgroundImage: 'url(data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgNTEyIDUxMiI+PHN0eWxlPi5zdDB7ZmlsbDojNWEzMDg3fTwvc3R5bGU+PHBhdGggY2xhc3M9InN0MCIgZD0iTTMxOS4xIDIxN2MyMC4yIDIwLjIgMTkuOSA1My4yLS42IDczLjdzLTUzLjUgMjAuOC03My43LjZsLTE5MC0xOTBjLTIwLjEtMjAuMi0xOS44LTUzLjIuNy03My43UzEwOSA2LjggMTI5LjEgMjdsMTkwIDE5MHoiLz48cGF0aCBjbGFzcz0ic3QwIiBkPSJNMzE5LjEgMjkwLjVjMjAuMi0yMC4yIDE5LjktNTMuMi0uNi03My43cy01My41LTIwLjgtNzMuNy0uNmwtMTkwIDE5MGMtMjAuMiAyMC4yLTE5LjkgNTMuMi42IDczLjdzNTMuNSAyMC44IDczLjcuNmwxOTAtMTkweiIvPjwvc3ZnPg==)',
animationName: '$bounceAlpha',
animationDuration: '1.4s',
animationIterationCount: 'infinite',
animationTimingFunction: 'linear',
marginLeft: 8,
},
'@keyframes bounceAlpha': {
'0%': {
opacity: 1,
transform: 'translateX(0px) scale(1)',
},
'25%': {
opacity: 0,
transform: 'translateX(10px) scale(0.9)',
},
'26%': {
opacity: 0,
transform: 'translateX(-10px) scale(0.9)',
},
'55%': {
opacity: 1,
transform: 'translateX(0px) scale(1)',
},
},
}));
type RangeSliderProps = {
heading: string;
unit: 'yen' | 'kw';
fillWidthFn: (value: number) => number;
min: number;
max: number;
step: number;
onChange: (newValue: number) => void;
steps?: (number | string)[];
initialValue: number;
resultFn: (value: any) => any;
showArrows?: boolean;
};
const RangeSlider: React.FC<RangeSliderProps> = ({ heading, resultFn, unit, fillWidthFn, min, max, step, onChange, steps, initialValue, showArrows = false }) => {
const isMobile = useOnMobile();
const inputRef = useRef<HTMLInputElement | null>(null);
const [sliderArrowsVisible, setSliderArrowsVisible] = useState(true);
const [sliderArrowLeftPosition, setSliderArrowLeftPosition] = useState<number>();
const [sliderArrowRightPosition, setSliderArrowRightPosition] = useState<number>();
const [value, setValue] = useState<string>();
const [result, setResult] = useState();
const [fillWidth, setFillWidth] = useState<number>();
const classes = useStyles();
useEffect(() => {
setValue(String(initialValue));
setFillWidth(fillWidthFn(initialValue));
}, [initialValue]);
useEffect(() => {
setSliderArrowsVisible(showArrows);
}, [showArrows]);
useEffect(() => {
if (resultFn && initialValue) {
setResult(resultFn(initialValue));
}
}, [resultFn, initialValue]);
useEffect(() => {
if (inputRef.current && showArrows && value) {
const inputWidth = inputRef.current.clientWidth;
const newPoint = Number(value) / max;
const newPlace = inputWidth * newPoint;
setSliderArrowLeftPosition(newPlace - 52);
setSliderArrowRightPosition(newPlace - 3);
}
}, [inputRef, value, max, showArrows]);
const hideSliderArrows = () => {
setSliderArrowsVisible(false);
};
const updateValue = (event: React.ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
setResult(resultFn(event.target.value));
setFillWidth(fillWidthFn(Number(event.target.value)));
};
return (
<div className={classes.RangeSlider}>
{isMobile && <p className={classes.RangeSlider__result}><span className={classes.RangeSlider__resultNumber}>{result}</span>{ unit === 'yen' ? '円' : 'kW' }</p>}
<div className={classes.RangeSlider__wrap}>
<div className={classes.RangeSlider__headingWrap}>
<p className={classes.RangeSlider__heading}>{heading}</p>
{!isMobile && <p className={classes.RangeSlider__result}><span className={classes.RangeSlider__resultNumber}>{result}</span>{ unit === 'yen' ? '円' : 'kW' }</p>}
</div>
<div className={classes.RangeSlider__inputWrap}>
<div className={classes.RangeSlider__base} />
<div className={classes.RangeSlider__fill} style={{ width: `${fillWidth}%` }} />
<div className={clsx(classes.SliderIndicatorArrow, classes['SliderIndicatorArrow--left'], sliderArrowsVisible && classes['SliderIndicatorArrow--visible'])} style={{ left: `${sliderArrowLeftPosition}px` }}>
<div className={classes.SliderIndicatorArrow__inner}>
<span className={classes.SliderIndicatorArrow__firstArrowIcon} />
<span className={classes.SliderIndicatorArrow__secondArrowIcon} />
</div>
</div>
<input
type="range"
name="monthlyBill"
min={min}
max={max}
step={step}
onChange={updateValue}
className={clsx('ga-click-tracking-target', classes.RangeSlider__input)}
onMouseDown={hideSliderArrows}
onTouchStart={hideSliderArrows}
value={value !== undefined ? value : ''}
onMouseUp={() => onChange(Number(value))}
onTouchEnd={() => onChange(Number(value))}
ref={inputRef}
/>
<div className={clsx(classes.SliderIndicatorArrow, sliderArrowsVisible && classes['SliderIndicatorArrow--visible'])} style={{ left: `${sliderArrowRightPosition}px` }}>
<div className={classes.SliderIndicatorArrow__inner}>
<span className={classes.SliderIndicatorArrow__firstArrowIcon} />
<span className={classes.SliderIndicatorArrow__secondArrowIcon} />
</div>
</div>
<div className={classes.RangeSlider__ticks}>
{ steps && (
steps.map((s, index) => (
// eslint-disable-next-line react/no-array-index-key
<div className={classes.RangeSlider__tick} key={`${index}-${s}`}>{s}</div>
))
)}
{ !steps && (
<>
<div className={classes.RangeSlider__tick}>0</div>
<div className={classes.RangeSlider__tick}>1</div>
<div className={classes.RangeSlider__tick}>2</div>
<div className={classes.RangeSlider__tick}>3</div>
<div className={classes.RangeSlider__tick}>4</div>
<div className={classes.RangeSlider__tick}>5</div>
</>
)}
</div>
<p className={classes.RangeSlider__tickName}>({ unit === 'yen' ? '万円' : 'kW' })</p>
</div>
</div>
</div>
);
};
export default RangeSlider;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment