• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zhihu / griffith / 8326834451

18 Mar 2024 12:49PM UTC coverage: 48.075% (+0.3%) from 47.737%
8326834451

push

github

ambar
chore: upgrade lerna/commitlint/husky/commitlint

336 of 1190 branches covered (28.24%)

Branch coverage included in aggregate %.

1525 of 2681 relevant lines covered (56.88%)

76.94 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.82
/packages/griffith/src/components/Slider.tsx
1
import React, {useEffect, useRef} from 'react'
46✔
2
import {css, StyleDeclarationMap} from 'aphrodite/no-important'
10✔
3
import clamp from 'lodash/clamp'
10✔
4
import {ProgressDot as ProgressDotType} from '../types'
5
import ProgressDot, {ProgressDotsProps} from './ProgressDot'
10✔
6
import styles, {
7
  horizontal as horizontalStyles,
8
  vertical as verticalStyles,
9
} from './Slider.styles'
10✔
10
import useHandler from '../hooks/useHandler'
10✔
11
import useSetState from '../hooks/useSetState'
10✔
12

13
type SlideStyle = typeof horizontalStyles & typeof verticalStyles // & {thumbSliding: unknown}
14
type OwnStyleKey = keyof SlideStyle
15
type StyleKey = OwnStyleKey | 'buffered' | 'thumb'
16

17
type OwnProps = {
18
  orientation?: 'horizontal' | 'vertical'
19
  reverse?: boolean
20
  value?: number
21
  buffered?: number
22
  total?: number
23
  step?: number
24
  onFocus?: React.FocusEventHandler<HTMLDivElement>
25
  onBlur?: React.FocusEventHandler<HTMLDivElement>
26
  onDragStart?: () => void
27
  onDragEnd?: () => void
28
  onChange?: (value: number) => void
29
  onProgressDotHover?: ProgressDotsProps['onProgressDotHover']
30
  onProgressDotLeave?: ProgressDotsProps['onProgressDotLeave']
31
  noInteraction?: boolean
32
  progressDots?: ProgressDotType[]
33
  styles: Record<string, unknown> | Record<string, unknown>[]
34
  // styles: StyleDeclaration<SlideStyle>[] | Partial<StyleDeclaration<SlideStyle>>
35
}
36

37
const getRatio = (value: number, total?: number) =>
10✔
38
  total ? clamp(value / total, 0, 1) : 0
32!
39

40
const toPercentage = (value: number) => `${value * 100}%`
12✔
41

42
export type SliderProps = OwnProps //& typeof Slider.defaultProps
43

44
Slider.defaultProps = {
10✔
45
  orientation: 'horizontal',
46
  reverse: false,
47
  progressDots: [] as ProgressDotType[],
48
}
49

50
function Slider(props: SliderProps) {
51
  const {
52
    orientation,
53
    reverse,
54
    onFocus,
55
    onBlur,
56
    onChange,
57
    onDragStart,
58
    onDragEnd,
59
    noInteraction,
60
    progressDots,
61
    value = 0,
×
62
    buffered = 0,
2✔
63
    total = 0,
×
64
    step = 1,
6✔
65
    onProgressDotHover,
66
    onProgressDotLeave,
67
  } = props
16✔
68
  const [{isSlideActive, isSliding, slidingValue}, setState] = useSetState({
16✔
69
    isSlideActive: false,
70
    isSliding: false,
71
    slidingValue: null as null | number,
72
  })
73

74
  const isHorizontal = orientation === 'horizontal'
16✔
75
  const trackRef = useRef<HTMLDivElement>(null)
16✔
76

77
  const getStyles = (name: StyleKey) => {
16✔
78
    const variantStyles = isHorizontal ? horizontalStyles : verticalStyles
116✔
79
    let customStyles = props.styles
116✔
80
    if (!Array.isArray(customStyles)) {
116✔
81
      customStyles = [customStyles]
28✔
82
    }
83
    customStyles = customStyles.filter(Boolean)
116✔
84

85
    return [
116✔
86
      styles[name as OwnStyleKey],
87
      variantStyles[name as OwnStyleKey],
88
      ...customStyles.map((item) => item[name]),
150✔
89
    ] as StyleDeclarationMap[]
90
  }
91

92
  const getClassName = (...names: StyleKey[]) => {
16✔
93
    return css(...names.map((name) => getStyles(name)))
116✔
94
  }
95

96
  const alignKey = (() => {
16✔
97
    if (isHorizontal) {
16✔
98
      return reverse ? 'right' : 'left'
12!
99
    } else {
100
      return reverse ? 'top' : 'bottom'
4!
101
    }
102
  })()
103

104
  const sizeKey = isHorizontal ? 'width' : 'height'
16✔
105

106
  const getProgressStyle = (value: number) => {
16✔
107
    const scaleAxis = isHorizontal ? 'scaleX' : 'scaleY'
24✔
108
    return {
24✔
109
      [sizeKey]: '100%',
110
      transform: `${scaleAxis}(${value})`,
111
      transformOrigin: alignKey,
112
    }
113
  }
114

115
  const getProgressThumbStyle = (value: number) => {
16✔
116
    const translateAxis = isHorizontal ? 'translateX' : 'translateY'
12✔
117
    return {
12✔
118
      [sizeKey]: '100%',
119
      transform: `${translateAxis}(${toPercentage(
120
        isHorizontal ? value : 1 - value
6✔
121
      )})`,
122
      transformOrigin: alignKey,
123
    }
124
  }
125

126
  const getSlidingValue = (event: MouseEvent) => {
16✔
127
    const track = trackRef.current
2✔
128
    if (!track) return 0
2!
129
    const rect = track.getBoundingClientRect()
2✔
130

131
    let value
132
    if (isHorizontal) {
2!
133
      value = (event.clientX - rect.left) / rect.width
2✔
134
    } else {
135
      value = (rect.bottom - event.clientY) / rect.height
×
136
    }
137
    value = clamp(value, 0, 1)
2✔
138
    if (reverse) {
2!
139
      value = 1 - value
×
140
    }
141

142
    return value * total
2✔
143
  }
144

145
  const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): void => {
16✔
146
    let direction = 0
2✔
147
    let handled = false
2✔
148
    if (event.key === 'ArrowLeft' || event.key === 'ArrowDown') {
2!
149
      handled = true
×
150
      direction = -1
×
151
    } else if (event.key === 'ArrowRight' || event.key === 'ArrowUp') {
2!
152
      handled = true
2✔
153
      direction = 1
2✔
154
    }
155
    if (handled) {
2✔
156
      event.preventDefault()
2✔
157
      if (reverse) {
2!
158
        direction = -direction
×
159
      }
160
      const result = clamp(value + step * direction, 0, total)
2✔
161
      if (result !== value) {
2✔
162
        handleChange(result)
2✔
163
      }
164
    }
165
  }
