@cdx-ui/primitives 0.0.1-beta.114 → 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.
Files changed (85) hide show
  1. package/README.md +32 -30
  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,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;
@@ -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
+ }
@@ -0,0 +1,11 @@
1
+ import type { ForwardedRef } from 'react';
2
+
3
+ export function assignRef<T>(ref: ForwardedRef<T>, value: T | null) {
4
+ if (typeof ref === 'function') {
5
+ ref(value);
6
+ return;
7
+ }
8
+ if (ref) {
9
+ ref.current = value;
10
+ }
11
+ }
@@ -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 {