@cdx-ui/primitives 0.0.1-beta.115 → 0.0.1-beta.116
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/lib/commonjs/index.js +35 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/overlay/useAnchorPosition.js +60 -11
- package/lib/commonjs/overlay/useAnchorPosition.js.map +1 -1
- package/lib/commonjs/slider/SliderRail.js +131 -0
- package/lib/commonjs/slider/SliderRail.js.map +1 -0
- package/lib/commonjs/slider/SliderRoot.js +186 -0
- package/lib/commonjs/slider/SliderRoot.js.map +1 -0
- package/lib/commonjs/slider/SliderThumb.js +228 -0
- package/lib/commonjs/slider/SliderThumb.js.map +1 -0
- package/lib/commonjs/slider/context.js +11 -0
- package/lib/commonjs/slider/context.js.map +1 -0
- package/lib/commonjs/slider/index.js +65 -0
- package/lib/commonjs/slider/index.js.map +1 -0
- package/lib/commonjs/slider/types.js +6 -0
- package/lib/commonjs/slider/types.js.map +1 -0
- package/lib/commonjs/slider/useSliderPointer.js +116 -0
- package/lib/commonjs/slider/useSliderPointer.js.map +1 -0
- package/lib/commonjs/slider/utils.js +155 -0
- package/lib/commonjs/slider/utils.js.map +1 -0
- package/lib/commonjs/utils/assignRef.js +16 -0
- package/lib/commonjs/utils/assignRef.js.map +1 -0
- package/lib/commonjs/utils/createPortal.js +3 -1
- package/lib/commonjs/utils/createPortal.js.map +1 -1
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/overlay/useAnchorPosition.js +60 -11
- package/lib/module/overlay/useAnchorPosition.js.map +1 -1
- package/lib/module/slider/SliderRail.js +127 -0
- package/lib/module/slider/SliderRail.js.map +1 -0
- package/lib/module/slider/SliderRoot.js +182 -0
- package/lib/module/slider/SliderRoot.js.map +1 -0
- package/lib/module/slider/SliderThumb.js +224 -0
- package/lib/module/slider/SliderThumb.js.map +1 -0
- package/lib/module/slider/context.js +5 -0
- package/lib/module/slider/context.js.map +1 -0
- package/lib/module/slider/index.js +8 -0
- package/lib/module/slider/index.js.map +1 -0
- package/lib/module/slider/types.js +4 -0
- package/lib/module/slider/types.js.map +1 -0
- package/lib/module/slider/useSliderPointer.js +110 -0
- package/lib/module/slider/useSliderPointer.js.map +1 -0
- package/lib/module/slider/utils.js +138 -0
- package/lib/module/slider/utils.js.map +1 -0
- package/lib/module/utils/assignRef.js +12 -0
- package/lib/module/utils/assignRef.js.map +1 -0
- package/lib/module/utils/createPortal.js +3 -1
- package/lib/module/utils/createPortal.js.map +1 -1
- package/lib/typescript/index.d.ts +4 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/overlay/useAnchorPosition.d.ts +7 -1
- package/lib/typescript/overlay/useAnchorPosition.d.ts.map +1 -1
- package/lib/typescript/slider/SliderRail.d.ts +6 -0
- package/lib/typescript/slider/SliderRail.d.ts.map +1 -0
- package/lib/typescript/slider/SliderRoot.d.ts +7 -0
- package/lib/typescript/slider/SliderRoot.d.ts.map +1 -0
- package/lib/typescript/slider/SliderThumb.d.ts +6 -0
- package/lib/typescript/slider/SliderThumb.d.ts.map +1 -0
- package/lib/typescript/slider/context.d.ts +37 -0
- package/lib/typescript/slider/context.d.ts.map +1 -0
- package/lib/typescript/slider/index.d.ts +8 -0
- package/lib/typescript/slider/index.d.ts.map +1 -0
- package/lib/typescript/slider/types.d.ts +37 -0
- package/lib/typescript/slider/types.d.ts.map +1 -0
- package/lib/typescript/slider/useSliderPointer.d.ts +23 -0
- package/lib/typescript/slider/useSliderPointer.d.ts.map +1 -0
- package/lib/typescript/slider/utils.d.ts +23 -0
- package/lib/typescript/slider/utils.d.ts.map +1 -0
- package/lib/typescript/utils/assignRef.d.ts +3 -0
- package/lib/typescript/utils/assignRef.d.ts.map +1 -0
- package/lib/typescript/utils/createPortal.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +4 -0
- package/src/overlay/useAnchorPosition.ts +79 -11
- package/src/slider/SliderRail.tsx +143 -0
- package/src/slider/SliderRoot.tsx +266 -0
- package/src/slider/SliderThumb.tsx +268 -0
- package/src/slider/context.tsx +38 -0
- package/src/slider/index.ts +19 -0
- package/src/slider/types.ts +44 -0
- package/src/slider/useSliderPointer.ts +157 -0
- package/src/slider/utils.ts +181 -0
- package/src/utils/assignRef.ts +11 -0
- 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;
|