@cdx-ui/primitives 0.0.1-beta.170 → 0.0.1-beta.171

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 (49) hide show
  1. package/lib/commonjs/slider/SliderRail.js +35 -9
  2. package/lib/commonjs/slider/SliderRail.js.map +1 -1
  3. package/lib/commonjs/slider/SliderRoot.js +155 -97
  4. package/lib/commonjs/slider/SliderRoot.js.map +1 -1
  5. package/lib/commonjs/slider/SliderThumb.js +182 -68
  6. package/lib/commonjs/slider/SliderThumb.js.map +1 -1
  7. package/lib/commonjs/slider/context.js +4 -1
  8. package/lib/commonjs/slider/context.js.map +1 -1
  9. package/lib/commonjs/slider/index.js +36 -0
  10. package/lib/commonjs/slider/index.js.map +1 -1
  11. package/lib/commonjs/slider/useSliderPointer.js +1 -1
  12. package/lib/commonjs/slider/useSliderPointer.js.map +1 -1
  13. package/lib/commonjs/slider/utils.js +68 -0
  14. package/lib/commonjs/slider/utils.js.map +1 -1
  15. package/lib/module/slider/SliderRail.js +35 -9
  16. package/lib/module/slider/SliderRail.js.map +1 -1
  17. package/lib/module/slider/SliderRoot.js +157 -99
  18. package/lib/module/slider/SliderRoot.js.map +1 -1
  19. package/lib/module/slider/SliderThumb.js +185 -71
  20. package/lib/module/slider/SliderThumb.js.map +1 -1
  21. package/lib/module/slider/context.js +1 -0
  22. package/lib/module/slider/context.js.map +1 -1
  23. package/lib/module/slider/index.js +2 -2
  24. package/lib/module/slider/index.js.map +1 -1
  25. package/lib/module/slider/useSliderPointer.js +1 -1
  26. package/lib/module/slider/useSliderPointer.js.map +1 -1
  27. package/lib/module/slider/utils.js +63 -0
  28. package/lib/module/slider/utils.js.map +1 -1
  29. package/lib/typescript/slider/SliderRail.d.ts.map +1 -1
  30. package/lib/typescript/slider/SliderRoot.d.ts +2 -2
  31. package/lib/typescript/slider/SliderRoot.d.ts.map +1 -1
  32. package/lib/typescript/slider/SliderThumb.d.ts.map +1 -1
  33. package/lib/typescript/slider/context.d.ts +53 -9
  34. package/lib/typescript/slider/context.d.ts.map +1 -1
  35. package/lib/typescript/slider/index.d.ts +4 -4
  36. package/lib/typescript/slider/index.d.ts.map +1 -1
  37. package/lib/typescript/slider/types.d.ts +44 -8
  38. package/lib/typescript/slider/types.d.ts.map +1 -1
  39. package/lib/typescript/slider/utils.d.ts +8 -0
  40. package/lib/typescript/slider/utils.d.ts.map +1 -1
  41. package/package.json +3 -3
  42. package/src/slider/SliderRail.tsx +56 -8
  43. package/src/slider/SliderRoot.tsx +255 -136
  44. package/src/slider/SliderThumb.tsx +240 -88
  45. package/src/slider/context.tsx +53 -9
  46. package/src/slider/index.ts +9 -2
  47. package/src/slider/types.ts +49 -8
  48. package/src/slider/useSliderPointer.ts +1 -1
  49. package/src/slider/utils.ts +76 -0
