@cdx-ui/primitives 0.0.1-beta.115 → 0.0.1-beta.117

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 (85) hide show
  1. package/README.md +5 -0
  2. package/lib/commonjs/index.js +35 -0
  3. package/lib/commonjs/index.js.map +1 -1
  4. package/lib/commonjs/overlay/useAnchorPosition.js +60 -11
  5. package/lib/commonjs/overlay/useAnchorPosition.js.map +1 -1
  6. package/lib/commonjs/slider/SliderRail.js +131 -0
  7. package/lib/commonjs/slider/SliderRail.js.map +1 -0
  8. package/lib/commonjs/slider/SliderRoot.js +186 -0
  9. package/lib/commonjs/slider/SliderRoot.js.map +1 -0
  10. package/lib/commonjs/slider/SliderThumb.js +228 -0
  11. package/lib/commonjs/slider/SliderThumb.js.map +1 -0
  12. package/lib/commonjs/slider/context.js +11 -0
  13. package/lib/commonjs/slider/context.js.map +1 -0
  14. package/lib/commonjs/slider/index.js +65 -0
  15. package/lib/commonjs/slider/index.js.map +1 -0
  16. package/lib/commonjs/slider/types.js +6 -0
  17. package/lib/commonjs/slider/types.js.map +1 -0
  18. package/lib/commonjs/slider/useSliderPointer.js +116 -0
  19. package/lib/commonjs/slider/useSliderPointer.js.map +1 -0
  20. package/lib/commonjs/slider/utils.js +155 -0
  21. package/lib/commonjs/slider/utils.js.map +1 -0
  22. package/lib/commonjs/utils/assignRef.js +16 -0
  23. package/lib/commonjs/utils/assignRef.js.map +1 -0
  24. package/lib/commonjs/utils/createPortal.js +3 -1
  25. package/lib/commonjs/utils/createPortal.js.map +1 -1
  26. package/lib/module/index.js +4 -0
  27. package/lib/module/index.js.map +1 -1
  28. package/lib/module/overlay/useAnchorPosition.js +60 -11
  29. package/lib/module/overlay/useAnchorPosition.js.map +1 -1
  30. package/lib/module/slider/SliderRail.js +127 -0
  31. package/lib/module/slider/SliderRail.js.map +1 -0
  32. package/lib/module/slider/SliderRoot.js +182 -0
  33. package/lib/module/slider/SliderRoot.js.map +1 -0
  34. package/lib/module/slider/SliderThumb.js +224 -0
  35. package/lib/module/slider/SliderThumb.js.map +1 -0
  36. package/lib/module/slider/context.js +5 -0
  37. package/lib/module/slider/context.js.map +1 -0
  38. package/lib/module/slider/index.js +8 -0
  39. package/lib/module/slider/index.js.map +1 -0
  40. package/lib/module/slider/types.js +4 -0
  41. package/lib/module/slider/types.js.map +1 -0
  42. package/lib/module/slider/useSliderPointer.js +110 -0
  43. package/lib/module/slider/useSliderPointer.js.map +1 -0
  44. package/lib/module/slider/utils.js +138 -0
  45. package/lib/module/slider/utils.js.map +1 -0
  46. package/lib/module/utils/assignRef.js +12 -0
  47. package/lib/module/utils/assignRef.js.map +1 -0
  48. package/lib/module/utils/createPortal.js +3 -1
  49. package/lib/module/utils/createPortal.js.map +1 -1
  50. package/lib/typescript/index.d.ts +4 -0
  51. package/lib/typescript/index.d.ts.map +1 -1
  52. package/lib/typescript/overlay/useAnchorPosition.d.ts +7 -1
  53. package/lib/typescript/overlay/useAnchorPosition.d.ts.map +1 -1
  54. package/lib/typescript/slider/SliderRail.d.ts +6 -0
  55. package/lib/typescript/slider/SliderRail.d.ts.map +1 -0
  56. package/lib/typescript/slider/SliderRoot.d.ts +7 -0
  57. package/lib/typescript/slider/SliderRoot.d.ts.map +1 -0
  58. package/lib/typescript/slider/SliderThumb.d.ts +6 -0
  59. package/lib/typescript/slider/SliderThumb.d.ts.map +1 -0
  60. package/lib/typescript/slider/context.d.ts +37 -0
  61. package/lib/typescript/slider/context.d.ts.map +1 -0
  62. package/lib/typescript/slider/index.d.ts +8 -0
  63. package/lib/typescript/slider/index.d.ts.map +1 -0
  64. package/lib/typescript/slider/types.d.ts +37 -0
  65. package/lib/typescript/slider/types.d.ts.map +1 -0
  66. package/lib/typescript/slider/useSliderPointer.d.ts +23 -0
  67. package/lib/typescript/slider/useSliderPointer.d.ts.map +1 -0
  68. package/lib/typescript/slider/utils.d.ts +23 -0
  69. package/lib/typescript/slider/utils.d.ts.map +1 -0
  70. package/lib/typescript/utils/assignRef.d.ts +3 -0
  71. package/lib/typescript/utils/assignRef.d.ts.map +1 -0
  72. package/lib/typescript/utils/createPortal.d.ts.map +1 -1
  73. package/package.json +2 -2
  74. package/src/index.ts +4 -0
  75. package/src/overlay/useAnchorPosition.ts +79 -11
  76. package/src/slider/SliderRail.tsx +143 -0
  77. package/src/slider/SliderRoot.tsx +266 -0
  78. package/src/slider/SliderThumb.tsx +268 -0
  79. package/src/slider/context.tsx +38 -0
  80. package/src/slider/index.ts +19 -0
  81. package/src/slider/types.ts +44 -0
  82. package/src/slider/useSliderPointer.ts +157 -0
  83. package/src/slider/utils.ts +181 -0
  84. package/src/utils/assignRef.ts +11 -0
  85. package/src/utils/createPortal.ts +3 -1
