Created
May 16, 2022 02:36
-
-
Save adbutterfield/1913ac62da1f02b4d97009af2e8a4433 to your computer and use it in GitHub Desktop.
RangeSlider component
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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