@tamagui/slider 1.0.1-beta.56 → 1.0.1-beta.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Slider.tsx ADDED
@@ -0,0 +1,584 @@
1
+ // forked from radix-ui
2
+
3
+ import { useComposedRefs } from '@tamagui/compose-refs'
4
+ import { getButtonSize, getSize, isWeb, styled, withStaticProperties } from '@tamagui/core'
5
+ import { clamp, composeEventHandlers } from '@tamagui/helpers'
6
+ import { SizableStackProps, ThemeableStack, YStackProps } from '@tamagui/stacks'
7
+ import { useControllableState } from '@tamagui/use-controllable-state'
8
+ import { useDirection } from '@tamagui/use-direction'
9
+ import * as React from 'react'
10
+ import { LayoutRectangle, View } from 'react-native'
11
+
12
+ import {
13
+ ARROW_KEYS,
14
+ BACK_KEYS,
15
+ PAGE_KEYS,
16
+ SLIDER_NAME,
17
+ SliderOrientationProvider,
18
+ SliderProvider,
19
+ useSliderContext,
20
+ useSliderOrientationContext,
21
+ } from './constants'
22
+ import {
23
+ convertValueToPercentage,
24
+ getClosestValueIndex,
25
+ getDecimalCount,
26
+ getLabel,
27
+ getNextSortedValues,
28
+ getThumbInBoundsOffset,
29
+ hasMinStepsBetweenValues,
30
+ linearScale,
31
+ roundValue,
32
+ } from './helpers'
33
+ import { SliderFrame, SliderImpl } from './SliderImpl'
34
+ import {
35
+ ScopedProps,
36
+ SliderContextValue,
37
+ SliderHorizontalProps,
38
+ SliderImplElement,
39
+ SliderProps,
40
+ SliderTrackProps,
41
+ SliderVerticalProps,
42
+ } from './types'
43
+
44
+ /* -------------------------------------------------------------------------------------------------
45
+ * SliderHorizontal
46
+ * -----------------------------------------------------------------------------------------------*/
47
+
48
+ type SliderHorizontalElement = SliderImplElement
49
+
50
+ const SliderHorizontal = React.forwardRef<SliderHorizontalElement, SliderHorizontalProps>(
51
+ (props: ScopedProps<SliderHorizontalProps>, forwardedRef) => {
52
+ const { min, max, dir, onSlideStart, onSlideMove, onStepKeyDown, ...sliderProps } = props
53
+ const direction = useDirection(dir)
54
+ const isDirectionLTR = direction === 'ltr'
55
+ const layoutRef = React.useRef<LayoutRectangle | null>(null)
56
+ const [size, setSize] = React.useState(0)
57
+
58
+ function getValueFromPointer(pointerPosition: number) {
59
+ const layout = layoutRef.current
60
+ if (!layout) return
61
+ const input: [number, number] = [0, layout.width]
62
+ const output: [number, number] = isDirectionLTR ? [min, max] : [max, min]
63
+ const value = linearScale(input, output)
64
+ return value(pointerPosition)
65
+ }
66
+
67
+ return (
68
+ <SliderOrientationProvider
69
+ scope={props.__scopeSlider}
70
+ startEdge={isDirectionLTR ? 'left' : 'right'}
71
+ endEdge={isDirectionLTR ? 'right' : 'left'}
72
+ direction={isDirectionLTR ? 1 : -1}
73
+ sizeProp="width"
74
+ size={size}
75
+ >
76
+ <SliderImpl
77
+ ref={forwardedRef}
78
+ dir={direction}
79
+ {...sliderProps}
80
+ orientation="horizontal"
81
+ onLayout={(e) => {
82
+ const layout = e.nativeEvent.layout
83
+ layoutRef.current = layout
84
+ setSize(layout.height)
85
+ }}
86
+ onSlideStart={(event, target) => {
87
+ const value = getValueFromPointer(event.nativeEvent.locationX)
88
+ if (value) {
89
+ onSlideStart?.(value, target)
90
+ }
91
+ }}
92
+ onSlideMove={(event) => {
93
+ const value = getValueFromPointer(event.nativeEvent.locationX)
94
+ if (value) {
95
+ onSlideMove?.(value)
96
+ }
97
+ }}
98
+ onSlideEnd={() => {}}
99
+ onStepKeyDown={(event) => {
100
+ const isBackKey = BACK_KEYS[direction].includes(event.key)
101
+ onStepKeyDown?.({ event, direction: isBackKey ? -1 : 1 })
102
+ }}
103
+ />
104
+ </SliderOrientationProvider>
105
+ )
106
+ }
107
+ )
108
+
109
+ /* -------------------------------------------------------------------------------------------------
110
+ * SliderVertical
111
+ * -----------------------------------------------------------------------------------------------*/
112
+
113
+ type SliderVerticalElement = SliderImplElement
114
+
115
+ const SliderVertical = React.forwardRef<SliderVerticalElement, SliderVerticalProps>(
116
+ (props: ScopedProps<SliderVerticalProps>, forwardedRef) => {
117
+ const { min, max, onSlideStart, onSlideMove, onStepKeyDown, ...sliderProps } = props
118
+ const layoutRef = React.useRef<LayoutRectangle | null>(null)
119
+ const [size, setSize] = React.useState(0)
120
+
121
+ function getValueFromPointer(pointerPosition: number) {
122
+ const layout = layoutRef.current
123
+ if (!layout) return
124
+ const input: [number, number] = [0, layout.height]
125
+ const output: [number, number] = [max, min]
126
+ const value = linearScale(input, output)
127
+ return value(pointerPosition)
128
+ }
129
+
130
+ return (
131
+ <SliderOrientationProvider
132
+ scope={props.__scopeSlider}
133
+ startEdge="bottom"
134
+ endEdge="top"
135
+ sizeProp="height"
136
+ size={size}
137
+ direction={1}
138
+ >
139
+ <SliderImpl
140
+ ref={forwardedRef}
141
+ {...sliderProps}
142
+ orientation="vertical"
143
+ onLayout={(e) => {
144
+ const layout = e.nativeEvent.layout
145
+ layoutRef.current = layout
146
+ setSize(layout.height)
147
+ }}
148
+ onSlideStart={(event, target) => {
149
+ const value = getValueFromPointer(event.nativeEvent.locationY)
150
+ if (value) {
151
+ onSlideStart?.(value, target)
152
+ }
153
+ }}
154
+ onSlideMove={(event) => {
155
+ const value = getValueFromPointer(event.nativeEvent.locationY)
156
+ if (value) {
157
+ onSlideMove?.(value)
158
+ }
159
+ }}
160
+ onSlideEnd={() => {}}
161
+ onStepKeyDown={(event) => {
162
+ const isBackKey = BACK_KEYS.ltr.includes(event.key)
163
+ onStepKeyDown?.({ event, direction: isBackKey ? -1 : 1 })
164
+ }}
165
+ />
166
+ </SliderOrientationProvider>
167
+ )
168
+ }
169
+ )
170
+
171
+ /* -------------------------------------------------------------------------------------------------
172
+ * SliderTrack
173
+ * -----------------------------------------------------------------------------------------------*/
174
+
175
+ const TRACK_NAME = 'SliderTrack'
176
+
177
+ type SliderTrackElement = HTMLElement | View
178
+
179
+ const SliderTrackFrame = styled(SliderFrame, {
180
+ name: 'SliderTrack',
181
+ height: '100%',
182
+ width: '100%',
183
+ backgroundColor: '$background',
184
+ position: 'relative',
185
+ borderRadius: 100_000,
186
+ overflow: 'hidden',
187
+ })
188
+
189
+ const SliderTrack = React.forwardRef<SliderTrackElement, SliderTrackProps>(
190
+ (props: ScopedProps<SliderTrackProps>, forwardedRef) => {
191
+ const { __scopeSlider, ...trackProps } = props
192
+ const context = useSliderContext(TRACK_NAME, __scopeSlider)
193
+ return (
194
+ <SliderTrackFrame
195
+ data-disabled={context.disabled ? '' : undefined}
196
+ data-orientation={context.orientation}
197
+ orientation={context.orientation}
198
+ size={context.size}
199
+ {...trackProps}
200
+ ref={forwardedRef}
201
+ />
202
+ )
203
+ }
204
+ )
205
+
206
+ SliderTrack.displayName = TRACK_NAME
207
+
208
+ /* -------------------------------------------------------------------------------------------------
209
+ * SliderTrackActive
210
+ * -----------------------------------------------------------------------------------------------*/
211
+
212
+ const RANGE_NAME = 'SliderTrackActive'
213
+
214
+ type SliderTrackActiveElement = HTMLElement | View
215
+ interface SliderTrackActiveProps extends YStackProps {}
216
+
217
+ const SliderTrackActiveFrame = styled(SliderFrame, {
218
+ name: 'SliderTrackActive',
219
+ backgroundColor: '$background',
220
+ position: 'absolute',
221
+ })
222
+
223
+ const SliderTrackActive = React.forwardRef<SliderTrackActiveElement, SliderTrackActiveProps>(
224
+ (props: ScopedProps<SliderTrackActiveProps>, forwardedRef) => {
225
+ const { __scopeSlider, ...rangeProps } = props
226
+ const context = useSliderContext(RANGE_NAME, __scopeSlider)
227
+ const orientation = useSliderOrientationContext(RANGE_NAME, __scopeSlider)
228
+ const ref = React.useRef<HTMLSpanElement>(null)
229
+ const composedRefs = useComposedRefs(forwardedRef, ref)
230
+ const valuesCount = context.values.length
231
+ const percentages = context.values.map((value) =>
232
+ convertValueToPercentage(value, context.min, context.max)
233
+ )
234
+ const offsetStart = valuesCount > 1 ? Math.min(...percentages) : 0
235
+ const offsetEnd = 100 - Math.max(...percentages)
236
+
237
+ return (
238
+ <SliderTrackActiveFrame
239
+ orientation={context.orientation}
240
+ data-orientation={context.orientation}
241
+ data-disabled={context.disabled ? '' : undefined}
242
+ size={context.size}
243
+ {...rangeProps}
244
+ ref={composedRefs}
245
+ {...{
246
+ [orientation.startEdge]: offsetStart + '%',
247
+ [orientation.endEdge]: offsetEnd + '%',
248
+ }}
249
+ {...(orientation.sizeProp === 'width'
250
+ ? {
251
+ height: '100%',
252
+ }
253
+ : {
254
+ left: 0,
255
+ right: 0,
256
+ })}
257
+ />
258
+ )
259
+ }
260
+ )
261
+
262
+ SliderTrackActive.displayName = RANGE_NAME
263
+
264
+ /* -------------------------------------------------------------------------------------------------
265
+ * SliderThumb
266
+ * -----------------------------------------------------------------------------------------------*/
267
+
268
+ const THUMB_NAME = 'SliderThumb'
269
+
270
+ const SliderThumbFrame = styled(ThemeableStack, {
271
+ name: 'SliderThumb',
272
+ position: 'absolute',
273
+ // TODO not taking up 2
274
+ bordered: 2,
275
+ // OR THIS
276
+ borderWidth: 2,
277
+ backgrounded: true,
278
+ pressable: true,
279
+ focusable: true,
280
+ hoverable: true,
281
+
282
+ variants: {
283
+ size: {
284
+ '...size': (val) => {
285
+ const size = typeof val === 'number' ? val : getSize(val, -1)
286
+ return {
287
+ width: size,
288
+ height: size,
289
+ minWidth: size,
290
+ minHeight: size,
291
+ }
292
+ },
293
+ },
294
+ },
295
+ })
296
+
297
+ type SliderThumbElement = HTMLElement | View
298
+ interface SliderThumbProps extends SizableStackProps {
299
+ index: number
300
+ }
301
+
302
+ const SliderThumb = React.forwardRef<SliderThumbElement, SliderThumbProps>(
303
+ (props: ScopedProps<SliderThumbProps>, forwardedRef) => {
304
+ const { __scopeSlider, index, size: sizeProp, ...thumbProps } = props
305
+ const context = useSliderContext(THUMB_NAME, __scopeSlider)
306
+ const orientation = useSliderOrientationContext(THUMB_NAME, __scopeSlider)
307
+ const [thumb, setThumb] = React.useState<View | HTMLElement | null>(null)
308
+ const composedRefs = useComposedRefs(forwardedRef, (node) => setThumb(node))
309
+
310
+ // We cast because index could be `-1` which would return undefined
311
+ const value = context.values[index] as number | undefined
312
+ const percent =
313
+ value === undefined ? 0 : convertValueToPercentage(value, context.min, context.max)
314
+ const label = getLabel(index, context.values.length)
315
+ const [size, setSize] = React.useState(0)
316
+
317
+ const thumbInBoundsOffset = size
318
+ ? getThumbInBoundsOffset(size, percent, orientation.direction)
319
+ : 0
320
+
321
+ React.useEffect(() => {
322
+ if (thumb) {
323
+ context.thumbs.add(thumb)
324
+ return () => {
325
+ context.thumbs.delete(thumb)
326
+ }
327
+ }
328
+ }, [thumb, context.thumbs])
329
+
330
+ return (
331
+ <SliderThumbFrame
332
+ ref={composedRefs}
333
+ // TODO
334
+ // @ts-ignore
335
+ role="slider"
336
+ aria-label={props['aria-label'] || label}
337
+ aria-valuemin={context.min}
338
+ aria-valuenow={value}
339
+ aria-valuemax={context.max}
340
+ aria-orientation={context.orientation}
341
+ data-orientation={context.orientation}
342
+ data-disabled={context.disabled ? '' : undefined}
343
+ // TODO
344
+ // @ts-ignore
345
+ tabIndex={context.disabled ? undefined : 0}
346
+ {...thumbProps}
347
+ {...(context.orientation === 'horizontal'
348
+ ? {
349
+ x: thumbInBoundsOffset - size / 2,
350
+ y: -size / 2,
351
+ top: '50%',
352
+ ...(size === 0 && {
353
+ top: 'auto',
354
+ bottom: 'auto',
355
+ }),
356
+ }
357
+ : {
358
+ x: -size / 2,
359
+ y: size / 2,
360
+ left: '50%',
361
+ ...(size === 0 && {
362
+ left: 'auto',
363
+ right: 'auto',
364
+ }),
365
+ })}
366
+ size={sizeProp ?? context.size ?? '$4'}
367
+ onLayout={(e) => {
368
+ setSize(e.nativeEvent.layout[orientation.sizeProp])
369
+ }}
370
+ {...{
371
+ [orientation.startEdge]: `${percent}%`,
372
+ }}
373
+ /**
374
+ * There will be no value on initial render while we work out the index so we hide thumbs
375
+ * without a value, otherwise SSR will render them in the wrong position before they
376
+ * snap into the correct position during hydration which would be visually jarring for
377
+ * slower connections.
378
+ */
379
+ // style={value === undefined ? { display: 'none' } : props.style}
380
+ onFocus={composeEventHandlers(props.onFocus, () => {
381
+ context.valueIndexToChangeRef.current = index
382
+ })}
383
+ />
384
+ )
385
+ }
386
+ )
387
+
388
+ SliderThumb.displayName = THUMB_NAME
389
+
390
+ /* -------------------------------------------------------------------------------------------------
391
+ * Slider
392
+ * -----------------------------------------------------------------------------------------------*/
393
+
394
+ type SliderElement = SliderHorizontalElement | SliderVerticalElement
395
+
396
+ const Slider = withStaticProperties(
397
+ React.forwardRef<SliderElement, SliderProps>((props: ScopedProps<SliderProps>, forwardedRef) => {
398
+ const {
399
+ name,
400
+ min = 0,
401
+ max = 100,
402
+ step = 1,
403
+ orientation = 'horizontal',
404
+ disabled = false,
405
+ minStepsBetweenThumbs = 0,
406
+ defaultValue = [min],
407
+ value,
408
+ onValueChange = () => {},
409
+ size: sizeProp,
410
+ ...sliderProps
411
+ } = props
412
+ const sliderRef = React.useRef<SliderImplElement>(null)
413
+ const composedRefs = useComposedRefs(sliderRef, forwardedRef)
414
+ const thumbRefs = React.useRef<SliderContextValue['thumbs']>(new Set())
415
+ const valueIndexToChangeRef = React.useRef<number>(0)
416
+ const isHorizontal = orientation === 'horizontal'
417
+ // We set this to true by default so that events bubble to forms without JS (SSR)
418
+ // TODO
419
+ // const isFormControl = slider ? Boolean(slider.closest('form')) : true
420
+
421
+ const [values = [], setValues] = useControllableState({
422
+ prop: value,
423
+ defaultProp: defaultValue,
424
+ onChange: (value) => {
425
+ if (isWeb) {
426
+ const thumbs = [...thumbRefs.current]
427
+ thumbs[valueIndexToChangeRef.current]?.focus()
428
+ }
429
+ onValueChange(value)
430
+ },
431
+ })
432
+
433
+ if (isWeb) {
434
+ React.useEffect(() => {
435
+ const node = sliderRef.current as HTMLElement
436
+ if (!node) return
437
+ const preventDefault = (e) => {
438
+ e.preventDefault()
439
+ }
440
+ node.addEventListener('touchstart', preventDefault)
441
+ return () => {
442
+ node.removeEventListener('touchstart', preventDefault)
443
+ }
444
+ }, [])
445
+ }
446
+
447
+ function handleSlideMove(value: number) {
448
+ updateValues(value, valueIndexToChangeRef.current)
449
+ }
450
+
451
+ function updateValues(value: number, atIndex: number) {
452
+ const decimalCount = getDecimalCount(step)
453
+ const snapToStep = roundValue(Math.round((value - min) / step) * step + min, decimalCount)
454
+ const nextValue = clamp(snapToStep, [min, max])
455
+ setValues((prevValues = []) => {
456
+ const nextValues = getNextSortedValues(prevValues, nextValue, atIndex)
457
+ if (hasMinStepsBetweenValues(nextValues, minStepsBetweenThumbs * step)) {
458
+ valueIndexToChangeRef.current = nextValues.indexOf(nextValue)
459
+ return String(nextValues) === String(prevValues) ? prevValues : nextValues
460
+ } else {
461
+ return prevValues
462
+ }
463
+ })
464
+ }
465
+
466
+ const SliderOriented = isHorizontal ? SliderHorizontal : SliderVertical
467
+
468
+ return (
469
+ <SliderProvider
470
+ scope={props.__scopeSlider}
471
+ disabled={disabled}
472
+ min={min}
473
+ max={max}
474
+ valueIndexToChangeRef={valueIndexToChangeRef}
475
+ thumbs={thumbRefs.current}
476
+ values={values}
477
+ orientation={orientation}
478
+ size={sizeProp}
479
+ >
480
+ <SliderOriented
481
+ aria-disabled={disabled}
482
+ data-disabled={disabled ? '' : undefined}
483
+ {...sliderProps}
484
+ ref={composedRefs}
485
+ min={min}
486
+ max={max}
487
+ onSlideStart={
488
+ disabled
489
+ ? undefined
490
+ : (value: number, target) => {
491
+ // when starting on the track, move it right away
492
+ // when starting on thumb, dont jump until movemenet as it feels weird
493
+ if (target !== 'thumb') {
494
+ const closestIndex = getClosestValueIndex(values, value)
495
+ updateValues(value, closestIndex)
496
+ }
497
+ }
498
+ }
499
+ onSlideMove={disabled ? undefined : handleSlideMove}
500
+ onHomeKeyDown={() => !disabled && updateValues(min, 0)}
501
+ onEndKeyDown={() => !disabled && updateValues(max, values.length - 1)}
502
+ onStepKeyDown={({ event, direction: stepDirection }) => {
503
+ if (!disabled) {
504
+ const isPageKey = PAGE_KEYS.includes(event.key)
505
+ const isSkipKey = isPageKey || (event.shiftKey && ARROW_KEYS.includes(event.key))
506
+ const multiplier = isSkipKey ? 10 : 1
507
+ const atIndex = valueIndexToChangeRef.current
508
+ const value = values[atIndex]
509
+ const stepInDirection = step * multiplier * stepDirection
510
+ updateValues(value + stepInDirection, atIndex)
511
+ }
512
+ }}
513
+ />
514
+ {/* {isFormControl &&
515
+ values.map((value, index) => (
516
+ <BubbleInput
517
+ key={index}
518
+ name={name ? name + (values.length > 1 ? '[]' : '') : undefined}
519
+ value={value}
520
+ />
521
+ ))} */}
522
+ </SliderProvider>
523
+ )
524
+ }),
525
+ {
526
+ Track: SliderTrack,
527
+ TrackActive: SliderTrackActive,
528
+ Thumb: SliderThumb,
529
+ }
530
+ )
531
+
532
+ Slider.displayName = SLIDER_NAME
533
+
534
+ /* -----------------------------------------------------------------------------------------------*/
535
+
536
+ // TODO
537
+ // const BubbleInput = (props: any) => {
538
+ // const { value, ...inputProps } = props
539
+ // const ref = React.useRef<HTMLInputElement>(null)
540
+ // const prevValue = usePrevious(value)
541
+
542
+ // // Bubble value change to parents (e.g form change event)
543
+ // React.useEffect(() => {
544
+ // const input = ref.current!
545
+ // const inputProto = window.HTMLInputElement.prototype
546
+ // const descriptor = Object.getOwnPropertyDescriptor(inputProto, 'value') as PropertyDescriptor
547
+ // const setValue = descriptor.set
548
+ // if (prevValue !== value && setValue) {
549
+ // const event = new Event('input', { bubbles: true })
550
+ // setValue.call(input, value)
551
+ // input.dispatchEvent(event)
552
+ // }
553
+ // }, [prevValue, value])
554
+
555
+ // /**
556
+ // * We purposefully do not use `type="hidden"` here otherwise forms that
557
+ // * wrap it will not be able to access its value via the FormData API.
558
+ // *
559
+ // * We purposefully do not add the `value` attribute here to allow the value
560
+ // * to be set programatically and bubble to any parent form `onChange` event.
561
+ // * Adding the `value` will cause React to consider the programatic
562
+ // * dispatch a duplicate and it will get swallowed.
563
+ // */
564
+ // return <input style={{ display: 'none' }} {...inputProps} ref={ref} defaultValue={value} />
565
+ // }
566
+
567
+ /* -----------------------------------------------------------------------------------------------*/
568
+
569
+ const Track = SliderTrack
570
+ const Range = SliderTrackActive
571
+ const Thumb = SliderThumb
572
+
573
+ export {
574
+ Slider,
575
+ SliderTrack,
576
+ SliderTrackActive,
577
+ SliderThumb,
578
+ //
579
+ Track,
580
+ Range,
581
+ Thumb,
582
+ }
583
+
584
+ export type { SliderProps, SliderTrackProps, SliderTrackActiveProps, SliderThumbProps }
@@ -0,0 +1,105 @@
1
+ /* -------------------------------------------------------------------------------------------------
2
+ * SliderImpl
3
+ * -----------------------------------------------------------------------------------------------*/
4
+
5
+ import { composeEventHandlers, getSize, getVariableValue, isWeb, styled } from '@tamagui/core'
6
+ import { YStack } from '@tamagui/stacks'
7
+ import * as React from 'react'
8
+
9
+ import { ARROW_KEYS, PAGE_KEYS, SLIDER_NAME, useSliderContext } from './constants'
10
+ import { ScopedProps, SliderImplElement, SliderImplProps } from './types'
11
+
12
+ export const DirectionalYStack = styled(YStack, {
13
+ variants: {
14
+ orientation: {
15
+ horizontal: {},
16
+ vertical: {},
17
+ },
18
+ },
19
+ })
20
+
21
+ export const SliderFrame = styled(DirectionalYStack, {
22
+ position: 'relative',
23
+
24
+ variants: {
25
+ size: (val, extras) => {
26
+ const orientation = extras.props.orientation
27
+ const size = Math.round(getVariableValue(getSize(val)) / 6)
28
+ if (orientation === 'horizontal') {
29
+ return {
30
+ height: size,
31
+ borderRadius: size,
32
+ justifyContent: 'center',
33
+ }
34
+ }
35
+ return {
36
+ width: size,
37
+ borderRadius: size,
38
+ alignItems: 'center',
39
+ }
40
+ },
41
+ },
42
+ })
43
+
44
+ export const SliderImpl = React.forwardRef<SliderImplElement, SliderImplProps>(
45
+ (props: ScopedProps<SliderImplProps>, forwardedRef) => {
46
+ const {
47
+ __scopeSlider,
48
+ onSlideStart,
49
+ onSlideMove,
50
+ onSlideEnd,
51
+ onHomeKeyDown,
52
+ onEndKeyDown,
53
+ onStepKeyDown,
54
+ ...sliderProps
55
+ } = props
56
+ const context = useSliderContext(SLIDER_NAME, __scopeSlider)
57
+ return (
58
+ <SliderFrame
59
+ size="$4"
60
+ {...sliderProps}
61
+ data-orientation={sliderProps.orientation}
62
+ ref={forwardedRef}
63
+ {...(isWeb && {
64
+ onKeyDown: (event) => {
65
+ if (event.key === 'Home') {
66
+ onHomeKeyDown(event)
67
+ // Prevent scrolling to page start
68
+ event.preventDefault()
69
+ } else if (event.key === 'End') {
70
+ onEndKeyDown(event)
71
+ // Prevent scrolling to page end
72
+ event.preventDefault()
73
+ } else if (PAGE_KEYS.concat(ARROW_KEYS).includes(event.key)) {
74
+ onStepKeyDown(event)
75
+ // Prevent scrolling for directional key presses
76
+ event.preventDefault()
77
+ }
78
+ },
79
+ })}
80
+ onStartShouldSetResponder={() => true}
81
+ onResponderGrant={composeEventHandlers(props.onResponderGrant, (event) => {
82
+ const target = event.target as HTMLElement | number
83
+ const isStartingOnThumb = context.thumbs.has(event.target)
84
+ // // Prevent browser focus behaviour because we focus a thumb manually when values change.
85
+ // Touch devices have a delay before focusing so won't focus if touch immediately moves
86
+ // away from target (sliding). We want thumb to focus regardless.
87
+ if (isWeb && target instanceof HTMLElement) {
88
+ if (context.thumbs.has(target)) {
89
+ target.focus()
90
+ }
91
+ }
92
+ onSlideStart(event, isStartingOnThumb ? 'thumb' : 'track')
93
+ })}
94
+ onResponderMove={composeEventHandlers(props.onResponderMove, (event) => {
95
+ // const target = event.target as HTMLElement
96
+ onSlideMove(event)
97
+ })}
98
+ onResponderRelease={composeEventHandlers(props.onResponderRelease, (event) => {
99
+ // const target = event.target as HTMLElement
100
+ onSlideEnd(event)
101
+ })}
102
+ />
103
+ )
104
+ }
105
+ )