@@ -0,0 +1,266 @@
1
+ import { forwardRef, useCallback, useMemo, useRef, useState } from 'react';
2
+ import {
3
+ Platform,
4
+ View,
5
+ type NativeSyntheticEvent,
6
+ type TargetedEvent,
7
+ type ViewProps,
8
+ } from 'react-native';
9
+ import { composeEventHandlers, useControllableState } from '@cdx-ui/utils';
10
+ import { useFocus } from '@react-native-aria/focus';
11
+ import { Slot } from '../slot';
12
+ import { dataAttributes } from '../utils/dataAttributes';
13
+ import { SliderProvider, type SliderTrackLayout } from './context';
14
+ import type { InterfaceSliderProps } from './types';
15
+ import { clamp, formatSliderValue, snapToStep, valueToPercent } from './utils';
16
+
17
+ const STEP_KEYS = new Set(['ArrowUp', 'ArrowRight', 'ArrowDown', 'ArrowLeft', 'Home', 'End']);
18
+
19
+ export interface SliderRootProps extends InterfaceSliderProps {
20
+ readonly asChild?: boolean;
21
+ }
22
+
23
+ export const SliderRoot = forwardRef<View, SliderRootProps>(
24
+ (
25
+ {
26
+ asChild,
27
+ value: valueProp,
28
+ defaultValue,
29
+ onChange,
30
+ onChangeComplete,
31
+ min = 0,
32
+ max = 100,
33
+ step = 1,
34
+ isDisabled = false,
35
+ orientation = 'horizontal',
36
+ accessibilityLabel,
37
+ accessibilityValue: accessibilityValueProp,
38
+ accessibilityRole = 'adjustable',
39
+ role = 'slider',
40
+ children,
41
+ onLayout,
42
+ ...props
43
+ },
44
+ ref,
45
+ ) => {
46
+ const [trackLayout, setTrackLayoutState] = useState<SliderTrackLayout | null>(null);
47
+ const [thumbSize, setThumbSizeState] = useState(20);
48
+ const [isDragging, setIsDragging] = useState(false);
49
+ const [isHovered, setIsHovered] = useState(false);
50
+ const [isFocusedState, setIsFocusedState] = useState(false);
51
+ const thumbRef = useRef<View>(null);
52
+ const { focusProps, isFocused } = useFocus();
53
+
54
+ const resolvedDefault = defaultValue ?? min;
55
+ const [value, setValueState] = useControllableState<number>({
56
+ prop: valueProp,
57
+ defaultProp: resolvedDefault,
58
+ onChange,
59
+ });
60
+
61
+ const currentValue = snapToStep(value ?? resolvedDefault, min, max, step);
62
+ const percent = valueToPercent(currentValue, min, max);
63
+
64
+ const setValue = useCallback(
65
+ (next: number, complete = false) => {
66
+ if (isDisabled) {
67
+ return;
68
+ }
69
+ const snapped = snapToStep(next, min, max, step);
70
+ setValueState(snapped);
71
+ if (complete) {
72
+ onChangeComplete?.(snapped);
73
+ }
74
+ },
75
+ [isDisabled, max, min, onChangeComplete, setValueState, step],
76
+ );
77
+
78
+ const setValueFromRatio = useCallback(
79
+ (ratio: number, complete = false) => {
80
+ const next = min + clamp(ratio, 0, 1) * (max - min);
81
+ setValue(next, complete);
82
+ },
83
+ [max, min, setValue],
84
+ );
85
+
86
+ const setTrackLayout = useCallback((layout: SliderTrackLayout) => {
87
+ setTrackLayoutState((prev) => {
88
+ if (
89
+ prev !== null &&
90
+ prev.width === layout.width &&
91
+ prev.height === layout.height &&
92
+ prev.x === layout.x &&
93
+ prev.y === layout.y
94
+ ) {
95
+ return prev;
96
+ }
97
+ return layout;
98
+ });
99
+ }, []);
100
+
101
+ const setThumbSize = useCallback((size: number) => {
102
+ setThumbSizeState((prev) => (prev === size ? prev : size));
103
+ }, []);
104
+
105
+ const accessibilityValue = useMemo(() => {
106
+ const defaults = {
107
+ min,
108
+ max,
109
+ now: currentValue,
110
+ text: formatSliderValue(currentValue),
111
+ };
112
+
113
+ if (accessibilityValueProp === undefined) {
114
+ return defaults;
115
+ }
116
+
117
+ return { ...defaults, ...accessibilityValueProp };
118
+ }, [accessibilityValueProp, currentValue, max, min]);
119
+
120
+ const getValueLabel = useCallback(() => accessibilityValue.text, [accessibilityValue.text]);
121
+
122
+ const handleKeyDown = useCallback(
123
+ (event: NativeSyntheticEvent<TargetedEvent> & { nativeEvent: { key?: string } }) => {
124
+ if (isDisabled) {
125
+ return;
126
+ }
127
+ const key = event.nativeEvent.key;
128
+ if (!key || !STEP_KEYS.has(key)) {
129
+ return;
130
+ }
131
+ if (Platform.OS === 'web') {
132
+ (event as unknown as { preventDefault: () => void }).preventDefault();
133
+ }
134
+
135
+ let next = currentValue;
136
+ if (key === 'ArrowUp' || key === 'ArrowRight') {
137
+ next = currentValue + step;
138
+ } else if (key === 'ArrowDown' || key === 'ArrowLeft') {
139
+ next = currentValue - step;
140
+ } else if (key === 'Home') {
141
+ next = min;
142
+ } else if (key === 'End') {
143
+ next = max;
144
+ }
145
+
146
+ setValue(next, true);
147
+ },
148
+ [currentValue, isDisabled, max, min, setValue, step],
149
+ );
150
+
151
+ const contextValue = useMemo(
152
+ () => ({
153
+ value: currentValue,
154
+ min,
155
+ max,
156
+ step,
157
+ percent,
158
+ orientation,
159
+ isDisabled,
160
+ isDragging,
161
+ isHovered,
162
+ isFocused: isFocused || isFocusedState,
163
+ trackLayout,
164
+ thumbSize,
165
+ setValue,
166
+ setValueFromRatio,
167
+ setTrackLayout,
168
+ setThumbSize,
169
+ setIsDragging,
170
+ setIsHovered,
171
+ setIsFocused: setIsFocusedState,
172
+ getValueLabel,
173
+ thumbRef,
174
+ }),
175
+ [
176
+ currentValue,
177
+ getValueLabel,
178
+ isDisabled,
179
+ isDragging,
180
+ isFocused,
181
+ isFocusedState,
182
+ isHovered,
183
+ max,
184
+ min,
185
+ orientation,
186
+ percent,
187
+ setTrackLayout,
188
+ setThumbSize,
189
+ setValue,
190
+ setValueFromRatio,
191
+ step,
192
+ thumbSize,
193
+ trackLayout,
194
+ thumbRef,
195
+ ],
196
+ );
197
+
198
+ const dataAttrs = dataAttributes({
199
+ slot: 'slider',
200
+ orientation,
201
+ disabled: isDisabled,
202
+ dragging: isDragging,
203
+ hover: isHovered,
204
+ focus: isFocused,
205
+ value: String(currentValue),
206
+ min: String(min),
207
+ max: String(max),
208
+ });
209
+
210
+ const Comp = asChild ? Slot : View;
211
+
212
+ const handleLayout = composeEventHandlers(onLayout, () => undefined);
213
+
214
+ const webKeyProps =
215
+ Platform.OS === 'web'
216
+ ? {
217
+ tabIndex: isDisabled ? -1 : 0,
218
+ onKeyDown: composeEventHandlers(
219
+ (props as ViewProps & { onKeyDown?: (event: unknown) => void }).onKeyDown,
220
+ handleKeyDown as never,
221
+ ),
222
+ }
223
+ : {};
224
+
225
+ return (
226
+ <SliderProvider value={contextValue}>
227
+ <Comp
228
+ ref={ref}
229
+ {...(props as ViewProps)}
230
+ {...focusProps}
231
+ {...webKeyProps}
232
+ onLayout={handleLayout}
233
+ onFocus={composeEventHandlers(
234
+ composeEventHandlers(
235
+ props.onFocus as (...args: unknown[]) => unknown,
236
+ focusProps.onFocus,
237
+ ),
238
+ () => setIsFocusedState(true),
239
+ )}
240
+ onBlur={composeEventHandlers(
241
+ composeEventHandlers(
242
+ props.onBlur as (...args: unknown[]) => unknown,
243
+ focusProps.onBlur,
244
+ ),
245
+ () => setIsFocusedState(false),
246
+ )}
247
+ accessibilityRole={accessibilityRole}
248
+ accessibilityLabel={accessibilityLabel}
249
+ accessibilityValue={accessibilityValue}
250
+ accessibilityState={{ disabled: isDisabled }}
251
+ aria-valuemin={min}
252
+ aria-valuemax={max}
253
+ aria-valuenow={currentValue}
254
+ aria-orientation={orientation}
255
+ aria-disabled={isDisabled}
256
+ role={role}
257
+ {...dataAttrs}
258
+ >
259
+ {children}
260
+ </Comp>
261
+ </SliderProvider>
262
+ );
263
+ },
264
+ );
265
+
266
+ SliderRoot.displayName = 'SliderRoot';
@@ -0,0 +1,268 @@
1
+ import { forwardRef, useCallback, useMemo, useRef, type ComponentRef } from 'react';
2
+ import {
3
+ PanResponder,
4
+ Platform,
5
+ Pressable,
6
+ View,
7
+ type GestureResponderEvent,
8
+ type LayoutChangeEvent,
9
+ type PressableProps,
10
+ type ViewStyle,
11
+ } from 'react-native';
12
+ import { composeEventHandlers } from '@cdx-ui/utils';
13
+ import { useFocus, useFocusRing } from '@react-native-aria/focus';
14
+ import { useHover, usePress } from '@react-native-aria/interactions';
15
+ import { Slot } from '../slot';
16
+ import { assignRef } from '../utils/assignRef';
17
+ import { dataAttributes } from '../utils/dataAttributes';
18
+ import { useSliderContext } from './context';
19
+ import type { InterfaceSliderThumbProps } from './types';
20
+ import { getPointerCoords, useWebSliderDrag } from './useSliderPointer';
21
+ import { clamp, getThumbTravel, percentToThumbOffset } from './utils';
22
+
23
+ export interface SliderThumbProps extends InterfaceSliderThumbProps {}
24
+
25
+ export const SliderThumb = forwardRef<ComponentRef<typeof Pressable>, SliderThumbProps>(
26
+ (
27
+ {
28
+ asChild,
29
+ style,
30
+ onLayout,
31
+ onHoverIn,
32
+ onHoverOut,
33
+ onFocus,
34
+ onBlur,
35
+ onPressIn,
36
+ onPressOut,
37
+ children,
38
+ ...props
39
+ },
40
+ ref,
41
+ ) => {
42
+ const {
43
+ percent,
44
+ orientation,
45
+ isDisabled,
46
+ isDragging,
47
+ trackLayout,
48
+ thumbSize,
49
+ setValueFromRatio,
50
+ setIsDragging,
51
+ setIsHovered,
52
+ setIsFocused,
53
+ setThumbSize,
54
+ thumbRef,
55
+ } = useSliderContext();
56
+
57
+ const percentRef = useRef(percent);
58
+ percentRef.current = percent;
59
+
60
+ const dragStartRatio = useRef(0);
61
+ const lastRatioRef = useRef(percent / 100);
62
+
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
+ const orientationRef = useRef(orientation);
70
+ orientationRef.current = orientation;
71
+ const trackLayoutRef = useRef(trackLayout);
72
+ trackLayoutRef.current = trackLayout;
73
+ const thumbSizeRef = useRef(thumbSize);
74
+ thumbSizeRef.current = thumbSize;
75
+ const isDisabledRef = useRef(isDisabled);
76
+ isDisabledRef.current = isDisabled;
77
+ const setValueFromRatioRef = useRef(setValueFromRatio);
78
+ setValueFromRatioRef.current = setValueFromRatio;
79
+ const setIsDraggingRef = useRef(setIsDragging);
80
+ setIsDraggingRef.current = setIsDragging;
81
+
82
+ const { hoverProps, isHovered } = useHover();
83
+ const { focusProps, isFocused } = useFocus();
84
+ const { isFocusVisible, focusProps: focusRingProps } = useFocusRing() as {
85
+ isFocusVisible: boolean;
86
+ focusProps: typeof focusProps;
87
+ };
88
+ const { pressProps: pressableProps, isPressed } = usePress({ isDisabled }) as {
89
+ isPressed: boolean;
90
+ pressProps: Pick<PressableProps, 'onPressIn' | 'onPressOut'>;
91
+ };
92
+
93
+ const { startDrag } = useWebSliderDrag({
94
+ isDisabled,
95
+ setIsDragging,
96
+ setValueFromRatio,
97
+ trackLayout,
98
+ orientation,
99
+ thumbSize,
100
+ });
101
+
102
+ const panResponder = useMemo(
103
+ () =>
104
+ PanResponder.create({
105
+ onStartShouldSetPanResponder: () => !isDisabledRef.current,
106
+ onMoveShouldSetPanResponder: () => !isDisabledRef.current,
107
+ onPanResponderGrant: () => {
108
+ if (isDisabledRef.current) {
109
+ return;
110
+ }
111
+ setIsDraggingRef.current(true);
112
+ dragStartRatio.current = percentRef.current / 100;
113
+ lastRatioRef.current = dragStartRatio.current;
114
+ },
115
+ onPanResponderMove: (_event, gestureState) => {
116
+ const layout = trackLayoutRef.current;
117
+ if (!layout) {
118
+ return;
119
+ }
120
+ const isHorizontal = orientationRef.current === 'horizontal';
121
+ const trackSize = isHorizontal ? layout.width : layout.height;
122
+ const travel = getThumbTravel(trackSize, thumbSizeRef.current);
123
+ if (travel <= 0) {
124
+ return;
125
+ }
126
+ const delta = isHorizontal ? gestureState.dx : -gestureState.dy;
127
+ const nextRatio = clamp(dragStartRatio.current + delta / travel, 0, 1);
128
+ lastRatioRef.current = nextRatio;
129
+ setValueFromRatioRef.current(nextRatio);
130
+ },
131
+ onPanResponderRelease: () => {
132
+ setIsDraggingRef.current(false);
133
+ setValueFromRatioRef.current(lastRatioRef.current, true);
134
+ },
135
+ onPanResponderTerminate: () => {
136
+ setIsDraggingRef.current(false);
137
+ setValueFromRatioRef.current(lastRatioRef.current, true);
138
+ },
139
+ }),
140
+ [],
141
+ );
142
+
143
+ const thumbStyle = useMemo((): ViewStyle => {
144
+ const trackSize =
145
+ trackLayout == null
146
+ ? 0
147
+ : orientation === 'horizontal'
148
+ ? trackLayout.width
149
+ : trackLayout.height;
150
+ const offset = percentToThumbOffset(percent, trackSize, thumbSize);
151
+
152
+ if (orientation === 'vertical') {
153
+ return {
154
+ position: 'absolute',
155
+ left: '50%',
156
+ bottom: offset,
157
+ marginLeft: -thumbSize / 2,
158
+ zIndex: 2,
159
+ };
160
+ }
161
+
162
+ return {
163
+ position: 'absolute',
164
+ top: '50%',
165
+ left: offset,
166
+ transform: [{ translateY: -thumbSize / 2 }],
167
+ zIndex: 2,
168
+ };
169
+ }, [orientation, percent, thumbSize, trackLayout]);
170
+
171
+ const handleLayout = useCallback(
172
+ (event: LayoutChangeEvent) => {
173
+ const { width, height } = event.nativeEvent.layout;
174
+ const measured = orientation === 'horizontal' ? width : height;
175
+ if (measured > 0) {
176
+ setThumbSize(measured);
177
+ }
178
+ if (onLayout) {
179
+ onLayout(event);
180
+ }
181
+ },
182
+ [onLayout, orientation, setThumbSize],
183
+ );
184
+
185
+ const handlePointerDown = useCallback(
186
+ (event: GestureResponderEvent) => {
187
+ if (Platform.OS !== 'web' || isDisabled) {
188
+ return;
189
+ }
190
+ event.preventDefault();
191
+ event.stopPropagation();
192
+ const { x, y } = getPointerCoords(event.nativeEvent);
193
+ startDrag(x, y, percentRef.current / 100);
194
+ },
195
+ [isDisabled, startDrag],
196
+ );
197
+
198
+ const isWeb = Platform.OS === 'web';
199
+
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.
206
+ const Comp = asChild ? Slot : isWeb ? Pressable : View;
207
+
208
+ const setThumbRef = useCallback(
209
+ (node: ComponentRef<typeof Pressable> | null) => {
210
+ thumbRef.current = node;
211
+ assignRef(ref, node);
212
+ },
213
+ [ref, thumbRef],
214
+ );
215
+
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
+ };
231
+
232
+ 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>
264
+ );
265
+ },
266
+ );
267
+
268
+ SliderThumb.displayName = 'SliderThumb';
@@ -0,0 +1,38 @@
1
+ import type { RefObject } from 'react';
2
+ import type { View } from 'react-native';
3
+ import { createContext } from '@cdx-ui/utils';
4
+ import type { SliderOrientation } from './types';
5
+
6
+ export interface SliderTrackLayout {
7
+ readonly width: number;
8
+ readonly height: number;
9
+ readonly x: number;
10
+ readonly y: number;
11
+ }
12
+
13
+ export interface SliderContextValue {
14
+ readonly value: number;
15
+ readonly min: number;
16
+ readonly max: number;
17
+ readonly step: number;
18
+ readonly percent: number;
19
+ readonly orientation: SliderOrientation;
20
+ readonly isDisabled: boolean;
21
+ readonly isDragging: boolean;
22
+ readonly isHovered: boolean;
23
+ readonly isFocused: boolean;
24
+ readonly trackLayout: SliderTrackLayout | null;
25
+ readonly thumbSize: number;
26
+ readonly setValue: (value: number, complete?: boolean) => void;
27
+ readonly setValueFromRatio: (ratio: number, complete?: boolean) => void;
28
+ readonly setTrackLayout: (layout: SliderTrackLayout) => void;
29
+ readonly setThumbSize: (size: number) => void;
30
+ readonly setIsDragging: (dragging: boolean) => void;
31
+ readonly setIsHovered: (hovered: boolean) => void;
32
+ readonly setIsFocused: (focused: boolean) => void;
33
+ readonly getValueLabel: () => string;
34
+ readonly thumbRef: RefObject<View | null>;
35
+ }
36
+
37
+ export const [SliderProvider, useSliderContext] =
38
+ createContext<SliderContextValue>('SliderContext');
@@ -0,0 +1,19 @@
1
+ export type {
2
+ ISliderProps,
3
+ ISliderRailProps,
4
+ ISliderThumbProps,
5
+ SliderAccessibilityValue,
6
+ SliderOrientation,
7
+ } from './types';
8
+ export type { SliderContextValue, SliderTrackLayout } from './context';
9
+ export { useSliderContext } from './context';
10
+ export { SliderRoot, type SliderRootProps } from './SliderRoot';
11
+ export { SliderRail, type SliderRailProps } from './SliderRail';
12
+ export { SliderThumb, type SliderThumbProps } from './SliderThumb';
13
+ export {
14
+ getMarkerPositionStyle,
15
+ getMarkerStepValues,
16
+ percentToFillExtent,
17
+ SLIDER_MARKER_SIZE,
18
+ valueToPercent,
19
+ } from './utils';
@@ -0,0 +1,44 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { PressableProps, ViewProps } from 'react-native';
3
+
4
+ export type SliderOrientation = 'horizontal' | 'vertical';
5
+
6
+ export type SliderAccessibilityValue = NonNullable<ViewProps['accessibilityValue']>;
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;
13
+ readonly min?: number;
14
+ readonly max?: number;
15
+ readonly step?: number;
16
+ readonly isDisabled?: boolean;
17
+ readonly orientation?: SliderOrientation;
18
+ /**
19
+ * Screen reader label when there is no visible label.
20
+ */
21
+ readonly accessibilityLabel?: string;
22
+ /**
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.
25
+ */
26
+ readonly accessibilityValue?: SliderAccessibilityValue;
27
+ readonly children?: ReactNode;
28
+ }
29
+
30
+ export type ISliderProps = InterfaceSliderProps;
31
+
32
+ export interface InterfaceSliderRailProps extends ViewProps {
33
+ readonly asChild?: boolean;
34
+ readonly children?: ReactNode;
35
+ }
36
+
37
+ export type ISliderRailProps = InterfaceSliderRailProps;
38
+
39
+ export interface InterfaceSliderThumbProps extends PressableProps {
40
+ readonly asChild?: boolean;
41
+ readonly children?: ReactNode;
42
+ }
43
+
44
+ export type ISliderThumbProps = InterfaceSliderThumbProps;