@symbiote-native/slider 2.0.4 → 2.0.6

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.
@@ -0,0 +1,46 @@
1
+ import type {ColorValue, HostComponent, ViewProps} from 'react-native';
2
+ import {ImageSource, codegenNativeComponent} from 'react-native';
3
+ import type {
4
+ Float,
5
+ WithDefault,
6
+ DirectEventHandler,
7
+ BubblingEventHandler,
8
+ Double,
9
+ } from 'react-native/Libraries/Types/CodegenTypes';
10
+
11
+ type Event = Readonly<{
12
+ value: Float;
13
+ fromUser?: boolean;
14
+ }>;
15
+
16
+ export interface NativeProps extends ViewProps {
17
+ accessibilityUnits?: string;
18
+ accessibilityIncrements?: ReadonlyArray<string>;
19
+ disabled?: WithDefault<boolean, false>;
20
+ inverted?: WithDefault<boolean, false>;
21
+ vertical?: WithDefault<boolean, false>;
22
+ tapToSeek?: WithDefault<boolean, false>;
23
+ maximumTrackImage?: ImageSource;
24
+ maximumTrackTintColor?: ColorValue;
25
+ maximumValue?: Double;
26
+ minimumTrackImage?: ImageSource;
27
+ minimumTrackTintColor?: ColorValue;
28
+ minimumValue?: Double;
29
+ onChange?: BubblingEventHandler<Event>;
30
+ onRNCSliderSlidingStart?: DirectEventHandler<Event>;
31
+ onRNCSliderSlidingComplete?: DirectEventHandler<Event>;
32
+ onRNCSliderValueChange?: BubblingEventHandler<Event>;
33
+ step?: Double;
34
+ testID?: string;
35
+ thumbImage?: ImageSource;
36
+ thumbTintColor?: ColorValue;
37
+ thumbSize?: Float;
38
+ trackImage?: ImageSource;
39
+ value?: Float;
40
+ lowerLimit?: Float;
41
+ upperLimit?: Float;
42
+ }
43
+
44
+ export default codegenNativeComponent<NativeProps>('RNCSlider', {
45
+ interfaceOnly: true,
46
+ }) as HostComponent<NativeProps>;
@@ -0,0 +1,398 @@
1
+ import React, {RefObject, useCallback} from 'react';
2
+ import {
3
+ Animated,
4
+ View,
5
+ ColorValue,
6
+ ViewStyle,
7
+ GestureResponderEvent,
8
+ LayoutChangeEvent,
9
+ Image,
10
+ ImageSourcePropType,
11
+ } from 'react-native';
12
+ //@ts-ignore
13
+ import type {ImageSource} from 'react-native/Libraries/Image/ImageSource';
14
+ import {constants} from './utils/constants';
15
+
16
+ type Event = Readonly<{
17
+ nativeEvent: {
18
+ value: number;
19
+ };
20
+ }>;
21
+
22
+ type AnimationValues = {
23
+ val: Animated.Value;
24
+ min: Animated.Value;
25
+ max: Animated.Value;
26
+ diff: Animated.Value;
27
+ };
28
+
29
+ export interface Props {
30
+ value: number;
31
+ minimumValue: number;
32
+ maximumValue: number;
33
+ lowerLimit?: number;
34
+ upperLimit?: number;
35
+ step: number;
36
+ minimumTrackTintColor: ColorValue;
37
+ maximumTrackTintColor: ColorValue;
38
+ thumbTintColor: ColorValue;
39
+ thumbStyle: ViewStyle;
40
+ style: ViewStyle;
41
+ inverted: boolean;
42
+ disabled: boolean;
43
+ trackHeight: number;
44
+ thumbImage?: ImageSource;
45
+ onRNCSliderSlidingStart: (event: Event) => void;
46
+ onRNCSliderSlidingComplete: (event: Event) => void;
47
+ onRNCSliderValueChange: (event: Event) => void;
48
+ }
49
+
50
+ const valueToEvent = (value: number): Event => ({nativeEvent: {value}});
51
+
52
+ const RCTSliderWebComponent = React.forwardRef(
53
+ (
54
+ {
55
+ value: initialValue = 0,
56
+ minimumValue = 0,
57
+ maximumValue = 0,
58
+ lowerLimit = 0,
59
+ upperLimit = 0,
60
+ step = 1,
61
+ minimumTrackTintColor = '#009688',
62
+ maximumTrackTintColor = '#939393',
63
+ thumbTintColor = '#009688',
64
+ thumbStyle = {},
65
+ style = {},
66
+ inverted = false,
67
+ disabled = false,
68
+ trackHeight = 4,
69
+ thumbImage,
70
+ onRNCSliderSlidingStart = (_: Event) => {},
71
+ onRNCSliderSlidingComplete = (_: Event) => {},
72
+ onRNCSliderValueChange = (_: Event) => {},
73
+ ...others
74
+ }: Props,
75
+ forwardedRef: any,
76
+ ) => {
77
+ const containerSize = React.useRef({width: 0, height: 0});
78
+ const containerPositionX = React.useRef(0);
79
+ const containerRef = forwardedRef || React.createRef();
80
+ const containerPositionInvalidated = React.useRef(false);
81
+ const [value, setValue] = React.useState(initialValue || minimumValue);
82
+ const lastInitialValue = React.useRef<number>(0);
83
+ const animationValues = React.useRef<AnimationValues>({
84
+ val: new Animated.Value(value),
85
+ min: new Animated.Value(minimumValue),
86
+ max: new Animated.Value(maximumValue),
87
+ // make sure we never divide by 0
88
+ diff: new Animated.Value(maximumValue - minimumValue || 1),
89
+ }).current;
90
+
91
+ // update minimumValue & maximumValue animations
92
+ React.useEffect(() => {
93
+ animationValues.min.setValue(minimumValue);
94
+ animationValues.max.setValue(maximumValue);
95
+ // make sure we never divide by 0
96
+ animationValues.diff.setValue(maximumValue - minimumValue || 1);
97
+ }, [animationValues, minimumValue, maximumValue]);
98
+
99
+ // compute animated slider position based on animated value
100
+ const minPercent = React.useRef(
101
+ Animated.multiply(
102
+ new Animated.Value(100),
103
+ Animated.divide(
104
+ Animated.subtract(animationValues.val, animationValues.min),
105
+ animationValues.diff,
106
+ ),
107
+ ),
108
+ ).current;
109
+ const maxPercent = React.useRef(
110
+ Animated.subtract(new Animated.Value(100), minPercent),
111
+ ).current;
112
+
113
+ const onValueChange = useCallback(
114
+ (value: number) => {
115
+ onRNCSliderValueChange && onRNCSliderValueChange(valueToEvent(value));
116
+ },
117
+ [onRNCSliderValueChange],
118
+ );
119
+
120
+ const onSlidingStart = useCallback(
121
+ (value: number) => {
122
+ isUserInteracting.current = true;
123
+ onRNCSliderSlidingStart && onRNCSliderSlidingStart(valueToEvent(value));
124
+ },
125
+ [onRNCSliderSlidingStart],
126
+ );
127
+
128
+ const onSlidingComplete = useCallback(
129
+ (value: number) => {
130
+ isUserInteracting.current = false;
131
+ onRNCSliderSlidingComplete &&
132
+ onRNCSliderSlidingComplete(valueToEvent(value));
133
+ },
134
+ [onRNCSliderSlidingComplete],
135
+ );
136
+ // Add a ref to track user interaction
137
+ const isUserInteracting = React.useRef(false);
138
+ const updateValue = useCallback(
139
+ (newValue: number) => {
140
+ // Ensure that the value is correctly rounded
141
+ const hardRounded =
142
+ decimalPrecision.current < 20
143
+ ? Number.parseFloat(newValue.toFixed(decimalPrecision.current))
144
+ : newValue;
145
+
146
+ // Ensure that the new value is still between the bounds
147
+ const withinBounds = Math.max(
148
+ minimumValue,
149
+ Math.min(hardRounded, maximumValue),
150
+ );
151
+ if (value !== withinBounds) {
152
+ setValue(withinBounds);
153
+ if (isUserInteracting.current) {
154
+ onValueChange(withinBounds);
155
+ }
156
+ return withinBounds;
157
+ }
158
+ return hardRounded;
159
+ },
160
+ [minimumValue, maximumValue, value, onValueChange],
161
+ );
162
+
163
+ React.useLayoutEffect(() => {
164
+ // we have to do this check because `initialValue` gets default to `0` by
165
+ // Slider. If we don't this will get called every time `value` changes
166
+ // as `updateValue` is mutated when value changes. The result of not
167
+ // checking this is that the value constantly gets reset to `0` in
168
+ // contexts where `value` is not managed externally.
169
+ if (initialValue !== lastInitialValue.current) {
170
+ lastInitialValue.current = initialValue;
171
+ const newValue = updateValue(initialValue);
172
+ animationValues.val.setValue(newValue);
173
+ }
174
+ }, [initialValue, updateValue, animationValues]);
175
+
176
+ React.useEffect(() => {
177
+ const invalidateContainerPosition = () => {
178
+ containerPositionInvalidated.current = true;
179
+ };
180
+ const onDocumentScroll = (e: Event) => {
181
+ const isAlreadyInvalidated = !containerPositionInvalidated.current;
182
+ if (
183
+ isAlreadyInvalidated &&
184
+ containerRef.current &&
185
+ // @ts-ignore
186
+ e.target.contains(containerRef.current)
187
+ ) {
188
+ invalidateContainerPosition();
189
+ }
190
+ };
191
+ //@ts-ignore
192
+ window.addEventListener('resize', invalidateContainerPosition);
193
+ //@ts-ignore
194
+ document.addEventListener('scroll', onDocumentScroll, {capture: true});
195
+
196
+ return () => {
197
+ //@ts-ignore
198
+ window.removeEventListener('resize', invalidateContainerPosition);
199
+
200
+ //@ts-ignore
201
+ document.removeEventListener('scroll', onDocumentScroll, {
202
+ capture: true,
203
+ });
204
+ };
205
+ }, [containerRef]);
206
+
207
+ const containerStyle = [
208
+ {
209
+ flexGrow: 1,
210
+ flexShrink: 1,
211
+ flexBasis: 'auto',
212
+ flexDirection: 'row',
213
+ alignItems: 'center',
214
+ },
215
+ style,
216
+ ] as ViewStyle[];
217
+
218
+ const trackStyle = {
219
+ height: trackHeight,
220
+ borderRadius: trackHeight / 2,
221
+ userSelect: 'none',
222
+ };
223
+
224
+ const minimumTrackStyle = {
225
+ ...trackStyle,
226
+ backgroundColor: minimumTrackTintColor,
227
+ flexGrow: minPercent,
228
+ };
229
+
230
+ const maximumTrackStyle = {
231
+ ...trackStyle,
232
+ backgroundColor: maximumTrackTintColor,
233
+ flexGrow: maxPercent,
234
+ };
235
+
236
+ const thumbSize = constants.THUMB_SIZE;
237
+ const thumbViewStyle = [
238
+ {
239
+ width: thumbSize,
240
+ height: thumbSize,
241
+ backgroundColor: thumbTintColor,
242
+ zIndex: 1,
243
+ borderRadius: thumbSize / 2,
244
+ overflow: 'hidden',
245
+ },
246
+ thumbStyle,
247
+ ] as ViewStyle[];
248
+
249
+ const decimalPrecision = React.useRef(
250
+ calculatePrecision(minimumValue, maximumValue, step),
251
+ );
252
+ React.useEffect(() => {
253
+ decimalPrecision.current = calculatePrecision(
254
+ minimumValue,
255
+ maximumValue,
256
+ step,
257
+ );
258
+ }, [maximumValue, minimumValue, step]);
259
+
260
+ const updateContainerPositionX = () => {
261
+ const positionX = (
262
+ containerRef as RefObject<HTMLElement | undefined>
263
+ ).current?.getBoundingClientRect().x;
264
+ containerPositionX.current = positionX ?? 0;
265
+ };
266
+
267
+ const getValueFromNativeEvent = (pageX: number) => {
268
+ const adjustForThumbSize = (containerSize.current.width || 1) > thumbSize;
269
+ const width =
270
+ (containerSize.current.width || 1) -
271
+ (adjustForThumbSize ? thumbSize : 0);
272
+
273
+ if (containerPositionInvalidated.current) {
274
+ containerPositionInvalidated.current = false;
275
+ updateContainerPositionX();
276
+ }
277
+
278
+ const containerX =
279
+ containerPositionX.current + (adjustForThumbSize ? thumbSize / 2 : 0);
280
+ const lowerValue = minimumValue < lowerLimit ? lowerLimit : minimumValue;
281
+ const upperValue = maximumValue > upperLimit ? upperLimit : maximumValue;
282
+
283
+ if (pageX < containerX) {
284
+ return inverted ? upperValue : lowerValue;
285
+ } else if (pageX > containerX + width) {
286
+ return inverted ? lowerValue : upperValue;
287
+ } else {
288
+ const x = pageX - containerX;
289
+ const newValue = inverted
290
+ ? maximumValue - ((maximumValue - minimumValue) * x) / width
291
+ : minimumValue + ((maximumValue - minimumValue) * x) / width;
292
+
293
+ const valueAfterStep = step
294
+ ? Math.round(newValue / step) * step
295
+ : newValue;
296
+ const valueAfterLowerLimit =
297
+ valueAfterStep < lowerLimit ? lowerLimit : valueAfterStep;
298
+ const valueInLimitRange =
299
+ valueAfterLowerLimit > upperLimit ? upperLimit : valueAfterLowerLimit;
300
+ return valueInLimitRange;
301
+ }
302
+ };
303
+
304
+ const onTouchEnd = ({nativeEvent}: GestureResponderEvent) => {
305
+ const newValue = updateValue(getValueFromNativeEvent(nativeEvent.pageX));
306
+ animationValues.val.setValue(newValue);
307
+ onSlidingComplete(newValue);
308
+ };
309
+
310
+ const onMove = ({nativeEvent}: GestureResponderEvent) => {
311
+ const newValue = getValueFromNativeEvent(nativeEvent.pageX);
312
+ animationValues.val.setValue(newValue);
313
+ updateValue(newValue);
314
+ };
315
+
316
+ const accessibilityActions = (event: any) => {
317
+ const tenth = (maximumValue - minimumValue) / 10;
318
+ switch (event.nativeEvent.actionName) {
319
+ case 'increment':
320
+ updateValue(value + (step || tenth));
321
+ break;
322
+ case 'decrement':
323
+ updateValue(value - (step || tenth));
324
+ break;
325
+ }
326
+ };
327
+
328
+ React.useImperativeHandle(
329
+ forwardedRef,
330
+ () => ({
331
+ updateValue: (val: number) => {
332
+ updateValue(val);
333
+ },
334
+ }),
335
+ [updateValue],
336
+ );
337
+
338
+ return (
339
+ <View
340
+ ref={containerRef}
341
+ onLayout={({nativeEvent: {layout}}: LayoutChangeEvent) => {
342
+ containerSize.current.height = layout.height;
343
+ containerSize.current.width = layout.width;
344
+ if ((containerRef as RefObject<View>).current) {
345
+ updateContainerPositionX();
346
+ }
347
+ }}
348
+ accessibilityActions={[
349
+ {name: 'increment', label: 'increment'},
350
+ {name: 'decrement', label: 'decrement'},
351
+ ]}
352
+ onAccessibilityAction={accessibilityActions}
353
+ accessible={true}
354
+ accessibilityRole={'adjustable'}
355
+ style={containerStyle}
356
+ {...others}
357
+ // NOTE: gesture responders should all fall _after_ the {...others}
358
+ // spread operator, or they may not work appropriately.
359
+ onStartShouldSetResponder={() => !disabled}
360
+ onMoveShouldSetResponder={() => !disabled}
361
+ onResponderGrant={() => onSlidingStart(value)}
362
+ onResponderRelease={onTouchEnd}
363
+ onResponderMove={onMove}>
364
+ <Animated.View pointerEvents="none" style={minimumTrackStyle} />
365
+ <View pointerEvents="none" style={thumbViewStyle}>
366
+ {thumbImage !== undefined ? (
367
+ <Image
368
+ source={thumbImage as ImageSourcePropType}
369
+ style={{width: '100%', height: '100%'}}
370
+ />
371
+ ) : null}
372
+ </View>
373
+ <Animated.View pointerEvents="none" style={maximumTrackStyle} />
374
+ </View>
375
+ );
376
+ },
377
+ );
378
+
379
+ // We should round number with the same precision as the min, max or step values if provided
380
+ function calculatePrecision(
381
+ minimumValue: number,
382
+ maximumValue: number,
383
+ step: number,
384
+ ) {
385
+ if (!step) {
386
+ return Infinity;
387
+ } else {
388
+ // Calculate the number of decimals we can encounter in the results
389
+ const decimals = [minimumValue, maximumValue, step].map(
390
+ (value) => ((value + '').split('.').pop() || '').length,
391
+ );
392
+ return Math.max(...decimals);
393
+ }
394
+ }
395
+
396
+ RCTSliderWebComponent.displayName = 'RTCSliderWebComponent';
397
+
398
+ export default RCTSliderWebComponent;
@@ -0,0 +1,356 @@
1
+ import React, {useEffect, useState} from 'react';
2
+ import {
3
+ Image,
4
+ Platform,
5
+ AccessibilityActionEvent,
6
+ ViewProps,
7
+ ViewStyle,
8
+ ColorValue,
9
+ NativeSyntheticEvent,
10
+ StyleProp,
11
+ View,
12
+ ImageSource,
13
+ ImageSourcePropType,
14
+ } from 'react-native';
15
+ import RCTSliderNativeComponent from './index';
16
+
17
+ import type {FC, Ref} from 'react';
18
+ import {MarkerProps} from './components/TrackMark';
19
+ import {StepsIndicator} from './components/StepsIndicator';
20
+ import {styles} from './utils/styles';
21
+ import {constants} from './utils/constants';
22
+
23
+ type Event = NativeSyntheticEvent<
24
+ Readonly<{
25
+ value: number;
26
+ /**
27
+ * Android Only.
28
+ */
29
+ fromUser?: boolean;
30
+ }>
31
+ >;
32
+
33
+ type WindowsProps = Readonly<{
34
+ /**
35
+ * If true the slider will be inverted.
36
+ * Default value is false.
37
+ */
38
+ vertical?: boolean;
39
+ }>;
40
+
41
+ type IOSProps = Readonly<{
42
+ /**
43
+ * Assigns a single image for the track. Only static images are supported.
44
+ * The center pixel of the image will be stretched to fill the track.
45
+ */
46
+ trackImage?: ImageSource;
47
+
48
+ /**
49
+ * Assigns a minimum track image. Only static images are supported. The
50
+ * rightmost pixel of the image will be stretched to fill the track.
51
+ */
52
+ minimumTrackImage?: ImageSource;
53
+
54
+ /**
55
+ * Assigns a maximum track image. Only static images are supported. The
56
+ * leftmost pixel of the image will be stretched to fill the track.
57
+ */
58
+ maximumTrackImage?: ImageSource;
59
+
60
+ /**
61
+ * Permits tapping on the slider track to set the thumb position.
62
+ * Defaults to false on iOS. No effect on Android or Windows.
63
+ */
64
+ tapToSeek?: boolean;
65
+ }>;
66
+
67
+ type Props = ViewProps &
68
+ IOSProps &
69
+ WindowsProps &
70
+ Readonly<{
71
+ /**
72
+ * Used to style and layout the `Slider`. See `StyleSheet.js` and
73
+ * `DeprecatedViewStylePropTypes.js` for more info.
74
+ */
75
+ style?: StyleProp<ViewStyle>;
76
+
77
+ /**
78
+ * Write-only property representing the value of the slider.
79
+ * Can be used to programmatically control the position of the thumb.
80
+ * Entered once at the beginning still acts as an initial value.
81
+ * The value should be between minimumValue and maximumValue,
82
+ * which default to 0 and 1 respectively.
83
+ * Default value is 0.
84
+ *
85
+ * This is not a controlled component, you don't need to update the
86
+ * value during dragging.
87
+ */
88
+ value?: number;
89
+
90
+ /**
91
+ * Step value of the slider. The value should be
92
+ * between 0 and (maximumValue - minimumValue).
93
+ * Default value is 0.
94
+ */
95
+ step?: number;
96
+
97
+ /**
98
+ * Initial minimum value of the slider. Default value is 0.
99
+ */
100
+ minimumValue?: number;
101
+
102
+ /**
103
+ * Initial maximum value of the slider. Default value is 1.
104
+ */
105
+ maximumValue?: number;
106
+
107
+ /**
108
+ * The lower limit value of the slider. The user won't be able to slide below this limit.
109
+ */
110
+ lowerLimit?: number;
111
+
112
+ /**
113
+ * The upper limit value of the slider. The user won't be able to slide above this limit.
114
+ */
115
+ upperLimit?: number;
116
+
117
+ /**
118
+ * The color used for the track to the left of the button.
119
+ * Overrides the default blue gradient image on iOS.
120
+ */
121
+ minimumTrackTintColor?: ColorValue;
122
+
123
+ /**
124
+ * The color used for the track to the right of the button.
125
+ * Overrides the default blue gradient image on iOS.
126
+ */
127
+ maximumTrackTintColor?: ColorValue;
128
+ /**
129
+ * The color used to tint the default thumb images on iOS, or the
130
+ * color of the foreground switch grip on Android.
131
+ */
132
+ thumbTintColor?: ColorValue;
133
+
134
+ /**
135
+ * If true the user won't be able to move the slider.
136
+ * Default value is false.
137
+ */
138
+ disabled?: boolean;
139
+
140
+ /**
141
+ * Callback continuously called while the user is dragging the slider.
142
+ */
143
+ onValueChange?: (_value: number) => void;
144
+
145
+ /**
146
+ * Callback that is called when the user touches the slider,
147
+ * regardless if the value has changed. The current value is passed
148
+ * as an argument to the callback handler.
149
+ */
150
+ onSlidingStart?: (_value: number) => void;
151
+
152
+ /**
153
+ * Callback that is called when the user releases the slider,
154
+ * regardless if the value has changed. The current value is passed
155
+ * as an argument to the callback handler.
156
+ */
157
+ onSlidingComplete?: (_value: number) => void;
158
+
159
+ /**
160
+ * Used to locate this view in UI automation tests.
161
+ */
162
+ testID?: string;
163
+
164
+ /**
165
+ * Sets an image for the thumb. Only static images are supported.
166
+ */
167
+ thumbImage?: ImageSource;
168
+
169
+ /**
170
+ * Sets the size (width and height) of the thumb.
171
+ * If `thumbImage` is provided, it will be scaled to this size.
172
+ */
173
+ thumbSize?: number;
174
+
175
+ /**
176
+ * If true the slider will be inverted.
177
+ * Default value is false.
178
+ */
179
+ inverted?: boolean;
180
+
181
+ /**
182
+ * Component to be rendered for each step indicator.
183
+ */
184
+ StepMarker?: FC<MarkerProps>;
185
+
186
+ /**
187
+ *
188
+ */
189
+ renderStepNumber?: boolean;
190
+
191
+ /**
192
+ * A string of one or more words to be announced by the screen reader.
193
+ * Otherwise, it will announce the value as a percentage.
194
+ * Requires passing a value to `accessibilityIncrements` to work correctly.
195
+ * Should be a plural word, as singular units will be handled.
196
+ */
197
+ accessibilityUnits?: string;
198
+
199
+ /**
200
+ * An array of values that represent the different increments displayed
201
+ * by the slider. All the values passed into this prop must be strings.
202
+ * Requires passing a value to `accessibilityUnits` to work correctly.
203
+ * The number of elements must be the same as `maximumValue`.
204
+ */
205
+ accessibilityIncrements?: Array<string>;
206
+ }>;
207
+
208
+ const SliderComponent = (
209
+ {
210
+ onValueChange,
211
+ onSlidingStart,
212
+ onSlidingComplete,
213
+ onAccessibilityAction,
214
+ value = constants.SLIDER_DEFAULT_INITIAL_VALUE,
215
+ minimumValue = 0,
216
+ maximumValue = 1,
217
+ step = 0,
218
+ inverted = false,
219
+ tapToSeek = false,
220
+ lowerLimit = Platform.select({
221
+ web: minimumValue,
222
+ default: constants.LIMIT_MIN_VALUE,
223
+ }),
224
+ upperLimit = Platform.select({
225
+ web: maximumValue,
226
+ default: constants.LIMIT_MAX_VALUE,
227
+ }),
228
+ ...props
229
+ }: Props,
230
+ forwardedRef?: Ref<typeof RCTSliderNativeComponent>,
231
+ ) => {
232
+ const [currentValue, setCurrentValue] = useState(
233
+ value ?? minimumValue ?? constants.SLIDER_DEFAULT_INITIAL_VALUE,
234
+ );
235
+ const [width, setWidth] = useState(0);
236
+
237
+ const stepResolution = step ? step : constants.DEFAULT_STEP_RESOLUTION;
238
+
239
+ const defaultStep = (maximumValue - minimumValue) / stepResolution;
240
+ const stepLength = step || defaultStep;
241
+
242
+ const options = Array.from(
243
+ {
244
+ length: (step ? defaultStep : stepResolution) + 1,
245
+ },
246
+ (_, index) => minimumValue + index * stepLength,
247
+ );
248
+
249
+ const defaultStyle =
250
+ Platform.OS === 'ios' ? styles.defaultSlideriOS : styles.defaultSlider;
251
+ const sliderStyle = {zIndex: 1, width: width};
252
+ const style = [defaultStyle, props.style];
253
+
254
+ const onValueChangeEvent = (event: Event) => {
255
+ onValueChange && onValueChange(event.nativeEvent.value);
256
+ setCurrentValue(event.nativeEvent.value);
257
+ };
258
+
259
+ const _disabled =
260
+ typeof props.disabled === 'boolean'
261
+ ? props.disabled
262
+ : props.accessibilityState?.disabled === true;
263
+
264
+ const _accessibilityState =
265
+ typeof props.disabled === 'boolean'
266
+ ? {...props.accessibilityState, disabled: props.disabled}
267
+ : props.accessibilityState;
268
+
269
+ const onSlidingStartEvent = onSlidingStart
270
+ ? (event: Event) => {
271
+ onSlidingStart(event.nativeEvent.value);
272
+ }
273
+ : null;
274
+ const onSlidingCompleteEvent = onSlidingComplete
275
+ ? (event: Event) => {
276
+ onSlidingComplete(event.nativeEvent.value);
277
+ }
278
+ : null;
279
+ const onAccessibilityActionEvent = onAccessibilityAction
280
+ ? (event: AccessibilityActionEvent) => {
281
+ onAccessibilityAction(event);
282
+ }
283
+ : null;
284
+
285
+ const passedValue = Number.isNaN(value) || !value ? undefined : value;
286
+
287
+ useEffect(() => {
288
+ if (lowerLimit >= upperLimit) {
289
+ console.warn(
290
+ 'Invalid configuration: lower limit is supposed to be smaller than upper limit',
291
+ );
292
+ }
293
+ }, [lowerLimit, upperLimit]);
294
+
295
+ return (
296
+ <View
297
+ onLayout={(event) => {
298
+ setWidth(event.nativeEvent.layout.width);
299
+ }}
300
+ style={[style, {justifyContent: 'center'}]}>
301
+ {props.StepMarker || !!props.renderStepNumber ? (
302
+ <StepsIndicator
303
+ options={options}
304
+ sliderWidth={width}
305
+ currentValue={currentValue}
306
+ renderStepNumber={props.renderStepNumber}
307
+ thumbImage={props.thumbImage}
308
+ StepMarker={props.StepMarker}
309
+ isLTR={inverted}
310
+ />
311
+ ) : null}
312
+ <RCTSliderNativeComponent
313
+ {...props}
314
+ minimumValue={minimumValue}
315
+ maximumValue={maximumValue}
316
+ step={step}
317
+ inverted={inverted}
318
+ tapToSeek={tapToSeek}
319
+ value={passedValue}
320
+ lowerLimit={lowerLimit}
321
+ upperLimit={upperLimit}
322
+ accessibilityState={_accessibilityState}
323
+ thumbImage={
324
+ Platform.OS === 'web'
325
+ ? props.thumbImage
326
+ : props.StepMarker || !props.thumbImage
327
+ ? undefined
328
+ : Image.resolveAssetSource(props.thumbImage as ImageSourcePropType)
329
+ }
330
+ ref={forwardedRef}
331
+ style={[
332
+ sliderStyle,
333
+ defaultStyle,
334
+ {alignContent: 'center', alignItems: 'center'},
335
+ ]}
336
+ onChange={onValueChangeEvent}
337
+ onRNCSliderSlidingStart={onSlidingStartEvent}
338
+ onRNCSliderSlidingComplete={onSlidingCompleteEvent}
339
+ onRNCSliderValueChange={onValueChangeEvent}
340
+ disabled={_disabled}
341
+ onStartShouldSetResponder={() => true}
342
+ onResponderTerminationRequest={() => false}
343
+ onRNCSliderAccessibilityAction={onAccessibilityActionEvent}
344
+ thumbTintColor={
345
+ props.thumbImage && !!props.StepMarker
346
+ ? 'transparent'
347
+ : props.thumbTintColor
348
+ }
349
+ />
350
+ </View>
351
+ );
352
+ };
353
+
354
+ const SliderWithRef = React.forwardRef(SliderComponent);
355
+
356
+ export default SliderWithRef;
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ import {StyleProp, Text, TextStyle, View} from 'react-native';
3
+ import {styles} from '../utils/styles';
4
+
5
+ export const StepNumber = ({
6
+ i,
7
+ index,
8
+ style,
9
+ }: {
10
+ i: number;
11
+ index: number;
12
+ style: StyleProp<TextStyle>;
13
+ }) => {
14
+ return (
15
+ <View style={styles.stepNumber}>
16
+ <Text testID={`${index}th-step`} style={style}>
17
+ {i}
18
+ </Text>
19
+ </View>
20
+ );
21
+ };
@@ -0,0 +1,105 @@
1
+ import React, {FC, Fragment, useCallback, useMemo} from 'react';
2
+ import {Platform, View} from 'react-native';
3
+ import {StepNumber} from './StepNumber';
4
+ import {MarkerProps, SliderTrackMark} from './TrackMark';
5
+ //@ts-ignore
6
+ import type {ImageSource} from 'react-native/Libraries/Image/ImageSource';
7
+ import {styles} from '../utils/styles';
8
+ import {constants} from '../utils/constants';
9
+
10
+ export const StepsIndicator = ({
11
+ options,
12
+ sliderWidth,
13
+ currentValue,
14
+ StepMarker,
15
+ renderStepNumber,
16
+ thumbImage,
17
+ isLTR,
18
+ }: {
19
+ options: number[];
20
+ sliderWidth: number;
21
+ currentValue: number;
22
+ StepMarker?: FC<MarkerProps>;
23
+ renderStepNumber?: boolean;
24
+ thumbImage?: ImageSource;
25
+ isLTR?: boolean;
26
+ }) => {
27
+ const stepNumberFontStyle = useMemo(() => {
28
+ return {
29
+ fontSize:
30
+ options.length > 9
31
+ ? constants.STEP_NUMBER_TEXT_FONT_SMALL
32
+ : constants.STEP_NUMBER_TEXT_FONT_BIG,
33
+ };
34
+ }, [options.length]);
35
+
36
+ const platformDependentStyles = useMemo(() => {
37
+ const isWeb = Platform.OS === 'web';
38
+ return {
39
+ stepIndicatorContainerStyle: isWeb
40
+ ? styles.stepsIndicator
41
+ : {
42
+ ...styles.stepsIndicator,
43
+ marginHorizontal: sliderWidth * constants.MARGIN_HORIZONTAL_PADDING,
44
+ },
45
+ stepIndicatorElementStyle: isWeb
46
+ ? {
47
+ ...styles.stepIndicatorElement,
48
+ width: constants.THUMB_SIZE,
49
+ justifyContent: 'space-between' as const,
50
+ }
51
+ : styles.stepIndicatorElement,
52
+ };
53
+ }, [sliderWidth]);
54
+
55
+ const values = isLTR ? options.reverse() : options;
56
+
57
+ const renderStepIndicator = useCallback(
58
+ (i: number, index: number) => {
59
+ return (
60
+ <Fragment key={index}>
61
+ <View
62
+ style={platformDependentStyles.stepIndicatorElementStyle}
63
+ key={`${index}-View`}>
64
+ <SliderTrackMark
65
+ key={`${index}-SliderTrackMark`}
66
+ isTrue={currentValue === i}
67
+ index={i}
68
+ thumbImage={thumbImage}
69
+ StepMarker={StepMarker}
70
+ currentValue={currentValue}
71
+ min={options[0]}
72
+ max={options[options.length - 1]}
73
+ />
74
+ {renderStepNumber ? (
75
+ <StepNumber
76
+ i={i}
77
+ index={index}
78
+ style={stepNumberFontStyle}
79
+ key={`${index}th-step`}
80
+ />
81
+ ) : null}
82
+ </View>
83
+ </Fragment>
84
+ );
85
+ },
86
+ [
87
+ currentValue,
88
+ StepMarker,
89
+ options,
90
+ thumbImage,
91
+ renderStepNumber,
92
+ stepNumberFontStyle,
93
+ platformDependentStyles.stepIndicatorElementStyle,
94
+ ],
95
+ );
96
+
97
+ return (
98
+ <View
99
+ pointerEvents="none"
100
+ testID="StepsIndicator-Container"
101
+ style={platformDependentStyles.stepIndicatorContainerStyle}>
102
+ {values.map((i, index) => renderStepIndicator(i, index))}
103
+ </View>
104
+ );
105
+ };
@@ -0,0 +1,55 @@
1
+ import React, {FC} from 'react';
2
+ import {Image, ImageSource, ImageSourcePropType, View} from 'react-native';
3
+ import {styles} from '../utils/styles';
4
+
5
+ export type MarkerProps = {
6
+ stepMarked: boolean;
7
+ currentValue: number;
8
+ index: number;
9
+ min: number;
10
+ max: number;
11
+ };
12
+
13
+ export type TrackMarksProps = {
14
+ isTrue: boolean;
15
+ index: number;
16
+ thumbImage?: ImageSource;
17
+ StepMarker?: FC<MarkerProps>;
18
+ currentValue: number;
19
+ min: number;
20
+ max: number;
21
+ };
22
+
23
+ export const SliderTrackMark = ({
24
+ isTrue,
25
+ index,
26
+ thumbImage,
27
+ StepMarker,
28
+ currentValue,
29
+ min,
30
+ max,
31
+ }: TrackMarksProps) => {
32
+ return (
33
+ <View style={styles.trackMarkContainer}>
34
+ {StepMarker ? (
35
+ <StepMarker
36
+ stepMarked={isTrue}
37
+ index={index}
38
+ currentValue={currentValue}
39
+ min={min}
40
+ max={max}
41
+ />
42
+ ) : null}
43
+ {thumbImage && isTrue ? (
44
+ <View
45
+ style={styles.thumbImageContainer}
46
+ testID="sliderTrackMark-thumbImage">
47
+ <Image
48
+ source={thumbImage as ImageSourcePropType}
49
+ style={styles.thumbImage}
50
+ />
51
+ </View>
52
+ ) : null}
53
+ </View>
54
+ );
55
+ };
@@ -0,0 +1,3 @@
1
+ const RNCSlider = require('./RNCSliderNativeComponent').default;
2
+
3
+ export default RNCSlider;
@@ -0,0 +1,13 @@
1
+ import {Platform} from 'react-native';
2
+
3
+ export const constants = {
4
+ SLIDER_DEFAULT_INITIAL_VALUE: 0,
5
+ MARGIN_HORIZONTAL_PADDING: 0.05,
6
+ // Default thumb size for web platform (used in step indicator positioning)
7
+ THUMB_SIZE: 20,
8
+ STEP_NUMBER_TEXT_FONT_SMALL: 8,
9
+ STEP_NUMBER_TEXT_FONT_BIG: 12,
10
+ LIMIT_MIN_VALUE: Number.MIN_SAFE_INTEGER,
11
+ LIMIT_MAX_VALUE: Number.MAX_SAFE_INTEGER,
12
+ DEFAULT_STEP_RESOLUTION: Platform.OS === 'android' ? 128 : 1000,
13
+ };
@@ -0,0 +1,55 @@
1
+ import {Platform, StyleSheet} from 'react-native';
2
+
3
+ export const styles = StyleSheet.create({
4
+ stepNumber: {
5
+ marginTop: 20,
6
+ alignItems: 'center',
7
+ position: 'absolute',
8
+ },
9
+ sliderMainContainer: {zIndex: 1, width: '100%'},
10
+ defaultSlideriOS: {
11
+ height: 40,
12
+ },
13
+ defaultSlider: {},
14
+ stepsIndicator: {
15
+ flex: 1,
16
+ flexDirection: 'row',
17
+ justifyContent: 'space-between',
18
+ top: Platform.OS === 'ios' ? 10 : 0,
19
+ zIndex: 2,
20
+ },
21
+ trackMarkContainer: {
22
+ alignItems: 'center',
23
+ alignContent: 'center',
24
+ alignSelf: 'center',
25
+ justifyContent: 'center',
26
+ position: 'absolute',
27
+ zIndex: 3,
28
+ },
29
+ thumbImageContainer: {
30
+ position: 'absolute',
31
+ zIndex: 3,
32
+ justifyContent: 'center',
33
+ alignItems: 'center',
34
+ alignContent: 'center',
35
+ },
36
+ thumbImage: {
37
+ alignContent: 'center',
38
+ alignItems: 'center',
39
+ position: 'absolute',
40
+ },
41
+ stepIndicatorElement: {
42
+ alignItems: 'center',
43
+ alignContent: 'center',
44
+ },
45
+ defaultIndicatorMarked: {
46
+ height: 20,
47
+ width: 5,
48
+ backgroundColor: '#CCCCCC',
49
+ },
50
+ defaultIndicatorIdle: {
51
+ height: 10,
52
+ width: 2,
53
+ backgroundColor: '#C0C0C0',
54
+ },
55
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbiote-native/slider",
3
- "version": "2.0.4",
3
+ "version": "2.0.6",
4
4
  "description": "Third-party native Slider view wrapped for SymbioteNative — one framework-agnostic core, built once and reachable from the React, Vue, and Angular adapters.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -39,6 +39,7 @@
39
39
  "src",
40
40
  "build",
41
41
  "build-ngc",
42
+ "codegen-specs",
42
43
  "react-native.config.cjs",
43
44
  "symbiote-slider.podspec"
44
45
  ],
@@ -48,7 +49,7 @@
48
49
  "codegenConfig": {
49
50
  "name": "RNCSlider",
50
51
  "type": "components",
51
- "jsSrcsDir": "node_modules/@react-native-community/slider/src",
52
+ "jsSrcsDir": "codegen-specs",
52
53
  "android": {
53
54
  "javaPackageName": "com.reactnativecommunity.slider"
54
55
  },
@@ -70,7 +71,7 @@
70
71
  "react-native": ">=0.86",
71
72
  "vue": ">=3.5.0",
72
73
  "@symbiote-native/angular": "0.3.3",
73
- "@symbiote-native/vue": "0.3.3",
74
+ "@symbiote-native/vue": "0.3.4",
74
75
  "@symbiote-native/react": "0.2.4"
75
76
  },
76
77
  "peerDependenciesMeta": {
@@ -105,10 +106,10 @@
105
106
  "@vue/runtime-core": "^3.5.13",
106
107
  "react": "19.2.3",
107
108
  "typescript": "~6.0.0",
108
- "@symbiote-native/react": "0.2.4",
109
109
  "@symbiote-native/test-utils": "0.1.5",
110
+ "@symbiote-native/react": "0.2.4",
110
111
  "@symbiote-native/angular": "0.3.3",
111
- "@symbiote-native/vue": "0.3.3"
112
+ "@symbiote-native/vue": "0.3.4"
112
113
  },
113
114
  "scripts": {
114
115
  "typecheck": "tsc --build",