related-ui-components 4.2.2 → 4.2.4
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/module/components/Input/CountryPickerView.js +9 -5
- package/lib/module/components/Input/CountryPickerView.js.map +1 -1
- package/lib/module/components/Input/PhoneInput.js +5 -0
- package/lib/module/components/Input/PhoneInput.js.map +1 -1
- package/lib/module/components/RangeSlider/RangeSlider.js +171 -26
- package/lib/module/components/RangeSlider/RangeSlider.js.map +1 -1
- package/lib/module/components/RangeSlider/SliderLabel.js +93 -30
- package/lib/module/components/RangeSlider/SliderLabel.js.map +1 -1
- package/lib/module/contexts/UniversalModalProvider.js +2 -2
- package/lib/module/contexts/UniversalModalProvider.js.map +1 -1
- package/lib/typescript/src/components/Input/CountryPickerView.d.ts +4 -2
- package/lib/typescript/src/components/Input/CountryPickerView.d.ts.map +1 -1
- package/lib/typescript/src/components/Input/PhoneInput.d.ts +1 -0
- package/lib/typescript/src/components/Input/PhoneInput.d.ts.map +1 -1
- package/lib/typescript/src/components/RangeSlider/RangeSlider.d.ts +10 -4
- package/lib/typescript/src/components/RangeSlider/RangeSlider.d.ts.map +1 -1
- package/lib/typescript/src/components/RangeSlider/SliderLabel.d.ts.map +1 -1
- package/package.json +78 -78
- package/src/components/Input/CountryPickerView.tsx +12 -3
- package/src/components/Input/PhoneInput.tsx +7 -0
- package/src/components/RangeSlider/RangeSlider.tsx +233 -39
- package/src/components/RangeSlider/SliderLabel.tsx +122 -38
- package/src/contexts/UniversalModalProvider.tsx +2 -2
|
@@ -6,10 +6,12 @@ import Animated, {
|
|
|
6
6
|
useAnimatedStyle,
|
|
7
7
|
useSharedValue,
|
|
8
8
|
} from "react-native-reanimated";
|
|
9
|
-
import { SliderLabels } from "./SliderLabel";
|
|
10
9
|
import { ThemeType, useTheme } from "../../theme";
|
|
10
|
+
import { SliderLabels } from "./SliderLabel";
|
|
11
|
+
import { Input } from "../Input";
|
|
11
12
|
|
|
12
13
|
const THUMB_SIZE = 28;
|
|
14
|
+
const THUMB_RADIUS = THUMB_SIZE / 2;
|
|
13
15
|
const RAIL_HEIGHT = 6;
|
|
14
16
|
const LABEL_HEIGHT = 26;
|
|
15
17
|
|
|
@@ -25,11 +27,17 @@ interface RangeSliderProps {
|
|
|
25
27
|
onValueChange: (values: { min: number; max: number }) => void;
|
|
26
28
|
theme?: ThemeType;
|
|
27
29
|
scale?: ScaleType;
|
|
28
|
-
/**
|
|
29
|
-
* For percentile scale: array of all values in your dataset
|
|
30
|
-
* The slider will distribute positions based on data density
|
|
31
|
-
*/
|
|
32
30
|
dataPoints?: number[];
|
|
31
|
+
showCustomInputs?: boolean;
|
|
32
|
+
inputPlaceholders?: {
|
|
33
|
+
min?: string;
|
|
34
|
+
max?: string;
|
|
35
|
+
};
|
|
36
|
+
inputLabels?: {
|
|
37
|
+
min?: string;
|
|
38
|
+
max?: string;
|
|
39
|
+
};
|
|
40
|
+
isBottomSheet?: boolean;
|
|
33
41
|
}
|
|
34
42
|
|
|
35
43
|
/**
|
|
@@ -39,12 +47,10 @@ interface RangeSliderProps {
|
|
|
39
47
|
function createPercentileBreakpoints(data: number[]): number[] {
|
|
40
48
|
const sorted = [...new Set(data)].sort((a, b) => a - b);
|
|
41
49
|
|
|
42
|
-
// If small dataset, use all unique values
|
|
43
50
|
if (sorted.length <= 100) {
|
|
44
51
|
return sorted;
|
|
45
52
|
}
|
|
46
53
|
|
|
47
|
-
// For larger datasets, sample at percentile intervals
|
|
48
54
|
const breakpoints: number[] = [];
|
|
49
55
|
const numBreakpoints = 100;
|
|
50
56
|
|
|
@@ -90,6 +96,10 @@ const RangeSlider: React.FC<RangeSliderProps> = ({
|
|
|
90
96
|
theme,
|
|
91
97
|
scale = "linear",
|
|
92
98
|
dataPoints,
|
|
99
|
+
showCustomInputs = false,
|
|
100
|
+
inputPlaceholders,
|
|
101
|
+
inputLabels,
|
|
102
|
+
isBottomSheet = false,
|
|
93
103
|
}) => {
|
|
94
104
|
const { theme: defaultTheme } = useTheme();
|
|
95
105
|
const currTheme = theme || defaultTheme;
|
|
@@ -98,9 +108,20 @@ const RangeSlider: React.FC<RangeSliderProps> = ({
|
|
|
98
108
|
|
|
99
109
|
const [leftLabel, setLeftLabel] = useState(initialMinValue.toLocaleString());
|
|
100
110
|
const [rightLabel, setRightLabel] = useState(
|
|
101
|
-
initialMaxValue.toLocaleString()
|
|
111
|
+
initialMaxValue.toLocaleString(),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// State for custom input fields
|
|
115
|
+
const [minInputValue, setMinInputValue] = useState(
|
|
116
|
+
initialMinValue.toString(),
|
|
117
|
+
);
|
|
118
|
+
const [maxInputValue, setMaxInputValue] = useState(
|
|
119
|
+
initialMaxValue.toString(),
|
|
102
120
|
);
|
|
103
121
|
|
|
122
|
+
// The effective track width (where thumb CENTER can travel)
|
|
123
|
+
const effectiveWidth = sliderWidth - THUMB_SIZE;
|
|
124
|
+
|
|
104
125
|
// Pre-compute breakpoints for percentile scale
|
|
105
126
|
const breakpoints = useMemo(() => {
|
|
106
127
|
if (scale === "percentile" && dataPoints?.length) {
|
|
@@ -117,7 +138,6 @@ const RangeSlider: React.FC<RangeSliderProps> = ({
|
|
|
117
138
|
let percentage: number;
|
|
118
139
|
|
|
119
140
|
if (scale === "percentile" && breakpoints.length > 1) {
|
|
120
|
-
// Find where this value falls in the breakpoints
|
|
121
141
|
const index = findBreakpointIndex(breakpoints, value);
|
|
122
142
|
|
|
123
143
|
if (index === 0) {
|
|
@@ -125,7 +145,6 @@ const RangeSlider: React.FC<RangeSliderProps> = ({
|
|
|
125
145
|
} else if (index >= breakpoints.length - 1) {
|
|
126
146
|
percentage = 1;
|
|
127
147
|
} else {
|
|
128
|
-
// Interpolate between breakpoints
|
|
129
148
|
const lowerVal = breakpoints[index - 1];
|
|
130
149
|
const upperVal = breakpoints[index];
|
|
131
150
|
const lowerPct = (index - 1) / (breakpoints.length - 1);
|
|
@@ -147,41 +166,52 @@ const RangeSlider: React.FC<RangeSliderProps> = ({
|
|
|
147
166
|
percentage = (value - min) / (max - min);
|
|
148
167
|
}
|
|
149
168
|
|
|
150
|
-
|
|
169
|
+
// Map to effective width, offset by thumb radius
|
|
170
|
+
const position = percentage * effectiveWidth + THUMB_RADIUS;
|
|
171
|
+
return isRTL ? sliderWidth - position : position;
|
|
151
172
|
},
|
|
152
|
-
[min, max, safeMin, sliderWidth, isRTL, scale, breakpoints]
|
|
173
|
+
[min, max, safeMin, sliderWidth, effectiveWidth, isRTL, scale, breakpoints],
|
|
153
174
|
);
|
|
154
175
|
|
|
155
176
|
const positionToValue = useCallback(
|
|
156
177
|
(position: number) => {
|
|
157
178
|
"worklet";
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
179
|
+
// Convert position to percentage using effective width
|
|
180
|
+
const adjustedPosition = isRTL ? sliderWidth - position : position;
|
|
181
|
+
const percentage = (adjustedPosition - THUMB_RADIUS) / effectiveWidth;
|
|
182
|
+
const clampedPercentage = Math.max(0, Math.min(1, percentage));
|
|
161
183
|
|
|
162
184
|
let rawValue: number;
|
|
163
185
|
|
|
164
186
|
if (scale === "percentile" && breakpoints.length > 1) {
|
|
165
|
-
|
|
166
|
-
const exactIndex = percentage * (breakpoints.length - 1);
|
|
187
|
+
const exactIndex = clampedPercentage * (breakpoints.length - 1);
|
|
167
188
|
const lowerIndex = Math.floor(exactIndex);
|
|
168
189
|
const upperIndex = Math.min(lowerIndex + 1, breakpoints.length - 1);
|
|
169
190
|
const ratio = exactIndex - lowerIndex;
|
|
170
|
-
|
|
171
191
|
rawValue =
|
|
172
192
|
breakpoints[lowerIndex] +
|
|
173
193
|
ratio * (breakpoints[upperIndex] - breakpoints[lowerIndex]);
|
|
174
194
|
} else if (scale === "logarithmic") {
|
|
175
195
|
const logMin = Math.log(safeMin);
|
|
176
196
|
const logMax = Math.log(max);
|
|
177
|
-
rawValue = Math.exp(logMin +
|
|
197
|
+
rawValue = Math.exp(logMin + clampedPercentage * (logMax - logMin));
|
|
178
198
|
} else {
|
|
179
|
-
rawValue =
|
|
199
|
+
rawValue = clampedPercentage * (max - min) + min;
|
|
180
200
|
}
|
|
181
201
|
|
|
182
202
|
return Math.round(rawValue / step) * step;
|
|
183
203
|
},
|
|
184
|
-
[
|
|
204
|
+
[
|
|
205
|
+
min,
|
|
206
|
+
max,
|
|
207
|
+
safeMin,
|
|
208
|
+
step,
|
|
209
|
+
sliderWidth,
|
|
210
|
+
effectiveWidth,
|
|
211
|
+
isRTL,
|
|
212
|
+
scale,
|
|
213
|
+
breakpoints,
|
|
214
|
+
],
|
|
185
215
|
);
|
|
186
216
|
|
|
187
217
|
const leftPosition = useSharedValue(valueToPosition(initialMinValue));
|
|
@@ -206,21 +236,29 @@ const RangeSlider: React.FC<RangeSliderProps> = ({
|
|
|
206
236
|
const newPos = context.value.x + e.translationX;
|
|
207
237
|
|
|
208
238
|
if (activeThumb.value === "left") {
|
|
209
|
-
const lowerBound = isRTL
|
|
239
|
+
const lowerBound = isRTL
|
|
240
|
+
? rightPosition.value + THUMB_SIZE
|
|
241
|
+
: THUMB_RADIUS;
|
|
210
242
|
const upperBound = isRTL
|
|
211
|
-
? sliderWidth
|
|
243
|
+
? sliderWidth - THUMB_RADIUS
|
|
212
244
|
: rightPosition.value - THUMB_SIZE;
|
|
213
245
|
const clampedPos = Math.max(Math.min(newPos, upperBound), lowerBound);
|
|
214
246
|
leftPosition.value = clampedPos;
|
|
215
|
-
|
|
247
|
+
const newValue = positionToValue(clampedPos);
|
|
248
|
+
runOnJS(setLeftLabel)(newValue.toLocaleString());
|
|
249
|
+
runOnJS(setMinInputValue)(newValue.toString());
|
|
216
250
|
} else {
|
|
217
|
-
const lowerBound = isRTL
|
|
251
|
+
const lowerBound = isRTL
|
|
252
|
+
? THUMB_RADIUS
|
|
253
|
+
: leftPosition.value + THUMB_SIZE;
|
|
218
254
|
const upperBound = isRTL
|
|
219
255
|
? leftPosition.value - THUMB_SIZE
|
|
220
|
-
: sliderWidth;
|
|
256
|
+
: sliderWidth - THUMB_RADIUS;
|
|
221
257
|
const clampedPos = Math.max(Math.min(newPos, upperBound), lowerBound);
|
|
222
258
|
rightPosition.value = clampedPos;
|
|
223
|
-
|
|
259
|
+
const newValue = positionToValue(clampedPos);
|
|
260
|
+
runOnJS(setRightLabel)(newValue.toLocaleString());
|
|
261
|
+
runOnJS(setMaxInputValue)(newValue.toString());
|
|
224
262
|
}
|
|
225
263
|
})
|
|
226
264
|
.onEnd(() => {
|
|
@@ -231,12 +269,99 @@ const RangeSlider: React.FC<RangeSliderProps> = ({
|
|
|
231
269
|
activeThumb.value = null;
|
|
232
270
|
});
|
|
233
271
|
|
|
272
|
+
// Handlers for custom input fields
|
|
273
|
+
const handleMinInputChange = useCallback((text: string) => {
|
|
274
|
+
// Allow only numeric input (with optional decimal)
|
|
275
|
+
const sanitized = text.replace(/[^0-9.]/g, "");
|
|
276
|
+
setMinInputValue(sanitized);
|
|
277
|
+
}, []);
|
|
278
|
+
|
|
279
|
+
const handleMaxInputChange = useCallback((text: string) => {
|
|
280
|
+
const sanitized = text.replace(/[^0-9.]/g, "");
|
|
281
|
+
setMaxInputValue(sanitized);
|
|
282
|
+
}, []);
|
|
283
|
+
|
|
284
|
+
const handleMinInputSubmit = useCallback(() => {
|
|
285
|
+
const parsed = parseFloat(minInputValue);
|
|
286
|
+
if (isNaN(parsed)) {
|
|
287
|
+
// Reset to current slider value
|
|
288
|
+
const currentValue = positionToValue(leftPosition.value);
|
|
289
|
+
setMinInputValue(currentValue.toString());
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Clamp value: must be >= min and <= current max value
|
|
294
|
+
const currentMaxValue = positionToValue(rightPosition.value);
|
|
295
|
+
const clampedValue = Math.max(
|
|
296
|
+
min,
|
|
297
|
+
Math.min(parsed, currentMaxValue - step),
|
|
298
|
+
);
|
|
299
|
+
const steppedValue = Math.round(clampedValue / step) * step;
|
|
300
|
+
|
|
301
|
+
// Update input display
|
|
302
|
+
setMinInputValue(steppedValue.toString());
|
|
303
|
+
setLeftLabel(steppedValue.toLocaleString());
|
|
304
|
+
|
|
305
|
+
// Update slider position
|
|
306
|
+
leftPosition.value = valueToPosition(steppedValue);
|
|
307
|
+
|
|
308
|
+
// Trigger callback
|
|
309
|
+
onValueChange({ min: steppedValue, max: currentMaxValue });
|
|
310
|
+
}, [
|
|
311
|
+
minInputValue,
|
|
312
|
+
min,
|
|
313
|
+
step,
|
|
314
|
+
leftPosition,
|
|
315
|
+
rightPosition,
|
|
316
|
+
positionToValue,
|
|
317
|
+
valueToPosition,
|
|
318
|
+
onValueChange,
|
|
319
|
+
]);
|
|
320
|
+
|
|
321
|
+
const handleMaxInputSubmit = useCallback(() => {
|
|
322
|
+
const parsed = parseFloat(maxInputValue);
|
|
323
|
+
if (isNaN(parsed)) {
|
|
324
|
+
// Reset to current slider value
|
|
325
|
+
const currentValue = positionToValue(rightPosition.value);
|
|
326
|
+
setMaxInputValue(currentValue.toString());
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Clamp value: must be <= max and >= current min value
|
|
331
|
+
const currentMinValue = positionToValue(leftPosition.value);
|
|
332
|
+
const clampedValue = Math.min(
|
|
333
|
+
max,
|
|
334
|
+
Math.max(parsed, currentMinValue + step),
|
|
335
|
+
);
|
|
336
|
+
const steppedValue = Math.round(clampedValue / step) * step;
|
|
337
|
+
|
|
338
|
+
// Update input display
|
|
339
|
+
setMaxInputValue(steppedValue.toString());
|
|
340
|
+
setRightLabel(steppedValue.toLocaleString());
|
|
341
|
+
|
|
342
|
+
// Update slider position
|
|
343
|
+
rightPosition.value = valueToPosition(steppedValue);
|
|
344
|
+
|
|
345
|
+
// Trigger callback
|
|
346
|
+
onValueChange({ min: currentMinValue, max: steppedValue });
|
|
347
|
+
}, [
|
|
348
|
+
maxInputValue,
|
|
349
|
+
max,
|
|
350
|
+
step,
|
|
351
|
+
leftPosition,
|
|
352
|
+
rightPosition,
|
|
353
|
+
positionToValue,
|
|
354
|
+
valueToPosition,
|
|
355
|
+
onValueChange,
|
|
356
|
+
]);
|
|
357
|
+
|
|
358
|
+
// Thumb position = center of thumb, so offset by radius to get left edge
|
|
234
359
|
const animatedLeftThumbStyle = useAnimatedStyle(() => ({
|
|
235
|
-
transform: [{ translateX: leftPosition.value -
|
|
360
|
+
transform: [{ translateX: leftPosition.value - THUMB_RADIUS }],
|
|
236
361
|
}));
|
|
237
362
|
|
|
238
363
|
const animatedRightThumbStyle = useAnimatedStyle(() => ({
|
|
239
|
-
transform: [{ translateX: rightPosition.value -
|
|
364
|
+
transform: [{ translateX: rightPosition.value - THUMB_RADIUS }],
|
|
240
365
|
}));
|
|
241
366
|
|
|
242
367
|
const animatedActiveRailStyle = useAnimatedStyle(() => {
|
|
@@ -249,15 +374,53 @@ const RangeSlider: React.FC<RangeSliderProps> = ({
|
|
|
249
374
|
});
|
|
250
375
|
|
|
251
376
|
return (
|
|
252
|
-
<View
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
377
|
+
<View
|
|
378
|
+
style={[
|
|
379
|
+
showCustomInputs ? styles.containerWithInputs : styles.container,
|
|
380
|
+
{ width: sliderWidth },
|
|
381
|
+
]}
|
|
382
|
+
>
|
|
383
|
+
{showCustomInputs ? (
|
|
384
|
+
<View style={styles.inputsContainer}>
|
|
385
|
+
<View style={styles.inputWrapper}>
|
|
386
|
+
<Input
|
|
387
|
+
label={inputLabels?.min ?? "Min"}
|
|
388
|
+
value={minInputValue}
|
|
389
|
+
onChangeText={handleMinInputChange}
|
|
390
|
+
onBlur={handleMinInputSubmit}
|
|
391
|
+
onSubmitEditing={handleMinInputSubmit}
|
|
392
|
+
keyboardType="numeric"
|
|
393
|
+
placeholder={inputPlaceholders?.min}
|
|
394
|
+
placeholderTextColor={currTheme.surfaceVariant}
|
|
395
|
+
returnKeyType="done"
|
|
396
|
+
isBottomSheet={isBottomSheet}
|
|
397
|
+
/>
|
|
398
|
+
</View>
|
|
399
|
+
<View style={styles.inputWrapper}>
|
|
400
|
+
<Input
|
|
401
|
+
label={inputLabels?.max ?? "Max"}
|
|
402
|
+
value={maxInputValue}
|
|
403
|
+
onChangeText={handleMaxInputChange}
|
|
404
|
+
onBlur={handleMaxInputSubmit}
|
|
405
|
+
onSubmitEditing={handleMaxInputSubmit}
|
|
406
|
+
keyboardType="numeric"
|
|
407
|
+
placeholder={inputPlaceholders?.max}
|
|
408
|
+
placeholderTextColor={currTheme.surfaceVariant}
|
|
409
|
+
returnKeyType="done"
|
|
410
|
+
isBottomSheet={isBottomSheet}
|
|
411
|
+
/>
|
|
412
|
+
</View>
|
|
413
|
+
</View>
|
|
414
|
+
) : (
|
|
415
|
+
<SliderLabels
|
|
416
|
+
leftValue={leftLabel}
|
|
417
|
+
rightValue={rightLabel}
|
|
418
|
+
leftPosition={leftPosition}
|
|
419
|
+
rightPosition={rightPosition}
|
|
420
|
+
sliderWidth={sliderWidth}
|
|
421
|
+
thumbSize={THUMB_SIZE}
|
|
422
|
+
/>
|
|
423
|
+
)}
|
|
261
424
|
|
|
262
425
|
<GestureDetector gesture={panGesture}>
|
|
263
426
|
<View style={styles.railContainer}>
|
|
@@ -289,6 +452,9 @@ const getStyles = (theme: ThemeType) =>
|
|
|
289
452
|
marginTop: LABEL_HEIGHT,
|
|
290
453
|
direction: "ltr",
|
|
291
454
|
},
|
|
455
|
+
containerWithInputs: {
|
|
456
|
+
direction: "ltr",
|
|
457
|
+
},
|
|
292
458
|
railContainer: {
|
|
293
459
|
justifyContent: "center",
|
|
294
460
|
height: THUMB_SIZE,
|
|
@@ -318,6 +484,34 @@ const getStyles = (theme: ThemeType) =>
|
|
|
318
484
|
borderColor: theme.primary,
|
|
319
485
|
borderWidth: 5,
|
|
320
486
|
},
|
|
487
|
+
inputsContainer: {
|
|
488
|
+
flexDirection: "row",
|
|
489
|
+
justifyContent: "space-between",
|
|
490
|
+
gap: 16,
|
|
491
|
+
marginBottom: 12,
|
|
492
|
+
direction: I18nManager.isRTL ? "rtl" : "ltr",
|
|
493
|
+
},
|
|
494
|
+
inputWrapper: {
|
|
495
|
+
flex: 1,
|
|
496
|
+
},
|
|
497
|
+
inputLabel: {
|
|
498
|
+
color: theme.onSurface,
|
|
499
|
+
fontSize: 13,
|
|
500
|
+
fontFamily: "DinMedium",
|
|
501
|
+
marginBottom: 6,
|
|
502
|
+
},
|
|
503
|
+
input: {
|
|
504
|
+
backgroundColor: theme.background,
|
|
505
|
+
paddingHorizontal: 16,
|
|
506
|
+
paddingVertical: 10,
|
|
507
|
+
borderRadius: 8,
|
|
508
|
+
borderWidth: 1,
|
|
509
|
+
borderColor: theme.surfaceVariant,
|
|
510
|
+
color: theme.onSurface,
|
|
511
|
+
fontSize: 15,
|
|
512
|
+
fontFamily: "DinBold",
|
|
513
|
+
textAlign: "center",
|
|
514
|
+
},
|
|
321
515
|
});
|
|
322
516
|
|
|
323
|
-
export default RangeSlider;
|
|
517
|
+
export default RangeSlider;
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { I18nManager, StyleSheet, Text, View } from "react-native";
|
|
3
3
|
import Animated, {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
SharedValue,
|
|
5
|
+
useAnimatedStyle,
|
|
6
|
+
useDerivedValue,
|
|
7
|
+
useSharedValue,
|
|
8
8
|
} from "react-native-reanimated";
|
|
9
9
|
import { ThemeType, useTheme } from "../../theme";
|
|
10
10
|
|
|
11
11
|
const LABEL_HEIGHT = 26;
|
|
12
|
+
const LABEL_PADDING = 8;
|
|
13
|
+
const MIN_GAP = 4;
|
|
12
14
|
|
|
13
15
|
interface SliderLabelsProps {
|
|
14
16
|
leftValue: string;
|
|
@@ -17,51 +19,100 @@ interface SliderLabelsProps {
|
|
|
17
19
|
rightPosition: SharedValue<number>;
|
|
18
20
|
sliderWidth: number;
|
|
19
21
|
thumbSize: number;
|
|
20
|
-
theme?: ThemeType
|
|
22
|
+
theme?: ThemeType;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
interface
|
|
25
|
+
interface SingleLabelProps {
|
|
24
26
|
value: string;
|
|
25
27
|
position: SharedValue<number>;
|
|
26
28
|
sliderWidth: number;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
onWidthChange: (width: number) => void;
|
|
30
|
+
isVisible: SharedValue<boolean>;
|
|
31
|
+
theme: ThemeType;
|
|
32
|
+
}
|
|
29
33
|
|
|
34
|
+
interface MergedLabelProps {
|
|
35
|
+
leftValue: string;
|
|
36
|
+
rightValue: string;
|
|
37
|
+
leftPosition: SharedValue<number>;
|
|
38
|
+
rightPosition: SharedValue<number>;
|
|
39
|
+
sliderWidth: number;
|
|
40
|
+
isVisible: SharedValue<boolean>;
|
|
41
|
+
theme: ThemeType;
|
|
30
42
|
}
|
|
31
43
|
|
|
32
|
-
const
|
|
44
|
+
const SingleLabel: React.FC<SingleLabelProps> = ({
|
|
33
45
|
value,
|
|
34
46
|
position,
|
|
35
47
|
sliderWidth,
|
|
36
|
-
|
|
37
|
-
|
|
48
|
+
onWidthChange,
|
|
49
|
+
isVisible,
|
|
50
|
+
theme,
|
|
38
51
|
}) => {
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const currTheme = theme || defaultTheme;
|
|
42
|
-
const styles = getStyles(currTheme);
|
|
52
|
+
const styles = getStyles(theme);
|
|
53
|
+
const localWidth = useSharedValue(0);
|
|
43
54
|
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const
|
|
55
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
56
|
+
const halfLabel = localWidth.value / 2;
|
|
57
|
+
const rawCenter = position.value;
|
|
58
|
+
const shifted = rawCenter - halfLabel;
|
|
59
|
+
const left = Math.min(Math.max(shifted, 0), sliderWidth - localWidth.value);
|
|
47
60
|
|
|
61
|
+
return {
|
|
62
|
+
left,
|
|
63
|
+
opacity: isVisible.value ? 1 : 0,
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Animated.View
|
|
69
|
+
style={[styles.labelContainer, animatedStyle]}
|
|
70
|
+
onLayout={(e) => {
|
|
71
|
+
const width = e.nativeEvent.layout.width;
|
|
72
|
+
localWidth.value = width;
|
|
73
|
+
onWidthChange(width);
|
|
74
|
+
}}
|
|
75
|
+
>
|
|
76
|
+
<Text style={styles.labelText}>{value}</Text>
|
|
77
|
+
</Animated.View>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const MergedLabel: React.FC<MergedLabelProps> = ({
|
|
82
|
+
leftValue,
|
|
83
|
+
rightValue,
|
|
84
|
+
leftPosition,
|
|
85
|
+
rightPosition,
|
|
86
|
+
sliderWidth,
|
|
87
|
+
isVisible,
|
|
88
|
+
theme,
|
|
89
|
+
}) => {
|
|
90
|
+
const styles = getStyles(theme);
|
|
91
|
+
const labelWidth = useSharedValue(0);
|
|
92
|
+
|
|
93
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
94
|
+
const centerPosition = (leftPosition.value + rightPosition.value) / 2;
|
|
95
|
+
const halfLabel = labelWidth.value / 2;
|
|
96
|
+
const shifted = centerPosition - halfLabel;
|
|
48
97
|
const left = Math.min(Math.max(shifted, 0), sliderWidth - labelWidth.value);
|
|
49
98
|
|
|
50
99
|
return {
|
|
51
|
-
left
|
|
100
|
+
left,
|
|
101
|
+
opacity: isVisible.value ? 1 : 0,
|
|
52
102
|
};
|
|
53
103
|
});
|
|
54
104
|
|
|
105
|
+
const displayText =
|
|
106
|
+
leftValue === rightValue ? leftValue : `${leftValue} - ${rightValue}`;
|
|
107
|
+
|
|
55
108
|
return (
|
|
56
109
|
<Animated.View
|
|
57
|
-
style={[styles.labelContainer,
|
|
110
|
+
style={[styles.labelContainer, animatedStyle]}
|
|
58
111
|
onLayout={(e) => {
|
|
59
112
|
labelWidth.value = e.nativeEvent.layout.width;
|
|
60
113
|
}}
|
|
61
114
|
>
|
|
62
|
-
<Text style={styles.labelText}>
|
|
63
|
-
{value}
|
|
64
|
-
</Text>
|
|
115
|
+
<Text style={styles.labelText}>{displayText}</Text>
|
|
65
116
|
</Animated.View>
|
|
66
117
|
);
|
|
67
118
|
};
|
|
@@ -77,29 +128,62 @@ export const SliderLabels: React.FC<SliderLabelsProps> = ({
|
|
|
77
128
|
}) => {
|
|
78
129
|
const { theme: defaultTheme } = useTheme();
|
|
79
130
|
const currTheme = theme || defaultTheme;
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
131
|
+
const isRTL = I18nManager.isRTL;
|
|
132
|
+
|
|
133
|
+
// Track label widths in parent for overlap calculation
|
|
134
|
+
const leftLabelWidth = useSharedValue(0);
|
|
135
|
+
const rightLabelWidth = useSharedValue(0);
|
|
136
|
+
|
|
137
|
+
const shouldMerge = useDerivedValue(() => {
|
|
138
|
+
const leftCenter = leftPosition.value;
|
|
139
|
+
const rightCenter = rightPosition.value;
|
|
140
|
+
|
|
141
|
+
const leftEnd = leftCenter + leftLabelWidth.value / 2;
|
|
142
|
+
const rightStart = rightCenter - rightLabelWidth.value / 2;
|
|
143
|
+
|
|
144
|
+
if (isRTL) {
|
|
145
|
+
const rtlLeftEnd = rightCenter + rightLabelWidth.value / 2;
|
|
146
|
+
const rtlRightStart = leftCenter - leftLabelWidth.value / 2;
|
|
147
|
+
return rtlRightStart - rtlLeftEnd < MIN_GAP;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return rightStart - leftEnd < MIN_GAP;
|
|
86
151
|
});
|
|
87
152
|
|
|
88
|
-
const
|
|
153
|
+
const showSeparate = useDerivedValue(() => !shouldMerge.value);
|
|
154
|
+
const showMerged = useDerivedValue(() => shouldMerge.value);
|
|
89
155
|
|
|
90
156
|
return (
|
|
91
|
-
<View style={
|
|
92
|
-
<
|
|
157
|
+
<View style={getStyles(currTheme).wrapper} pointerEvents="none">
|
|
158
|
+
<SingleLabel
|
|
93
159
|
value={leftValue}
|
|
94
|
-
|
|
95
|
-
position={isRTL ? leftThumbLeftPosition : leftPosition}
|
|
160
|
+
position={leftPosition}
|
|
96
161
|
sliderWidth={sliderWidth}
|
|
162
|
+
onWidthChange={(width) => {
|
|
163
|
+
leftLabelWidth.value = width;
|
|
164
|
+
}}
|
|
165
|
+
isVisible={showSeparate}
|
|
166
|
+
theme={currTheme}
|
|
97
167
|
/>
|
|
98
|
-
<
|
|
168
|
+
<SingleLabel
|
|
99
169
|
value={rightValue}
|
|
100
|
-
|
|
101
|
-
|
|
170
|
+
position={rightPosition}
|
|
171
|
+
sliderWidth={sliderWidth}
|
|
172
|
+
onWidthChange={(width) => {
|
|
173
|
+
rightLabelWidth.value = width;
|
|
174
|
+
}}
|
|
175
|
+
isVisible={showSeparate}
|
|
176
|
+
theme={currTheme}
|
|
177
|
+
/>
|
|
178
|
+
|
|
179
|
+
<MergedLabel
|
|
180
|
+
leftValue={leftValue}
|
|
181
|
+
rightValue={rightValue}
|
|
182
|
+
leftPosition={leftPosition}
|
|
183
|
+
rightPosition={rightPosition}
|
|
102
184
|
sliderWidth={sliderWidth}
|
|
185
|
+
isVisible={showMerged}
|
|
186
|
+
theme={currTheme}
|
|
103
187
|
/>
|
|
104
188
|
</View>
|
|
105
189
|
);
|
|
@@ -119,7 +203,7 @@ const getStyles = (theme: ThemeType) =>
|
|
|
119
203
|
},
|
|
120
204
|
labelText: {
|
|
121
205
|
backgroundColor: theme.surface,
|
|
122
|
-
paddingHorizontal:
|
|
206
|
+
paddingHorizontal: LABEL_PADDING,
|
|
123
207
|
paddingVertical: 4,
|
|
124
208
|
borderRadius: 4,
|
|
125
209
|
color: theme.onSurface,
|