@tamagui/slider 1.0.1-beta.100

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