166

167
  const handleDragStart = (event: React.MouseEvent<HTMLDivElement>) => {
16✔
168
    if (event.button !== 0) return
2!
169
    const value = getSlidingValue(event as unknown as MouseEvent)
2✔
170
    setState({
2✔
171
      isSlideActive: true,
172
      slidingValue: value,
173
    })
174
    onDragStart?.()
2!
175
    handleChange(value)
2✔
176
  }
177

178
  const handleDragMove = useHandler((event: MouseEvent) => {
16✔
179
    const value = getSlidingValue(event)
×
180
    setState({
×
181
      slidingValue: value,
182
      isSliding: true,
183
    })
184
    handleChange(value)
×
185
  })
186

187
  const handleDragEnd = useHandler((event: MouseEvent): void => {
16✔
188
    onDragEnd?.()
2!
189
    if (isSliding) {
2!
190
      // 点击动作不需要重复触发 change event
191
      handleChange(getSlidingValue(event))
×
192
    }
193
    setState({
2✔
194
      isSlideActive: false,
195
      slidingValue: null,
196
      isSliding: false,
197
    })
198
  })
199

200
  useEffect(() => {
16✔
201
    if (isSlideActive) {
12✔
202
      document.addEventListener('mousemove', handleDragMove)
2✔
203
      document.addEventListener('mouseup', handleDragEnd)
2✔
204
      return () => {
2✔
205
        document.removeEventListener('mousemove', handleDragMove)
2✔
206
        document.removeEventListener('mouseup', handleDragEnd)
2✔
207
      }
208
    }
209
  }, [handleDragEnd, handleDragMove, isSlideActive])
210

211
  const handleChange = (value: number) => {
16✔
212
    onChange?.(value)
4!
213
  }
214

215
  const interactionProps = noInteraction
16✔
216
    ? {}
8✔
217
    : {
218
        tabIndex: 0,
219
        onFocus,
220
        onBlur,
221
        onKeyDown: handleKeyDown,
222
        onMouseDown: handleDragStart,
223
      }
224
  const ratio = getRatio(isSlideActive ? slidingValue! : value, total)
16✔
225
  const bufferedRatio = getRatio(buffered, total)
16✔
226

227
  return (
16✔
228
    <div className={getClassName('root')} {...interactionProps} role="slider">
229
      <div className={getClassName('inner')}>
230
        <div ref={trackRef} className={getClassName('track')}>
231
          {Boolean(buffered) && (
12✔
232
            <div
233
              className={getClassName('bar', 'buffered')}
234
              style={getProgressStyle(bufferedRatio)}
235
            />
236
          )}
237
          <div
238
            className={getClassName('bar')}
239
            style={getProgressStyle(ratio)}
240
          />
241
          {Boolean(progressDots?.length) && (
16!
242
            <ProgressDot
243
              progressDots={progressDots!}
244
              total={total}
245
              onProgressDotHover={onProgressDotHover}
246
              onProgressDotLeave={onProgressDotLeave}
247
            />
248
          )}
249
        </div>
250
        {!noInteraction && (
14✔
251
          // the position indicator (visible when hovering)
252
          <div
253
            className={getClassName('thumbWrapper')}
254
            style={getProgressThumbStyle(ratio)}
255
          >
256
            <div
257
              className={getClassName(
258
                'thumb',
259
                (isSlideActive && 'thumbSliding') as OwnStyleKey
7✔
260
              )}
261
            />
262
          </div>
263
        )}
264
      </div>
265
    </div>
266
  )
267
}
268

269
export default Slider
10✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc