Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .storybook/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const path = require('path')

module.exports = ({ config }) => {
config.resolve.modules.push(path.resolve(__dirname, '../src'))

return config
}
5 changes: 5 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
module.exports = {
setupFilesAfterEnv: ['./config/jest.setup.ts'],
moduleNameMapper: {
'^components(.*)$': '<rootDir>/src/component$1',
'^hooks(.*)$': '<rootDir>/src/hooks$1',
'^utils(.*)$': '<rootDir>/src/utils$1',
},
}
16 changes: 15 additions & 1 deletion src/components/RangeSlider/__stories__/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const Basic = () => {
return (
<DemoContainer>
<RangeSlider value={range} min={0} max={100} onChange={setRange} />
<pre>{JSON.stringify({ values: range }, null, 2)}</pre>
</DemoContainer>
)
}
Expand All @@ -39,6 +40,7 @@ const WithStep = () => {
return (
<DemoContainer>
<RangeSlider value={range} min={0} max={1000} step={100} onChange={setRange} />
<pre>{JSON.stringify({ values: range }, null, 2)}</pre>
</DemoContainer>
)
}
Expand All @@ -49,9 +51,21 @@ const WithMarkers = () => {
return (
<DemoContainer>
<RangeSlider value={range} min={0} max={100} markers={demoMarkers} onChange={setRange} />
<pre>{JSON.stringify({ values: range }, null, 2)}</pre>
</DemoContainer>
)
}

const WithLargeRange = () => {
const [range, setRange] = useRangeSliderDemo([300000, 700000])

return (
<DemoContainer>
<RangeSlider value={range} min={0} max={1000000} step={1000} onChange={setRange} />
<pre>{JSON.stringify({ values: range }, null, 2)}</pre>
</DemoContainer>
)
}

export default { title: 'RangeSlider' }
export { Basic, WithStep, WithMarkers }
export { Basic, WithStep, WithMarkers, WithLargeRange }
187 changes: 29 additions & 158 deletions src/components/RangeSlider/index.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,17 @@
import React from 'react'

import { TRangeTuple, IRangeMarker } from '../../types'

import {
SliderContainer,
SliderRail,
SliderTrack,
SliderThumb,
SliderMarker,
SliderLabelContainer,
SliderMarkerLabel,
} from './styled'
import {
calculatePercentage,
percentageToValue,
roundValueToStep,
clamp,
trackMovement,
isInRange,
} from './utils'

import { useRangeSlider } from 'hooks/useRangeSlider'

import { IRangeMarker, TRangeTuple } from 'types'

