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