@@ -1,9 +1,10 @@
1
- import { forwardRef, useCallback, useMemo, useRef, type ComponentRef } from 'react';
1
+ import { forwardRef, useCallback, useEffect, useMemo, useRef, type ComponentRef } from 'react';
2
2
  import {
3
3
  PanResponder,
4
4
  Platform,
5
5
  Pressable,
6
6
  View,
7
+ type AccessibilityActionEvent,
7
8
  type GestureResponderEvent,
8
9
  type LayoutChangeEvent,
9
10
  type PressableProps,
@@ -15,10 +16,32 @@ import { useHover, usePress } from '@react-native-aria/interactions';
15
16
  import { Slot } from '../slot';
16
17
  import { assignRef } from '../utils/assignRef';
17
18
  import { dataAttributes } from '../utils/dataAttributes';
18
- import { useSliderContext } from './context';
19
+ import { SliderThumbProvider, useSliderContext } from './context';
19
20
  import type { InterfaceSliderThumbProps } from './types';
20
21
  import { getPointerCoords, useWebSliderDrag } from './useSliderPointer';
21
- import { clamp, getThumbTravel, percentToThumbOffset } from './utils';
22
+ import {
23
+ clamp,
24
+ formatSliderValue,
25
+ getThumbTravel,
26
+ percentToThumbOffset,
27
+ valueToPercent,
28
+ } from './utils';
29
+
30
+ const STEP_KEYS = new Set(['ArrowUp', 'ArrowRight', 'ArrowDown', 'ArrowLeft', 'Home', 'End']);
31
+
32
+ function getDefaultThumbAccessibilityLabel(
33
+ index: number,
34
+ thumbCount: number,
35
+ groupAccessibilityLabel?: string,
36
+ ): string | undefined {
37
+ if (thumbCount === 2) {
38
+ return index === 0 ? 'Minimum' : 'Maximum';
39
+ }
40
+ if (thumbCount > 2) {
41
+ return `Value ${String(index + 1)} of ${String(thumbCount)}`;
42
+ }
43
+ return groupAccessibilityLabel;
44
+ }
22
45
 
23
46
  export interface SliderThumbProps extends InterfaceSliderThumbProps {}
24
47
 
@@ -26,7 +49,10 @@ export const SliderThumb = forwardRef<ComponentRef<typeof Pressable>, SliderThum
26
49
  (
27
50
  {
28
51
  asChild,
52
+ index = 0,
29
53
  style,
54
+ accessibilityLabel: accessibilityLabelProp,
55
+ accessibilityValue: accessibilityValueProp,
30
56
  onLayout,
31
57
  onHoverIn,
32
58
  onHoverOut,
@@ -40,32 +66,52 @@ export const SliderThumb = forwardRef<ComponentRef<typeof Pressable>, SliderThum
40
66
  ref,
41
67
  ) => {
42
68
  const {
43
- percent,
69
+ values,
70
+ min,
71
+ max,
72
+ step,
44
73
  orientation,
45
74
  isDisabled,
46
- isDragging,
75
+ thumbCount,
76
+ groupAccessibilityLabel,
77
+ accessibilityValueProp: rootAccessibilityValueProp,
47
78
  trackLayout,
48
79
  thumbSize,
49
- setValueFromRatio,
50
- setIsDragging,
51
- setIsHovered,
52
- setIsFocused,
80
+ getThumbPercent,
81
+ getThumbMinValue,
82
+ getThumbMaxValue,
83
+ setThumbValue,
84
+ setThumbValueFromRatio,
85
+ setDraggingIndex,
86
+ setHoveredIndex,
87
+ setFocusedIndex,
53
88
  setThumbSize,
54
- thumbRef,
89
+ registerThumb,
90
+ isThumbDragging,
91
+ thumbRef: deprecatedThumbRef,
55
92
  } = useSliderContext();
56
93
 
94
+ const value = values[index] ?? min;
95
+ const percent = getThumbPercent(index);
96
+ const isDragging = isThumbDragging(index);
97
+ const thumbRef = useRef<View>(null);
98
+
99
+ useEffect(() => {
100
+ return registerThumb(index);
101
+ }, [index, registerThumb]);
102
+
103
+ useEffect(() => {
104
+ if (index === 0) {
105
+ deprecatedThumbRef.current = thumbRef.current;
106
+ }
107
+ });
108
+
57
109
  const percentRef = useRef(percent);
58
110
  percentRef.current = percent;
59
111
 
60
112
  const dragStartRatio = useRef(0);
61
113
  const lastRatioRef = useRef(percent / 100);
62
114
 
63
- // The native PanResponder is created once (stable across renders) so an
64
- // in-flight gesture is never handed a fresh internal `gestureState`
65
- // mid-drag. In controlled mode `setValueFromRatio`/`trackLayout` change
66
- // identity on every value update, so the handlers read the current values
67
- // through refs instead of closing over them (which would force the
68
- // `useMemo` to re-create the responder on every move and reset `dx`).
69
115
  const orientationRef = useRef(orientation);
70
116
  orientationRef.current = orientation;
71
117
  const trackLayoutRef = useRef(trackLayout);
@@ -74,10 +120,12 @@ export const SliderThumb = forwardRef<ComponentRef<typeof Pressable>, SliderThum
74
120
  thumbSizeRef.current = thumbSize;
75
121
  const isDisabledRef = useRef(isDisabled);
76
122
  isDisabledRef.current = isDisabled;
77
- const setValueFromRatioRef = useRef(setValueFromRatio);
78
- setValueFromRatioRef.current = setValueFromRatio;
79
- const setIsDraggingRef = useRef(setIsDragging);
80
- setIsDraggingRef.current = setIsDragging;
123
+ const indexRef = useRef(index);
124
+ indexRef.current = index;
125
+ const setThumbValueFromRatioRef = useRef(setThumbValueFromRatio);
126
+ setThumbValueFromRatioRef.current = setThumbValueFromRatio;
127
+ const setDraggingIndexRef = useRef(setDraggingIndex);
128
+ setDraggingIndexRef.current = setDraggingIndex;
81
129
 
82
130
  const { hoverProps, isHovered } = useHover();
83
131
  const { focusProps, isFocused } = useFocus();
@@ -90,10 +138,24 @@ export const SliderThumb = forwardRef<ComponentRef<typeof Pressable>, SliderThum
90
138
  pressProps: Pick<PressableProps, 'onPressIn' | 'onPressOut'>;
91
139
  };
92
140
 
141
+ const setRatioForThumb = useCallback(
142
+ (ratio: number, complete?: boolean) => {
143
+ setThumbValueFromRatio(index, ratio, complete);
144
+ },
145
+ [index, setThumbValueFromRatio],
146
+ );
147
+
148
+ const setDraggingForThumb = useCallback(
149
+ (dragging: boolean) => {
150
+ setDraggingIndex(dragging ? index : null);
151
+ },
152
+ [index, setDraggingIndex],
153
+ );
154
+
93
155
  const { startDrag } = useWebSliderDrag({
94
156
  isDisabled,
95
- setIsDragging,
96
- setValueFromRatio,
157
+ setIsDragging: setDraggingForThumb,
158
+ setValueFromRatio: setRatioForThumb,
97
159
  trackLayout,
98
160
  orientation,
99
161
  thumbSize,
@@ -108,7 +170,7 @@ export const SliderThumb = forwardRef<ComponentRef<typeof Pressable>, SliderThum
108
170
  if (isDisabledRef.current) {
109
171
  return;
110
172
  }
111
- setIsDraggingRef.current(true);
173
+ setDraggingIndexRef.current(indexRef.current);
112
174
  dragStartRatio.current = percentRef.current / 100;
113
175
  lastRatioRef.current = dragStartRatio.current;
114
176
  },
@@ -126,15 +188,15 @@ export const SliderThumb = forwardRef<ComponentRef<typeof Pressable>, SliderThum
126
188
  const delta = isHorizontal ? gestureState.dx : -gestureState.dy;
127
189
  const nextRatio = clamp(dragStartRatio.current + delta / travel, 0, 1);
128
190
  lastRatioRef.current = nextRatio;
129
- setValueFromRatioRef.current(nextRatio);
191
+ setThumbValueFromRatioRef.current(indexRef.current, nextRatio);
130
192
  },
131
193
  onPanResponderRelease: () => {
132
- setIsDraggingRef.current(false);
133
- setValueFromRatioRef.current(lastRatioRef.current, true);
194
+ setDraggingIndexRef.current(null);
195
+ setThumbValueFromRatioRef.current(indexRef.current, lastRatioRef.current, true);
134
196
  },
135
197
  onPanResponderTerminate: () => {
136
- setIsDraggingRef.current(false);
137
- setValueFromRatioRef.current(lastRatioRef.current, true);
198
+ setDraggingIndexRef.current(null);
199
+ setThumbValueFromRatioRef.current(indexRef.current, lastRatioRef.current, true);
138
200
  },
139
201
  }),
140
202
  [],
@@ -149,13 +211,15 @@ export const SliderThumb = forwardRef<ComponentRef<typeof Pressable>, SliderThum
149
211
  : trackLayout.height;
150
212
  const offset = percentToThumbOffset(percent, trackSize, thumbSize);
151
213
 
214
+ const zIndex = isDragging ? 3 : 2;
215
+
152
216
  if (orientation === 'vertical') {
153
217
  return {
154
218
  position: 'absolute',
155
219
  left: '50%',
156
220
  bottom: offset,
157
221
  marginLeft: -thumbSize / 2,
158
- zIndex: 2,
222
+ zIndex,
159
223
  };
160
224
  }
161
225
 
@@ -164,9 +228,9 @@ export const SliderThumb = forwardRef<ComponentRef<typeof Pressable>, SliderThum
164
228
  top: '50%',
165
229
  left: offset,
166
230
  transform: [{ translateY: -thumbSize / 2 }],
167
- zIndex: 2,
231
+ zIndex,
168
232
  };
169
- }, [orientation, percent, thumbSize, trackLayout]);
233
+ }, [isDragging, orientation, percent, thumbSize, trackLayout]);
170
234
 
171
235
  const handleLayout = useCallback(
172
236
  (event: LayoutChangeEvent) => {
@@ -175,9 +239,7 @@ export const SliderThumb = forwardRef<ComponentRef<typeof Pressable>, SliderThum
175
239
  if (measured > 0) {
176
240
  setThumbSize(measured);
177
241
  }
178
- if (onLayout) {
179
- onLayout(event);
180
- }
242
+ onLayout?.(event);
181
243
  },
182
244
  [onLayout, orientation, setThumbSize],
183
245
  );
@@ -195,72 +257,162 @@ export const SliderThumb = forwardRef<ComponentRef<typeof Pressable>, SliderThum
195
257
  [isDisabled, startDrag],
196
258
  );
197
259
 
198
- const isWeb = Platform.OS === 'web';
260
+ const thumbMin = getThumbMinValue(index);
261
+ const thumbMax = getThumbMaxValue(index);
262
+
263
+ const accessibilityValue = useMemo(() => {
264
+ const thumbPercent = valueToPercent(value, min, max);
265
+ const defaults = {
266
+ min: 0,
267
+ max: 100,
268
+ now: Math.round(thumbPercent),
269
+ text: formatSliderValue(value),
270
+ };
271
+
272
+ const mergedRoot = rootAccessibilityValueProp
273
+ ? { ...defaults, ...rootAccessibilityValueProp }
274
+ : defaults;
275
+ if (accessibilityValueProp === undefined) {
276
+ return mergedRoot;
277
+ }
278
+ return { ...mergedRoot, ...accessibilityValueProp };
279
+ }, [accessibilityValueProp, max, min, rootAccessibilityValueProp, value]);
280
+
281
+ const resolvedAccessibilityLabel =
282
+ accessibilityLabelProp ??
283
+ getDefaultThumbAccessibilityLabel(index, thumbCount, groupAccessibilityLabel);
199
284
 
200
- // Native drag is driven by PanResponder. Its `panHandlers` set the same
201
- // responder props (`onResponderGrant/Move/Release`, etc.) that RN's
202
- // `Pressable`/`Pressability` also set internally — and `Pressable` spreads
203
- // its own handlers last, silently clobbering ours. So on native the thumb
204
- // must be a plain `View` that carries `panHandlers` directly. Web keeps
205
- // `Pressable` for its pointer-event, hover, and focus-ring behavior.
285
+ const handleKeyDown = useCallback(
286
+ (event: { nativeEvent: { key?: string }; preventDefault?: () => void }) => {
287
+ if (isDisabled) {
288
+ return;
289
+ }
290
+ const key = event.nativeEvent.key;
291
+ if (!key || !STEP_KEYS.has(key)) {
292
+ return;
293
+ }
294
+ if (Platform.OS === 'web') {
295
+ event.preventDefault?.();
296
+ }
297
+
298
+ let next = value;
299
+ if (key === 'ArrowUp' || key === 'ArrowRight') {
300
+ next = value + step;
301
+ } else if (key === 'ArrowDown' || key === 'ArrowLeft') {
302
+ next = value - step;
303
+ } else if (key === 'Home') {
304
+ next = thumbMin;
305
+ } else if (key === 'End') {
306
+ next = thumbMax;
307
+ }
308
+
309
+ setThumbValue(index, next, true);
310
+ },
311
+ [index, isDisabled, setThumbValue, step, thumbMax, thumbMin, value],
312
+ );
313
+
314
+ const handleAccessibilityAction = useCallback(
315
+ (event: AccessibilityActionEvent) => {
316
+ if (isDisabled) {
317
+ return;
318
+ }
319
+ if (event.nativeEvent.actionName === 'increment') {
320
+ setThumbValue(index, value + step, true);
321
+ } else if (event.nativeEvent.actionName === 'decrement') {
322
+ setThumbValue(index, value - step, true);
323
+ }
324
+ },
325
+ [index, isDisabled, setThumbValue, step, value],
326
+ );
327
+
328
+ const thumbContextValue = useMemo(
329
+ () => ({
330
+ index,
331
+ value,
332
+ percent,
333
+ isDragging,
334
+ isHovered,
335
+ isFocused,
336
+ thumbRef,
337
+ }),
338
+ [index, isDragging, isFocused, isHovered, percent, value],
339
+ );
340
+
341
+ const isWeb = Platform.OS === 'web';
206
342
  const Comp = asChild ? Slot : isWeb ? Pressable : View;
207
343
 
208
344
  const setThumbRef = useCallback(
209
345
  (node: ComponentRef<typeof Pressable> | null) => {
210
346
  thumbRef.current = node;
347
+ if (index === 0) {
348
+ deprecatedThumbRef.current = node;
349
+ }
211
350
  assignRef(ref, node);
212
351
  },
213
- [ref, thumbRef],
352
+ [deprecatedThumbRef, index, ref],
214
353
  );
215
354
 
216
- const webInteractionProps: PressableProps = {
217
- onPointerDown: handlePointerDown as unknown as PressableProps['onPointerDown'],
218
- onHoverIn: composeEventHandlers(
219
- onHoverIn,
220
- composeEventHandlers(hoverProps.onHoverIn, () => setIsHovered(true)),
221
- ),
222
- onHoverOut: composeEventHandlers(
223
- onHoverOut,
224
- composeEventHandlers(hoverProps.onHoverOut, () => setIsHovered(false)),
225
- ),
226
- onPressIn: composeEventHandlers(onPressIn, pressableProps.onPressIn),
227
- onPressOut: composeEventHandlers(onPressOut, pressableProps.onPressOut),
228
- disabled: isDisabled,
229
- tabIndex: -1,
230
- };
355
+ const webInteractionProps =
356
+ Platform.OS === 'web'
357
+ ? ({
358
+ onPointerDown: handlePointerDown as unknown as PressableProps['onPointerDown'],
359
+ onHoverIn: composeEventHandlers(
360
+ onHoverIn,
361
+ composeEventHandlers(hoverProps.onHoverIn, () => setHoveredIndex(index)),
362
+ ),
363
+ onHoverOut: composeEventHandlers(
364
+ onHoverOut,
365
+ composeEventHandlers(hoverProps.onHoverOut, () => setHoveredIndex(null)),
366
+ ),
367
+ onPressIn: composeEventHandlers(onPressIn, pressableProps.onPressIn),
368
+ onPressOut: composeEventHandlers(onPressOut, pressableProps.onPressOut),
369
+ disabled: isDisabled,
370
+ tabIndex: isDisabled ? -1 : 0,
371
+ onKeyDown: handleKeyDown,
372
+ role: 'slider',
373
+ 'aria-valuemin': thumbMin,
374
+ 'aria-valuemax': thumbMax,
375
+ 'aria-valuenow': value,
376
+ 'aria-orientation': orientation,
377
+ 'aria-disabled': isDisabled,
378
+ } as PressableProps)
379
+ : ({} as PressableProps);
231
380
 
232
381
  return (
233
- <Comp
234
- ref={setThumbRef}
235
- {...(props as PressableProps)}
236
- {...(isWeb ? webInteractionProps : panResponder.panHandlers)}
237
- onLayout={handleLayout}
238
- onFocus={composeEventHandlers(
239
- composeEventHandlers(onFocus as (...args: unknown[]) => unknown, focusProps.onFocus),
240
- composeEventHandlers(focusRingProps.onFocus, () => setIsFocused(true)),
241
- )}
242
- onBlur={composeEventHandlers(
243
- composeEventHandlers(onBlur as (...args: unknown[]) => unknown, focusProps.onBlur),
244
- composeEventHandlers(focusRingProps.onBlur, () => setIsFocused(false)),
245
- )}
246
- style={[thumbStyle, style]}
247
- accessibilityRole="none"
248
- importantForAccessibility="no"
249
- {...dataAttributes({
250
- slot: 'slider-thumb',
251
- hover: isHovered,
252
- focus: isFocused,
253
- focusVisible: isFocusVisible,
254
- // On native there is no `Pressable`, so `usePress` never fires; the
255
- // drag state stands in for "active" so the thumb still scales while
256
- // the user is dragging (see `data-[active=true]:scale-105`).
257
- active: isPressed || isDragging,
258
- dragging: isDragging,
259
- disabled: isDisabled,
260
- })}
261
- >
262
- {children}
263
- </Comp>
382
+ <SliderThumbProvider value={thumbContextValue}>
383
+ <Comp
384
+ ref={setThumbRef}
385
+ {...(props as PressableProps)}
386
+ {...(isWeb ? webInteractionProps : panResponder.panHandlers)}
387
+ onLayout={handleLayout}
388
+ onFocus={composeEventHandlers(
389
+ composeEventHandlers(onFocus as (...args: unknown[]) => unknown, focusProps.onFocus),
390
+ composeEventHandlers(focusRingProps.onFocus, () => setFocusedIndex(index)),
391
+ )}
392
+ onBlur={composeEventHandlers(
393
+ composeEventHandlers(onBlur as (...args: unknown[]) => unknown, focusProps.onBlur),
394
+ composeEventHandlers(focusRingProps.onBlur, () => setFocusedIndex(null)),
395
+ )}
396
+ style={[thumbStyle, style]}
397
+ accessibilityRole="adjustable"
398
+ accessibilityLabel={resolvedAccessibilityLabel}
399
+ accessibilityValue={accessibilityValue}
400
+ accessibilityActions={[{ name: 'increment' }, { name: 'decrement' }]}
401
+ onAccessibilityAction={handleAccessibilityAction}
402
+ accessibilityState={{ disabled: isDisabled }}
403
+ {...dataAttributes({
404
+ slot: 'slider-thumb',
405
+ hover: isHovered,
406
+ focus: isFocused,
407
+ focusVisible: isFocusVisible,
408
+ active: isPressed || isDragging,
409
+ dragging: isDragging,
410
+ disabled: isDisabled,
411
+ })}
412
+ >
413
+ {children}
414
+ </Comp>
415
+ </SliderThumbProvider>
264
416
  );
265
417
  },
266
418
  );
@@ -1,7 +1,7 @@
1
1
  import type { RefObject } from 'react';
2
2
  import type { View } from 'react-native';
3
3
  import { createContext } from '@cdx-ui/utils';
4
- import type { SliderOrientation } from './types';
4
+ import type { SliderAccessibilityValue, SliderOrientation } from './types';
5
5
 