export interface IRangeSlider {
value: TRangeTuple
Expand All @@ -31,169 +24,47 @@ export interface IRangeSlider {
}

const RangeSlider: React.FC<IRangeSlider> = ({
value: [minValue, maxValue],
value,
max,
min,
onChange = () => {},
step,
markers,
}) => {
const railRef = React.useRef<HTMLSpanElement>(null)
const trackRef = React.useRef<HTMLSpanElement>(null)
const minThumbRef = React.useRef<HTMLSpanElement>(null)
const maxThumbRef = React.useRef<HTMLSpanElement>(null)

const activeHandle = React.useRef<'min' | 'max' | null>(null)
const diff = React.useRef<number>(0)
const touchId = React.useRef<number>()

React.useLayoutEffect(() => {
const minThumbPercentage = calculatePercentage({ current: minValue, min, max })
const maxThumbPercentage = calculatePercentage({ current: maxValue, min, max })

if (minThumbRef.current && maxThumbRef.current && trackRef.current) {
minThumbRef.current.style.left = `${minThumbPercentage}%`
maxThumbRef.current.style.left = `${maxThumbPercentage}%`
trackRef.current.style.left = `${minThumbPercentage}%`
trackRef.current.style.width = `calc(${maxThumbPercentage}% - ${minThumbPercentage}%)`
}
}, [minValue, maxValue, min, max])

const calculateNewValue = (xPosition: number): number => {
const activeHandleRef = activeHandle.current === 'min' ? minThumbRef : maxThumbRef

const newXPosition =
xPosition - diff.current - (railRef.current?.getBoundingClientRect().left ?? 0)
const end = (railRef.current?.offsetWidth ?? 0) - (activeHandleRef.current?.offsetWidth ?? 0)

const newPercentage = calculatePercentage({ current: newXPosition, min: 0, max: end })

return percentageToValue({ percentage: newPercentage, min, max })
}

const handleMove = (event: MouseEvent | TouchEvent) => {
const newXPosition = trackMovement(event, touchId.current)

if (newXPosition === null) {
return
}

const newValue = calculateNewValue(newXPosition)

let clampedValue = clamp({
value: newValue,
min: activeHandle.current === 'min' ? min : minValue,
max: activeHandle.current === 'max' ? max : maxValue,
})
if (step) {
clampedValue = roundValueToStep({ value: clampedValue, step, min })
}

onChange(activeHandle.current === 'min' ? [clampedValue, maxValue] : [minValue, clampedValue])
}

const handleMouseUp = () => {
document.removeEventListener('mouseup', handleMouseUp)
document.removeEventListener('mousemove', handleMove)

activeHandle.current = null
}

const handleMouseDown = (event: React.MouseEvent<HTMLSpanElement>) => {
const activeHandleRef = activeHandle.current === 'min' ? minThumbRef : maxThumbRef
diff.current = event.clientX - (activeHandleRef.current?.getBoundingClientRect().left ?? 0)

document.addEventListener('mousemove', handleMove)
document.addEventListener('mouseup', handleMouseUp)
}

const handleTouchEnd = () => {
document.removeEventListener('touchmove', handleMove)
document.removeEventListener('touchend', handleTouchEnd)

activeHandle.current = null
}

const handleTouchStart = (event: React.TouchEvent) => {
const touch = event.touches[0]

if (touch) {
// Unique number that identifies the current finger in the touch session
touchId.current = touch.identifier
}

const newXPosition = trackMovement(event.nativeEvent, touchId.current)

if (newXPosition === null) {
return
}

const activeHandleRef = activeHandle.current === 'min' ? minThumbRef : maxThumbRef
diff.current = newXPosition - (activeHandleRef.current?.getBoundingClientRect().left ?? 0)

document.addEventListener('touchmove', handleMove)
document.addEventListener('touchend', handleTouchEnd)
}
const {
getRailProps,
getTrackProps,
getMinHandleProps,
getMaxHandleProps,
getMarkerProps,
} = useRangeSlider({
value,
max,
min,
onChange,
step,
})

return (
<SliderContainer>
<SliderRail ref={railRef} />
<SliderTrack ref={trackRef} data-testid="range-track" />

<SliderLabelContainer>
{markers?.map((marker) => {
const markerPosition = calculatePercentage({ current: marker.value, min, max })

return (
<SliderMarkerLabel
key={`marker-label-${marker.label}`}
style={{ left: `${markerPosition}%` }}
>
{marker.label}
</SliderMarkerLabel>
)
})}
</SliderLabelContainer>
<SliderRail {...getRailProps()} />
<SliderTrack data-testid="range-track" {...getTrackProps()} />

{markers?.map((marker) => {
const markerPosition = calculatePercentage({ current: marker.value, min, max })
const inRange = isInRange({ value: marker.value, min: minValue, max: maxValue })
const { style, isInRange } = getMarkerProps(marker)

return (
<SliderMarker
key={`marker-${marker.label}`}
isWithinRange={inRange}
style={{ left: `${markerPosition}%` }}
/>
<>
<SliderMarkerLabel key={`marker-label-${marker.label}`} style={style}>
{marker.label}
</SliderMarkerLabel>
<SliderMarker key={`marker-${marker.label}`} isInRange={isInRange} style={style} />
</>
)
})}

<SliderThumb
ref={minThumbRef}
role="slider"
data-testid="range-min-thumb"
onMouseDown={(event) => {
activeHandle.current = 'min'
handleMouseDown(event)
}}
onTouchStart={(event) => {
activeHandle.current = 'min'
handleTouchStart(event)
}}
/>
<SliderThumb
ref={maxThumbRef}
role="slider"
data-testid="range-max-thumb"
onMouseDown={(event) => {
activeHandle.current = 'max'
handleMouseDown(event)
}}
onTouchStart={(event) => {
activeHandle.current = 'max'
handleTouchStart(event)
}}
/>
<SliderThumb data-testid="range-min-thumb" {...getMinHandleProps()} />
<SliderThumb data-testid="range-max-thumb" {...getMaxHandleProps()} />
</SliderContainer>
)
}
Expand Down
42 changes: 20 additions & 22 deletions src/components/RangeSlider/styled.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import styled from 'styled-components'

const colors = {
primary: '#ef0d33',
grey: '#cbcbcb',
black: '#2f2f31',
white: '#fff',
}

const SliderContainer = styled.div`
position: relative;
box-sizing: content-box;
Expand All @@ -16,16 +23,15 @@ const SliderRail = styled.span`
display: block;
width: 100%;
height: 0.3rem;
background-color: #cfd7e6;
background-color: ${colors.grey};
border-radius: 0.1rem;
`

const SliderTrack = styled.span`
position: absolute;
display: block;
width: 50%;
height: 0.3rem;
background-color: #0095ff;
background-color: ${colors.primary};
border-radius: 0.1rem;
`

Expand All @@ -35,13 +41,17 @@ const SliderThumb = styled.span`
width: 2.4rem;
height: 2.4rem;
transform: translate3d(-50%, -50%, 0);
background: #fff;
background-color: ${colors.white};
box-shadow: 0 0.3rem 0.1rem rgba(0, 0, 0, 0.1), 0 0.4rem 0.8rem rgba(0, 0, 0, 0.13),
0 0 0 0.1rem rgba(0, 0, 0, 0);
border-radius: 50%;
transition: box-shadow 200ms ease-in-out;
transition: box-shadow 200ms ease-in-out, transform 200ms ease-in-out;
outline: none;

&:hover {
&:hover,
&:focus {
transform: scale(1.1) translate3d(-50%, -50%, 0);
z-index: 2;
box-shadow: 0 0.3rem 0.1rem rgba(0, 0, 0, 0.1), 0 0.4rem 0.8rem rgba(0, 0, 0, 0.3),
0 0 0 0.1rem rgba(0, 0, 0, 0.02);
}
Expand All @@ -54,27 +64,23 @@ const SliderThumb = styled.span`
`

interface ISliderMarkerProps {
isWithinRange: boolean
isInRange: boolean
}

const SliderMarker = styled.span<ISliderMarkerProps>`
position: absolute;
width: 0.2rem;
height: 0.9rem;
transform: translate3d(-0.1rem, -0.3rem, 0);
background-color: ${({ isWithinRange }) => (isWithinRange ? '#0095ff' : '#cfd7e6')};
background-color: ${({ isInRange }) => (isInRange ? colors.primary : colors.grey)};
`

const SliderLabelContainer = styled.div``

const SliderMarkerLabel = styled.span`
position: absolute;
min-width: 8rem;
font-weight: 600;
font-size: 1.2rem;
color: #b7becc;
letter-spacing: 0.04rem;
line-height: 1.6rem;
color: ${colors.grey};
user-select: none;

text-align: center;
Expand All @@ -91,12 +97,4 @@ const SliderMarkerLabel = styled.span`
}
`

export {
SliderContainer,
SliderRail,
SliderTrack,
SliderThumb,
SliderMarker,
SliderLabelContainer,
SliderMarkerLabel,
}
export { SliderContainer, SliderRail, SliderTrack, SliderThumb, SliderMarker, SliderMarkerLabel }
Loading