mirror of
https://github.com/zhigang1992/react.git
synced 2026-04-29 12:45:32 +08:00
feat(slider): add component
This commit is contained in:
3
components/slider/index.ts
Normal file
3
components/slider/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import Slider from './slider'
|
||||
|
||||
export default Slider
|
||||
69
components/slider/slider-dot.tsx
Normal file
69
components/slider/slider-dot.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import React from 'react'
|
||||
import useTheme from '../styles/use-theme'
|
||||
import withDefaults from '../utils/with-defaults'
|
||||
|
||||
interface Props {
|
||||
left: number
|
||||
disabled?: boolean
|
||||
isClick?: boolean
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
left: 0,
|
||||
disabled: false,
|
||||
isClick: false,
|
||||
}
|
||||
|
||||
type NativeAttrs = Omit<React.HTMLAttributes<any>, keyof Props>
|
||||
export type SliderDotProps = Props & typeof defaultProps & NativeAttrs
|
||||
|
||||
const SliderDot = React.forwardRef<HTMLDivElement, React.PropsWithChildren<SliderDotProps>>(({
|
||||
children, disabled, left, isClick,
|
||||
}, ref: React.Ref<HTMLDivElement>) => {
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<div className={`dot ${disabled ? 'disabled' : ''} ${isClick ? 'click' : ''}`} ref={ref}>
|
||||
{children}
|
||||
<style jsx>{`
|
||||
.dot {
|
||||
position: absolute;
|
||||
left: ${left}%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
height: 1.25rem;
|
||||
line-height: 1.25rem;
|
||||
border-radius: .625rem;
|
||||
user-select: none;
|
||||
font-weight: 700;
|
||||
font-size: .75rem;
|
||||
z-index: 100;
|
||||
background-color: ${theme.palette.success};
|
||||
color: ${theme.palette.background};
|
||||
text-align: center;
|
||||
padding: 0 calc(.86 * ${theme.layout.gapHalf});
|
||||
}
|
||||
|
||||
.dot.disabled {
|
||||
cursor: not-allowed !important;
|
||||
background-color: ${theme.palette.accents_2};
|
||||
color: ${theme.palette.accents_4};
|
||||
}
|
||||
|
||||
.dot.click {
|
||||
transition: all 200ms ease 0;
|
||||
}
|
||||
|
||||
.dot:hover {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.dot:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default withDefaults(SliderDot, defaultProps)
|
||||
53
components/slider/slider-mark.tsx
Normal file
53
components/slider/slider-mark.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import useTheme from '../styles/use-theme'
|
||||
|
||||
interface Props {
|
||||
max: number
|
||||
min: number
|
||||
step: number
|
||||
}
|
||||
|
||||
export type MarkLeftValue = number
|
||||
|
||||
export type Marks = Array<MarkLeftValue>
|
||||
|
||||
const getMarks = (
|
||||
min: number,
|
||||
max: number,
|
||||
step: number,
|
||||
): Marks => {
|
||||
const value = max - min
|
||||
const roundFunc = !(value % step) ? Math.floor : Math.ceil
|
||||
const count = roundFunc(value / step) - 1
|
||||
if (count >= 99) return []
|
||||
|
||||
return [...new Array(count)]
|
||||
.map((_, index) => (step * (index + 1) * 100) / value)
|
||||
}
|
||||
|
||||
const SliderMark: React.FC<React.PropsWithChildren<Props>> = ({
|
||||
step, max, min,
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
const marks = useMemo(() => getMarks(min, max, step), [min, max, step])
|
||||
|
||||
return (
|
||||
<>
|
||||
{marks.map((val, index) => (
|
||||
<span key={`${val}-${index}`} style={{ left: `${val}%` }} />
|
||||
))}
|
||||
<style jsx>{`
|
||||
span {
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: ${theme.palette.background};
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SliderMark
|
||||
145
components/slider/slider.tsx
Normal file
145
components/slider/slider.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import React, { RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import useTheme from '../styles/use-theme'
|
||||
import withDefaults from '../utils/with-defaults'
|
||||
import useDrag, { DraggingEvent } from '../utils/use-drag'
|
||||
import useCurrentState from '../utils/use-current-state'
|
||||
import SliderDot from './slider-dot'
|
||||
import SliderMark from './slider-mark'
|
||||
|
||||
interface Props {
|
||||
value?: number
|
||||
initialValue?: number
|
||||
step?: number
|
||||
max?: number
|
||||
min?: number
|
||||
disabled?: boolean
|
||||
showMarkers?: boolean
|
||||
onChange?: (val: number) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
initialValue: 0,
|
||||
step: 1,
|
||||
min: 0,
|
||||
max: 100,
|
||||
disabled: false,
|
||||
showMarkers: false,
|
||||
className: '',
|
||||
}
|
||||
|
||||
type NativeAttrs = Omit<React.HTMLAttributes<any>, keyof Props>
|
||||
export type SliderProps = Props & typeof defaultProps & NativeAttrs
|
||||
|
||||
const getRefWidth = (elementRef: RefObject<HTMLElement> | null): number => {
|
||||
if (!elementRef || !elementRef.current) return 0
|
||||
const rect = elementRef.current.getBoundingClientRect()
|
||||
return rect.width || (rect.right - rect.left)
|
||||
}
|
||||
|
||||
const getValue = (
|
||||
max: number,
|
||||
min: number,
|
||||
step: number,
|
||||
offsetX: number,
|
||||
railWidth: number,
|
||||
): number => {
|
||||
if (offsetX < 0) return min
|
||||
if (offsetX > railWidth) return max
|
||||
const widthForEachStep = railWidth / (max - min) * step
|
||||
if (widthForEachStep <= 0) return min
|
||||
|
||||
const slideDistance = Math.round(offsetX / widthForEachStep) * step + min
|
||||
return Number.isInteger(slideDistance) ? slideDistance : Number.parseFloat(slideDistance.toFixed(1))
|
||||
}
|
||||
|
||||
const Slider: React.FC<React.PropsWithChildren<SliderProps>> = ({
|
||||
disabled, step, max, min, initialValue, value: customValue,
|
||||
onChange, className, showMarkers, ...props
|
||||
}) => {
|
||||
|
||||
const theme = useTheme()
|
||||
const [value, setValue] = useState<number>(initialValue)
|
||||
const [, setSliderWidth, sideWidthRef] = useCurrentState<number>(0)
|
||||
const [, setLastDargOffset, lastDargOffsetRef] = useCurrentState<number>(0)
|
||||
const [isClick, setIsClick] = useState<boolean>(false)
|
||||
|
||||
const sliderRef = useRef<HTMLDivElement>(null)
|
||||
const dotRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const currentRatio = useMemo(
|
||||
() => (value - min) / (max - min) * 100,
|
||||
[value, max, min],
|
||||
)
|
||||
|
||||
const setLastOffsetManually = (val: number) => {
|
||||
const width = getRefWidth(sliderRef)
|
||||
const shouldOffset = (val - min) / (max - min) * width
|
||||
setLastDargOffset(shouldOffset)
|
||||
}
|
||||
|
||||
const updateValue = useCallback((offset) => {
|
||||
const currentValue = getValue(max, min, step, offset, sideWidthRef.current)
|
||||
setValue(currentValue)
|
||||
onChange && onChange(currentValue)
|
||||
}, [max, min, step, sideWidthRef])
|
||||
|
||||
const dragHandler = (event: DraggingEvent) => {
|
||||
if (disabled) return
|
||||
const currentOffset = event.currentX - event.startX
|
||||
const offset = currentOffset + lastDargOffsetRef.current
|
||||
updateValue(offset)
|
||||
}
|
||||
const dragStartHandler = () => setSliderWidth(getRefWidth(sliderRef))
|
||||
const dragEndHandler = (event: DraggingEvent) => {
|
||||
if (disabled) return
|
||||
const offset = event.currentX - event.startX
|
||||
const currentOffset = offset + lastDargOffsetRef.current
|
||||
setLastDargOffset(currentOffset)
|
||||
setIsClick(false)
|
||||
}
|
||||
const clickHandler = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (disabled) return
|
||||
if (!sliderRef || !sliderRef.current) return
|
||||
setIsClick(true)
|
||||
setSliderWidth(getRefWidth(sliderRef))
|
||||
const clickOffset = event.clientX - sliderRef.current.getBoundingClientRect().x
|
||||
updateValue(clickOffset)
|
||||
}
|
||||
|
||||
useDrag(dotRef, dragHandler, dragStartHandler, dragEndHandler)
|
||||
|
||||
useEffect(() => {
|
||||
if (customValue === undefined) return
|
||||
setValue(customValue)
|
||||
setLastOffsetManually(customValue)
|
||||
}, [customValue])
|
||||
|
||||
useEffect(() => {
|
||||
initialValue && setLastOffsetManually(initialValue)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={`slider ${className}`}
|
||||
onClick={clickHandler}
|
||||
ref={sliderRef} {...props}>
|
||||
<SliderDot disabled={disabled}
|
||||
ref={dotRef}
|
||||
isClick={isClick}
|
||||
left={currentRatio}>{value}</SliderDot>
|
||||
{showMarkers && <SliderMark max={max} min={min} step={step} />}
|
||||
<style jsx>{`
|
||||
.slider {
|
||||
width: 100%;
|
||||
height: .5rem;
|
||||
border-radius: 50px;
|
||||
background-color: ${disabled ? theme.palette.accents_2 : theme.palette.accents_8};
|
||||
position: relative;
|
||||
cursor: ${disabled ? 'not-allow' : 'pointer'};
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default withDefaults(Slider, defaultProps)
|
||||
Reference in New Issue
Block a user