@klyftig/mobile-ui 1.3.1 → 1.3.3
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/package.json +1 -1
- package/src/components/WheelPicker.tsx +81 -33
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useEffect, useMemo, useRef } from 'react';
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
ScrollView,
|
|
4
4
|
View,
|
|
@@ -22,6 +22,11 @@ export interface WheelPickerProps {
|
|
|
22
22
|
|
|
23
23
|
const DEFAULT_ITEM_HEIGHT = 48;
|
|
24
24
|
const DEFAULT_VISIBLE_COUNT = 5;
|
|
25
|
+
// Render this many rows above and below the active index. The remaining rows
|
|
26
|
+
// are virtual — the scroll container still spans the full height so snapping
|
|
27
|
+
// and momentum behave normally, but we don't mount 600 <Text> nodes on first
|
|
28
|
+
// paint of MeasurementsStep.
|
|
29
|
+
const WINDOW_OVERSCAN = 8;
|
|
25
30
|
|
|
26
31
|
export function WheelPicker({
|
|
27
32
|
values,
|
|
@@ -44,23 +49,81 @@ export function WheelPicker({
|
|
|
44
49
|
return idx >= 0 ? idx : 0;
|
|
45
50
|
}, [values, selected]);
|
|
46
51
|
|
|
52
|
+
// Track the row currently centered under the selection overlay during scroll
|
|
53
|
+
// so the rendered window follows the user's drag, not just the committed
|
|
54
|
+
// `selected` prop (which only updates on momentum end).
|
|
55
|
+
const [windowCenter, setWindowCenter] = useState(selectedIndex);
|
|
56
|
+
|
|
47
57
|
useEffect(() => {
|
|
48
|
-
if (selectedIndex
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
if (selectedIndex === lastIndexRef.current) return;
|
|
59
|
+
const prev = lastIndexRef.current;
|
|
60
|
+
lastIndexRef.current = selectedIndex;
|
|
61
|
+
setWindowCenter(selectedIndex);
|
|
62
|
+
// Skip on the very first effect — contentOffset already positioned us.
|
|
63
|
+
if (prev === -1) return;
|
|
64
|
+
scrollRef.current?.scrollTo({ y: selectedIndex * itemHeight, animated: false });
|
|
54
65
|
}, [selectedIndex, itemHeight]);
|
|
55
66
|
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
67
|
+
const handleScroll = useCallback(
|
|
68
|
+
(e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
69
|
+
const offsetY = e.nativeEvent.contentOffset.y;
|
|
70
|
+
const index = Math.round(offsetY / itemHeight);
|
|
71
|
+
const clamped = Math.max(0, Math.min(values.length - 1, index));
|
|
72
|
+
setWindowCenter((prev) => (Math.abs(prev - clamped) >= 2 ? clamped : prev));
|
|
73
|
+
},
|
|
74
|
+
[itemHeight, values.length],
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const handleMomentumEnd = useCallback(
|
|
78
|
+
(e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
79
|
+
const offsetY = e.nativeEvent.contentOffset.y;
|
|
80
|
+
const index = Math.round(offsetY / itemHeight);
|
|
81
|
+
const clamped = Math.max(0, Math.min(values.length - 1, index));
|
|
82
|
+
setWindowCenter(clamped);
|
|
83
|
+
if (values[clamped] !== selected) {
|
|
84
|
+
onChange(values[clamped]);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
[itemHeight, values, selected, onChange],
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const windowStart = Math.max(0, windowCenter - WINDOW_OVERSCAN);
|
|
91
|
+
const windowEnd = Math.min(values.length - 1, windowCenter + WINDOW_OVERSCAN);
|
|
92
|
+
|
|
93
|
+
const dimColor = colors.text.tertiary;
|
|
94
|
+
const activeColor = colors.text.primary;
|
|
95
|
+
|
|
96
|
+
const items = [];
|
|
97
|
+
for (let i = windowStart; i <= windowEnd; i++) {
|
|
98
|
+
const value = values[i];
|
|
99
|
+
const isSelected = i === selectedIndex;
|
|
100
|
+
items.push(
|
|
101
|
+
<View
|
|
102
|
+
key={value}
|
|
103
|
+
style={[
|
|
104
|
+
styles.item,
|
|
105
|
+
{
|
|
106
|
+
position: 'absolute',
|
|
107
|
+
top: padding + i * itemHeight,
|
|
108
|
+
left: 0,
|
|
109
|
+
right: 0,
|
|
110
|
+
height: itemHeight,
|
|
111
|
+
},
|
|
112
|
+
]}
|
|
113
|
+
>
|
|
114
|
+
<Text
|
|
115
|
+
style={[
|
|
116
|
+
styles.itemText,
|
|
117
|
+
{ color: isSelected ? activeColor : dimColor },
|
|
118
|
+
]}
|
|
119
|
+
>
|
|
120
|
+
{formatLabel ? formatLabel(value) : String(value)}
|
|
121
|
+
</Text>
|
|
122
|
+
</View>,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const totalContentHeight = values.length * itemHeight + padding * 2;
|
|
64
127
|
|
|
65
128
|
return (
|
|
66
129
|
<View style={[styles.wrapper, { height: containerHeight, width, flex }]}>
|
|
@@ -76,28 +139,13 @@ export function WheelPicker({
|
|
|
76
139
|
showsVerticalScrollIndicator={false}
|
|
77
140
|
snapToInterval={itemHeight}
|
|
78
141
|
decelerationRate="fast"
|
|
79
|
-
|
|
142
|
+
onScroll={handleScroll}
|
|
143
|
+
scrollEventThrottle={32}
|
|
80
144
|
onMomentumScrollEnd={handleMomentumEnd}
|
|
81
145
|
nestedScrollEnabled
|
|
82
146
|
contentOffset={{ x: 0, y: selectedIndex * itemHeight }}
|
|
83
147
|
>
|
|
84
|
-
{
|
|
85
|
-
const isSelected = index === selectedIndex;
|
|
86
|
-
return (
|
|
87
|
-
<View key={item} style={[styles.item, { height: itemHeight }]}>
|
|
88
|
-
<Text
|
|
89
|
-
style={[
|
|
90
|
-
styles.itemText,
|
|
91
|
-
{
|
|
92
|
-
color: isSelected ? colors.text.primary : colors.text.tertiary,
|
|
93
|
-
},
|
|
94
|
-
]}
|
|
95
|
-
>
|
|
96
|
-
{formatLabel ? formatLabel(item) : String(item)}
|
|
97
|
-
</Text>
|
|
98
|
-
</View>
|
|
99
|
-
);
|
|
100
|
-
})}
|
|
148
|
+
<View style={{ height: totalContentHeight }}>{items}</View>
|
|
101
149
|
</ScrollView>
|
|
102
150
|
</View>
|
|
103
151
|
);
|