@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.
- package/README.md +5 -0
- 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,157 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import { Platform } from 'react-native';
|
|
3
|
+
import type { SliderTrackLayout } from './context';
|
|
4
|
+
import type { SliderOrientation } from './types';
|
|
5
|
+
import { clamp, getThumbTravel, pointerCoordToRatio, pointerCoordToVerticalRatio } from './utils';
|
|
6
|
+
|
|
7
|
+
export function getPointerCoords(nativeEvent: {
|
|
8
|
+
pageX?: number;
|
|
9
|
+
pageY?: number;
|
|
10
|
+
clientX?: number;
|
|
11
|
+
clientY?: number;
|
|
12
|
+
}): { x: number; y: number } {
|
|
13
|
+
if (Platform.OS === 'web' && nativeEvent.clientX != null && nativeEvent.clientY != null) {
|
|
14
|
+
return { x: nativeEvent.clientX, y: nativeEvent.clientY };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
x: nativeEvent.pageX ?? 0,
|
|
19
|
+
y: nativeEvent.pageY ?? 0,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getRatioFromPagePoint(
|
|
24
|
+
pageX: number,
|
|
25
|
+
pageY: number,
|
|
26
|
+
trackLayout: SliderTrackLayout | null,
|
|
27
|
+
orientation: SliderOrientation,
|
|
28
|
+
thumbSize: number,
|
|
29
|
+
): number {
|
|
30
|
+
if (!trackLayout) {
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (orientation === 'horizontal') {
|
|
35
|
+
return pointerCoordToRatio(pageX, trackLayout.x, trackLayout.width, thumbSize);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return pointerCoordToVerticalRatio(pageY, trackLayout.y, trackLayout.height, thumbSize);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface WebDragContext {
|
|
42
|
+
readonly startPointer: number;
|
|
43
|
+
readonly startRatio: number;
|
|
44
|
+
readonly travel: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function useWebSliderDrag({
|
|
48
|
+
isDisabled,
|
|
49
|
+
setIsDragging,
|
|
50
|
+
setValueFromRatio,
|
|
51
|
+
trackLayout,
|
|
52
|
+
orientation,
|
|
53
|
+
thumbSize,
|
|
54
|
+
}: {
|
|
55
|
+
isDisabled: boolean;
|
|
56
|
+
setIsDragging: (dragging: boolean) => void;
|
|
57
|
+
setValueFromRatio: (ratio: number, complete?: boolean) => void;
|
|
58
|
+
trackLayout: SliderTrackLayout | null;
|
|
59
|
+
orientation: SliderOrientation;
|
|
60
|
+
thumbSize: number;
|
|
61
|
+
}) {
|
|
62
|
+
const draggingRef = useRef(false);
|
|
63
|
+
const lastRatioRef = useRef(0);
|
|
64
|
+
const dragContextRef = useRef<WebDragContext | null>(null);
|
|
65
|
+
const cleanupRef = useRef<(() => void) | null>(null);
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
return () => {
|
|
69
|
+
cleanupRef.current?.();
|
|
70
|
+
cleanupRef.current = null;
|
|
71
|
+
};
|
|
72
|
+
}, []);
|
|
73
|
+
|
|
74
|
+
const endDrag = useCallback(() => {
|
|
75
|
+
if (!draggingRef.current) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
draggingRef.current = false;
|
|
79
|
+
dragContextRef.current = null;
|
|
80
|
+
setIsDragging(false);
|
|
81
|
+
setValueFromRatio(lastRatioRef.current, true);
|
|
82
|
+
}, [setIsDragging, setValueFromRatio]);
|
|
83
|
+
|
|
84
|
+
const startDrag = useCallback(
|
|
85
|
+
(
|
|
86
|
+
pageX: number,
|
|
87
|
+
pageY: number,
|
|
88
|
+
anchorRatio?: number,
|
|
89
|
+
trackLayoutOverride?: SliderTrackLayout,
|
|
90
|
+
) => {
|
|
91
|
+
if (isDisabled || Platform.OS !== 'web' || typeof window === 'undefined') {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const layout = trackLayoutOverride ?? trackLayout;
|
|
96
|
+
if (!layout) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const trackSize = orientation === 'horizontal' ? layout.width : layout.height;
|
|
101
|
+
const travel = getThumbTravel(trackSize, thumbSize);
|
|
102
|
+
if (travel <= 0) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const startPointer = orientation === 'horizontal' ? pageX : pageY;
|
|
107
|
+
const startRatio =
|
|
108
|
+
anchorRatio != null
|
|
109
|
+
? clamp(anchorRatio, 0, 1)
|
|
110
|
+
: getRatioFromPagePoint(pageX, pageY, layout, orientation, thumbSize);
|
|
111
|
+
|
|
112
|
+
lastRatioRef.current = startRatio;
|
|
113
|
+
draggingRef.current = true;
|
|
114
|
+
dragContextRef.current = { startPointer, startRatio, travel };
|
|
115
|
+
setIsDragging(true);
|
|
116
|
+
setValueFromRatio(startRatio);
|
|
117
|
+
|
|
118
|
+
const onMove = (event: PointerEvent) => {
|
|
119
|
+
if (!draggingRef.current || !dragContextRef.current) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
event.preventDefault();
|
|
123
|
+
const pointer = orientation === 'horizontal' ? event.clientX : event.clientY;
|
|
124
|
+
const delta = pointer - dragContextRef.current.startPointer;
|
|
125
|
+
const deltaAlongTrack = orientation === 'horizontal' ? delta : -delta;
|
|
126
|
+
const nextRatio = clamp(
|
|
127
|
+
dragContextRef.current.startRatio + deltaAlongTrack / dragContextRef.current.travel,
|
|
128
|
+
0,
|
|
129
|
+
1,
|
|
130
|
+
);
|
|
131
|
+
lastRatioRef.current = nextRatio;
|
|
132
|
+
setValueFromRatio(nextRatio);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const onUp = () => {
|
|
136
|
+
cleanupRef.current = null;
|
|
137
|
+
window.removeEventListener('pointermove', onMove);
|
|
138
|
+
window.removeEventListener('pointerup', onUp);
|
|
139
|
+
window.removeEventListener('pointercancel', onUp);
|
|
140
|
+
endDrag();
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
window.addEventListener('pointermove', onMove);
|
|
144
|
+
window.addEventListener('pointerup', onUp);
|
|
145
|
+
window.addEventListener('pointercancel', onUp);
|
|
146
|
+
|
|
147
|
+
cleanupRef.current = () => {
|
|
148
|
+
window.removeEventListener('pointermove', onMove);
|
|
149
|
+
window.removeEventListener('pointerup', onUp);
|
|
150
|
+
window.removeEventListener('pointercancel', onUp);
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
[endDrag, isDisabled, orientation, setIsDragging, setValueFromRatio, thumbSize, trackLayout],
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
return { startDrag };
|
|
157
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type { ViewStyle } from 'react-native';
|
|
2
|
+
import type { SliderTrackLayout } from './context';
|
|
3
|
+
import type { SliderOrientation } from './types';
|
|
4
|
+
|
|
5
|
+
type PercentString = `${number}%`;
|
|
6
|
+
|
|
7
|
+
function toPercentDimension(percent: number): PercentString {
|
|
8
|
+
return `${String(percent)}%` as PercentString;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const SLIDER_MARKER_SIZE = 3;
|
|
12
|
+
|
|
13
|
+
export function clamp(value: number, min: number, max: number): number {
|
|
14
|
+
return Math.min(max, Math.max(min, value));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getThumbTravel(trackSize: number, _thumbSize: number): number {
|
|
18
|
+
return Math.max(trackSize, 0);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Thumb leading-edge offset along the track axis (center-based). */
|
|
22
|
+
export function percentToThumbOffset(
|
|
23
|
+
percent: number,
|
|
24
|
+
trackSize: number,
|
|
25
|
+
thumbSize: number,
|
|
26
|
+
): number {
|
|
27
|
+
if (trackSize <= 0) return 0;
|
|
28
|
+
return (clamp(percent, 0, 100) / 100) * trackSize - thumbSize / 2;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Filled track extent to the thumb center. */
|
|
32
|
+
export function percentToFillExtent(
|
|
33
|
+
percent: number,
|
|
34
|
+
trackSize: number,
|
|
35
|
+
_thumbSize: number,
|
|
36
|
+
): number {
|
|
37
|
+
if (trackSize <= 0) return 0;
|
|
38
|
+
return (clamp(percent, 0, 100) / 100) * trackSize;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Map a pointer coordinate on the track axis to a 0–1 ratio. */
|
|
42
|
+
export function pointerCoordToRatio(
|
|
43
|
+
pointerCoord: number,
|
|
44
|
+
trackStart: number,
|
|
45
|
+
trackSize: number,
|
|
46
|
+
_thumbSize: number,
|
|
47
|
+
): number {
|
|
48
|
+
if (trackSize <= 0) {
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
return clamp((pointerCoord - trackStart) / trackSize, 0, 1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Vertical slider: bottom = min (0), top = max (1). */
|
|
55
|
+
export function pointerCoordToVerticalRatio(
|
|
56
|
+
pointerCoord: number,
|
|
57
|
+
trackStart: number,
|
|
58
|
+
trackSize: number,
|
|
59
|
+
_thumbSize: number,
|
|
60
|
+
): number {
|
|
61
|
+
if (trackSize <= 0) {
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
return clamp((trackStart + trackSize - pointerCoord) / trackSize, 0, 1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function snapToStep(value: number, min: number, max: number, step: number): number {
|
|
68
|
+
if (step <= 0) {
|
|
69
|
+
return clamp(value, min, max);
|
|
70
|
+
}
|
|
71
|
+
const stepped = min + Math.round((value - min) / step) * step;
|
|
72
|
+
return clamp(stepped, min, max);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function valueToPercent(value: number, min: number, max: number): number {
|
|
76
|
+
if (max <= min) {
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
return ((clamp(value, min, max) - min) / (max - min)) * 100;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function percentToValue(percent: number, min: number, max: number, step: number): number {
|
|
83
|
+
const ratio = clamp(percent, 0, 100) / 100;
|
|
84
|
+
return snapToStep(min + ratio * (max - min), min, max, step);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function formatSliderValue(value: number): string {
|
|
88
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(1);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function getMarkerStepValues(min: number, max: number, step: number): number[] {
|
|
92
|
+
if (step <= 0 || max <= min) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const decimals = getDecimalPlaces(step);
|
|
97
|
+
const stepCount = Math.floor((max - min) / step);
|
|
98
|
+
const values = Array.from({ length: stepCount + 1 }, (_, index) =>
|
|
99
|
+
roundTo(min + index * step, decimals),
|
|
100
|
+
);
|
|
101
|
+
if (values[values.length - 1] < max) {
|
|
102
|
+
values.push(max);
|
|
103
|
+
}
|
|
104
|
+
return values;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function getDecimalPlaces(n: number): number {
|
|
108
|
+
const str = String(n);
|
|
109
|
+
const dotIndex = str.indexOf('.');
|
|
110
|
+
return dotIndex === -1 ? 0 : str.length - dotIndex - 1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function roundTo(value: number, decimals: number): number {
|
|
114
|
+
const factor = 10 ** decimals;
|
|
115
|
+
return Math.round(value * factor) / factor;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Marker leading-edge offset (center-aligned to the thumb coordinate system). */
|
|
119
|
+
export function percentToMarkerCenterOffset(
|
|
120
|
+
percent: number,
|
|
121
|
+
trackSize: number,
|
|
122
|
+
markerSize = SLIDER_MARKER_SIZE,
|
|
123
|
+
): number {
|
|
124
|
+
if (trackSize <= 0) return 0;
|
|
125
|
+
return (clamp(percent, 0, 100) / 100) * trackSize - markerSize / 2;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function getMarkerPositionStyle(
|
|
129
|
+
percent: number,
|
|
130
|
+
orientation: SliderOrientation,
|
|
131
|
+
trackLayout: SliderTrackLayout | null,
|
|
132
|
+
_thumbSize: number,
|
|
133
|
+
markerSize = SLIDER_MARKER_SIZE,
|
|
134
|
+
): ViewStyle {
|
|
135
|
+
const trackSize =
|
|
136
|
+
trackLayout == null ? 0 : orientation === 'horizontal' ? trackLayout.width : trackLayout.height;
|
|
137
|
+
|
|
138
|
+
if (trackSize > 0) {
|
|
139
|
+
const offset = percentToMarkerCenterOffset(percent, trackSize, markerSize);
|
|
140
|
+
if (orientation === 'vertical') {
|
|
141
|
+
return {
|
|
142
|
+
position: 'absolute',
|
|
143
|
+
left: '50%',
|
|
144
|
+
bottom: offset,
|
|
145
|
+
width: markerSize,
|
|
146
|
+
height: markerSize,
|
|
147
|
+
marginLeft: -markerSize / 2,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
position: 'absolute',
|
|
152
|
+
top: '50%',
|
|
153
|
+
left: offset,
|
|
154
|
+
width: markerSize,
|
|
155
|
+
height: markerSize,
|
|
156
|
+
marginTop: -markerSize / 2,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (orientation === 'vertical') {
|
|
161
|
+
return {
|
|
162
|
+
position: 'absolute',
|
|
163
|
+
left: '50%',
|
|
164
|
+
bottom: toPercentDimension(percent),
|
|
165
|
+
width: markerSize,
|
|
166
|
+
height: markerSize,
|
|
167
|
+
marginLeft: -markerSize / 2,
|
|
168
|
+
transform: [{ translateY: markerSize / 2 }],
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
position: 'absolute',
|
|
174
|
+
top: '50%',
|
|
175
|
+
left: toPercentDimension(percent),
|
|
176
|
+
width: markerSize,
|
|
177
|
+
height: markerSize,
|
|
178
|
+
marginTop: -markerSize / 2,
|
|
179
|
+
transform: [{ translateX: -markerSize / 2 }],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
@@ -10,8 +10,10 @@ type CreatePortalFn = (children: React.ReactNode, container: Element) => React.R
|
|
|
10
10
|
* `react-dom`, which would cause a build error on native platforms.
|
|
11
11
|
*/
|
|
12
12
|
export const createPortalFn: CreatePortalFn | undefined = (() => {
|
|
13
|
-
if (Platform.OS !== 'web') return undefined;
|
|
14
13
|
try {
|
|
14
|
+
if (Platform.OS !== 'web') {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
15
17
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
16
18
|
return (require('react-dom') as { createPortal: CreatePortalFn }).createPortal;
|
|
17
19
|
} catch {
|