6
6
  export interface SliderTrackLayout {
7
7
  readonly width: number;
@@ -11,28 +11,72 @@ export interface SliderTrackLayout {
11
11
  }
12
12
 
13
13
  export interface SliderContextValue {
14
- readonly value: number;
14
+ readonly values: readonly number[];
15
15
  readonly min: number;
16
16
  readonly max: number;
17
17
  readonly step: number;
18
- readonly percent: number;
19
18
  readonly orientation: SliderOrientation;
20
19
  readonly isDisabled: boolean;
21
- readonly isDragging: boolean;
22
- readonly isHovered: boolean;
23
- readonly isFocused: boolean;
20
+ readonly draggingIndex: number | null;
21
+ readonly hoveredIndex: number | null;
22
+ readonly focusedIndex: number | null;
24
23
  readonly trackLayout: SliderTrackLayout | null;
25
24
  readonly thumbSize: number;
26
- readonly setValue: (value: number, complete?: boolean) => void;
27
- readonly setValueFromRatio: (ratio: number, complete?: boolean) => void;
25
+ readonly thumbCount: number;
26
+ readonly groupAccessibilityLabel?: string;
27
+ readonly accessibilityValueProp?: SliderAccessibilityValue;
28
+ readonly getThumbPercent: (index: number) => number;
29
+ readonly getThumbMinValue: (index: number) => number;
30
+ readonly getThumbMaxValue: (index: number) => number;
31
+ readonly setThumbValue: (index: number, value: number, complete?: boolean) => void;
32
+ readonly setThumbValueFromRatio: (index: number, ratio: number, complete?: boolean) => void;
33
+ readonly setNearestThumbValueFromRatio: (ratio: number, complete?: boolean) => void;
28
34
  readonly setTrackLayout: (layout: SliderTrackLayout) => void;
29
35
  readonly setThumbSize: (size: number) => void;
36
+ readonly setDraggingIndex: (index: number | null) => void;
37
+ readonly setHoveredIndex: (index: number | null) => void;
38
+ readonly setFocusedIndex: (index: number | null) => void;
39
+ readonly getThumbValueLabel: (index: number) => string;
40
+ readonly registerThumb: (index: number) => () => void;
41
+ readonly isThumbDragging: (index: number) => boolean;
42
+ /** @deprecated Use `values[0]` */
43
+ readonly value: number;
44
+ /** @deprecated Use `getThumbPercent(0)` */
45
+ readonly percent: number;
46
+ /** @deprecated Use `draggingIndex !== null` */
47
+ readonly isDragging: boolean;
48
+ /** @deprecated Use `getThumbValueLabel(0)` */
49
+ readonly getValueLabel: () => string;
50
+ /** @deprecated Use `setThumbValue(0, …)` */
51
+ readonly setValue: (value: number, complete?: boolean) => void;
52
+ /** @deprecated Use `setNearestThumbValueFromRatio` */
53
+ readonly setValueFromRatio: (ratio: number, complete?: boolean) => void;
54
+ /** @deprecated Use per-thumb context `thumbRef` */
55
+ readonly thumbRef: RefObject<View | null>;
56
+ /** @deprecated Use `hoveredIndex !== null` */
57
+ readonly isHovered: boolean;
58
+ /** @deprecated Use `focusedIndex !== null` */
59
+ readonly isFocused: boolean;
60
+ /** @deprecated Use `setDraggingIndex` */
30
61
  readonly setIsDragging: (dragging: boolean) => void;
62
+ /** @deprecated Use `setHoveredIndex` */
31
63
  readonly setIsHovered: (hovered: boolean) => void;
64
+ /** @deprecated Use `setFocusedIndex` */
32
65
  readonly setIsFocused: (focused: boolean) => void;
33
- readonly getValueLabel: () => string;
66
+ }
67
+
68
+ export interface SliderThumbContextValue {
69
+ readonly index: number;
70
+ readonly value: number;
71
+ readonly percent: number;
72
+ readonly isDragging: boolean;
73
+ readonly isHovered: boolean;
74
+ readonly isFocused: boolean;
34
75
  readonly thumbRef: RefObject<View | null>;
35
76
  }
36
77
 
37
78
  export const [SliderProvider, useSliderContext] =
38
79
  createContext<SliderContextValue>('SliderContext');
80
+
81
+ export const [SliderThumbProvider, useSliderThumbContext] =
82
+ createContext<SliderThumbContextValue>('SliderThumbContext');
@@ -4,15 +4,22 @@ export type {
4
4
  ISliderThumbProps,
5
5
  SliderAccessibilityValue,
6
6
  SliderOrientation,
7
+ SliderRangeValueProps,
8
+ SliderSingleValueProps,
7
9
  } from './types';
8
- export type { SliderContextValue, SliderTrackLayout } from './context';
9
- export { useSliderContext } from './context';
10
+ export type { SliderContextValue, SliderThumbContextValue, SliderTrackLayout } from './context';
11
+ export { useSliderContext, useSliderThumbContext } from './context';
10
12
  export { SliderRoot, type SliderRootProps } from './SliderRoot';
11
13
  export { SliderRail, type SliderRailProps } from './SliderRail';
12
14
  export { SliderThumb, type SliderThumbProps } from './SliderThumb';
13
15
  export {
16
+ arraysEqual,
17
+ denormalizeSliderValue,
18
+ getClosestValueIndex,
19
+ getFillExtent,
14
20
  getMarkerPositionStyle,
15
21
  getMarkerStepValues,
22
+ normalizeSliderValue,
16
23
  percentToFillExtent,
17
24
  SLIDER_MARKER_SIZE,
18
25
  valueToPercent,
@@ -5,30 +5,67 @@ export type SliderOrientation = 'horizontal' | 'vertical';
5
5
 
6
6
  export type SliderAccessibilityValue = NonNullable<ViewProps['accessibilityValue']>;
7
7
 
8
- export interface InterfaceSliderProps extends Omit<ViewProps, 'children' | 'accessibilityValue'> {
9
- readonly value?: number;
10
- readonly defaultValue?: number;
11
- readonly onChange?: (value: number) => void;
12
- readonly onChangeComplete?: (value: number) => void;
8
+ export type SliderValue = number | readonly number[];
9
+
10
+ interface SliderBaseProps extends Omit<ViewProps, 'children' | 'accessibilityValue'> {
11
+ /** Minimum selectable value. @default `0` */
13
12
  readonly min?: number;
13
+ /** Maximum selectable value. @default `100` */
14
14
  readonly max?: number;
15
+ /**
16
+ * Increment used to snap value updates and generate automatic `Slider.MarkerGroup` marks.
17
+ * @default `1`
18
+ */
15
19
  readonly step?: number;
20
+ /** Disables pointer, keyboard, and screen reader interaction. @default `false` */
16
21
  readonly isDisabled?: boolean;
22
+ /** Slider axis. @default `'horizontal'` */
17
23
  readonly orientation?: SliderOrientation;
18
24
  /**
19
- * Screen reader label when there is no visible label.
25
+ * Screen reader label for the slider group when there is no visible label.
20
26
  */
21
27
  readonly accessibilityLabel?: string;
22
28
  /**
23
- * Screen reader value for the slider. Merged with derived `min`, `max`, `now`, and formatted
24
- * `text` when fields are omitted. `text` overrides the numeric announcement when set.
29
+ * Screen reader value override. Merged with derived per-thumb values when fields are omitted;
30
+ * `text` replaces the formatted announcement. Native numeric values are normalized to 0–100.
25
31
  */
26
32
  readonly accessibilityValue?: SliderAccessibilityValue;
27
33
  readonly children?: ReactNode;
28
34
  }
29
35
 
36
+ export interface SliderSingleValueProps {
37
+ readonly value?: number;
38
+ /** Initial uncontrolled value. Defaults to `min`. */
39
+ readonly defaultValue?: number;
40
+ /** Called while the value changes. */
41
+ readonly onChange?: (value: number) => void;
42
+ /** Called when a pointer, keyboard, or screen reader interaction completes. */
43
+ readonly onChangeComplete?: (value: number) => void;
44
+ }
45
+
46
+ export interface SliderRangeValueProps {
47
+ readonly value?: readonly number[];
48
+ /** Initial uncontrolled thumb values. */
49
+ readonly defaultValue?: readonly number[];
50
+ /** Called while any thumb value changes. */
51
+ readonly onChange?: (value: number[]) => void;
52
+ /** Called when a pointer, keyboard, or screen reader interaction completes. */
53
+ readonly onChangeComplete?: (value: number[]) => void;
54
+ }
55
+
56
+ export type InterfaceSliderProps = SliderBaseProps &
57
+ (SliderSingleValueProps | SliderRangeValueProps);
58
+
30
59
  export type ISliderProps = InterfaceSliderProps;
31
60
 
61
+ /** Wider internal props for the root primitive implementation. */
62
+ export interface SliderRootImplementationProps extends SliderBaseProps {
63
+ readonly value?: number | readonly number[];
64
+ readonly defaultValue?: number | readonly number[];
65
+ readonly onChange?: ((value: number) => void) | ((value: number[]) => void);
66
+ readonly onChangeComplete?: ((value: number) => void) | ((value: number[]) => void);
67
+ }
68
+
32
69
  export interface InterfaceSliderRailProps extends ViewProps {
33
70
  readonly asChild?: boolean;
34
71
  readonly children?: ReactNode;
@@ -38,6 +75,10 @@ export type ISliderRailProps = InterfaceSliderRailProps;
38
75
 
39
76
  export interface InterfaceSliderThumbProps extends PressableProps {
40
77
  readonly asChild?: boolean;
78
+ /**
79
+ * Zero-based thumb index. Defaults to `0` for single-value sliders.
80
+ */
81
+ readonly index?: number;
41
82
  readonly children?: ReactNode;
42
83
  }
43
84
 
@@ -77,8 +77,8 @@ export function useWebSliderDrag({
77
77
  }
78
78
  draggingRef.current = false;
79
79
  dragContextRef.current = null;
80
- setIsDragging(false);
81
80
  setValueFromRatio(lastRatioRef.current, true);
81
+ setIsDragging(false);
82
82
  }, [setIsDragging, setValueFromRatio]);
83
83
 
84
84
  const startDrag = useCallback(