From 98c88a318341c4aa77134d112b6f05e0b58bd9c0 Mon Sep 17 00:00:00 2001 From: adids1221 Date: Thu, 9 Apr 2026 11:50:01 +0300 Subject: [PATCH 1/3] [Incubator.Slider] Fix Android hitSlop by adding hitSlop to Pan gesture Added .hitSlop() to the RNGH Gesture.Pan() on the thumb so the gesture handler natively recognizes touches in the expanded hit area on Android. Also added hitSlop to the Pan gesture mock in jest setup. Co-Authored-By: Claude Sonnet 4.6 --- packages/react-native-ui-lib/jestSetup/jest-setup.js | 1 + packages/react-native-ui-lib/src/incubator/slider/Thumb.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/react-native-ui-lib/jestSetup/jest-setup.js b/packages/react-native-ui-lib/jestSetup/jest-setup.js index ee9e0f5325..97c32e1614 100644 --- a/packages/react-native-ui-lib/jestSetup/jest-setup.js +++ b/packages/react-native-ui-lib/jestSetup/jest-setup.js @@ -77,6 +77,7 @@ jest.mock('react-native-gesture-handler', PanMock.onFinalize = getDefaultMockedHandler('onFinalize'); PanMock.activateAfterLongPress = getDefaultMockedHandler('activateAfterLongPress'); PanMock.enabled = getDefaultMockedHandler('enabled'); + PanMock.hitSlop = getDefaultMockedHandler('hitSlop'); PanMock.onTouchesMove = getDefaultMockedHandler('onTouchesMove'); PanMock.prepare = jest.fn(); PanMock.initialize = jest.fn(); diff --git a/packages/react-native-ui-lib/src/incubator/slider/Thumb.tsx b/packages/react-native-ui-lib/src/incubator/slider/Thumb.tsx index 9e7aa67526..d8b374f591 100644 --- a/packages/react-native-ui-lib/src/incubator/slider/Thumb.tsx +++ b/packages/react-native-ui-lib/src/incubator/slider/Thumb.tsx @@ -62,6 +62,7 @@ const Thumb = (props: ThumbProps) => { const lastOffset = useSharedValue(0); const gesture = Gesture.Pan() + .hitSlop(hitSlop) .onBegin(() => { onSeekStart?.(); isPressed.value = true; From 2e0783f94e45e36af856f10068e8b7c922c1bac8 Mon Sep 17 00:00:00 2001 From: adids1221 Date: Thu, 9 Apr 2026 12:03:11 +0300 Subject: [PATCH 2/3] [Incubator.Slider] Add useRelativeDrag prop for relative drag mode Adds a container-level RNGH Gesture.Pan() that moves the thumb relative to its current position when useRelativeDrag is true. The thumb gets pointerEvents="none" so the container gesture takes over, and isActive is passed to keep the thumb's active styling in sync. Co-Authored-By: Claude Sonnet 4.6 --- .../src/incubator/slider/Thumb.tsx | 13 +++- .../src/incubator/slider/index.tsx | 77 +++++++++++++++++-- 2 files changed, 79 insertions(+), 11 deletions(-) diff --git a/packages/react-native-ui-lib/src/incubator/slider/Thumb.tsx b/packages/react-native-ui-lib/src/incubator/slider/Thumb.tsx index d8b374f591..f1e1620310 100644 --- a/packages/react-native-ui-lib/src/incubator/slider/Thumb.tsx +++ b/packages/react-native-ui-lib/src/incubator/slider/Thumb.tsx @@ -24,6 +24,7 @@ interface ThumbProps extends ViewProps { onSeekStart?: () => void; onSeekEnd?: () => void; enableShadow?: boolean; + isActive?: SharedValue; } const SHADOW_RADIUS = 4; @@ -53,7 +54,9 @@ const Thumb = (props: ThumbProps) => { stepInterpolatedValue, gap = 0, secondary, - enableShadow + enableShadow, + pointerEvents, + isActive } = props; const rtlFix = Constants.isRTL ? -1 : 1; @@ -93,15 +96,16 @@ const Thumb = (props: ThumbProps) => { offset.value = Math.round(offset.value / stepInterpolatedValue.value) * stepInterpolatedValue.value; } }); - gesture.enabled(!disabled); + gesture.enabled(!disabled && pointerEvents !== 'none'); const animatedStyle = useAnimatedStyle(() => { - const customStyle = isPressed.value ? activeStyle?.value : defaultStyle?.value; + const active = isPressed.value || isActive?.value; + const customStyle = active ? activeStyle?.value : defaultStyle?.value; return { ...customStyle, transform: [ {translateX: (offset.value - thumbSize.value.width / 2) * rtlFix}, - {scale: withSpring(!disableActiveStyling && isPressed.value ? 1.3 : 1)} + {scale: withSpring(!disableActiveStyling && active ? 1.3 : 1)} ] }; }); @@ -117,6 +121,7 @@ const Thumb = (props: ThumbProps) => { { accessible = true, testID, enableThumbShadow = true, + useRelativeDrag, throttleTime = 200 } = themeProps; @@ -373,6 +380,42 @@ const Slider = React.memo((props: Props) => { } }; + const containerDragStartOffset = useSharedValue(0); + const isContainerDragging = useSharedValue(false); + + const clampOffset = (offset: number) => { + 'worklet'; + return Math.max(0, Math.min(trackSize.value.width, offset)); + }; + + const snapToStep = () => { + 'worklet'; + if (shouldBounceToStep) { + const step = stepInterpolatedValue.value; + defaultThumbOffset.value = Math.round(defaultThumbOffset.value / step) * step; + } + }; + + const containerGesture = Gesture.Pan() + .onBegin(() => { + containerDragStartOffset.value = defaultThumbOffset.value; + isContainerDragging.value = true; + _onSeekStart(); + }) + .onUpdate(e => { + if (trackSize.value.width === 0) { + return; + } + const dx = e.translationX * (shouldDisableRTL ? 1 : rtlFix); + defaultThumbOffset.value = clampOffset(containerDragStartOffset.value + dx); + }) + .onEnd(() => _onSeekEnd()) + .onFinalize(() => { + isContainerDragging.value = false; + snapToStep(); + }); + containerGesture.enabled(!disabled && !!useRelativeDrag); + const trackAnimatedStyles = useAnimatedStyle(() => { if (useRange) { return { @@ -399,6 +442,8 @@ const Slider = React.memo((props: Props) => { onSeekEnd={_onSeekEnd} shouldDisableRTL={shouldDisableRTL} disabled={disabled} + pointerEvents={useRelativeDrag ? 'none' : undefined} + isActive={useRelativeDrag ? isContainerDragging : undefined} disableActiveStyling={disableActiveStyling} defaultStyle={_thumbStyle} activeStyle={_activeThumbStyle} @@ -415,7 +460,7 @@ const Slider = React.memo((props: Props) => { { ); }; + const renderSliderContent = () => ( + <> + {_renderTrack()} + {renderThumb(ThumbType.DEFAULT)} + {useRange && renderThumb(ThumbType.RANGE)} + + ); + return ( - {_renderTrack()} - {renderThumb(ThumbType.DEFAULT)} - {useRange && renderThumb(ThumbType.RANGE)} + {useRelativeDrag ? ( + + + {renderSliderContent()} + + + ) : ( + renderSliderContent() + )} ); }); @@ -446,6 +505,10 @@ const styles = StyleSheet.create({ height: THUMB_SIZE + SHADOW_RADIUS, justifyContent: 'center' }, + gestureContainer: { + flex: 1, + justifyContent: 'center' + }, disableRTL: { transform: [{scaleX: -1}] }, From efb49ce6fe809a77054e514b958b8e8c6131d168 Mon Sep 17 00:00:00 2001 From: adids1221 Date: Tue, 21 Apr 2026 12:46:10 +0300 Subject: [PATCH 3/3] Removed pointerEvents, fixed thumb not pressable and reviewer notes --- .../react-native-ui-lib/src/incubator/slider/Thumb.tsx | 8 +++----- .../react-native-ui-lib/src/incubator/slider/index.tsx | 5 ++--- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/react-native-ui-lib/src/incubator/slider/Thumb.tsx b/packages/react-native-ui-lib/src/incubator/slider/Thumb.tsx index f1e1620310..6bc3e5de2e 100644 --- a/packages/react-native-ui-lib/src/incubator/slider/Thumb.tsx +++ b/packages/react-native-ui-lib/src/incubator/slider/Thumb.tsx @@ -24,7 +24,7 @@ interface ThumbProps extends ViewProps { onSeekStart?: () => void; onSeekEnd?: () => void; enableShadow?: boolean; - isActive?: SharedValue; + isActive: SharedValue; } const SHADOW_RADIUS = 4; @@ -55,7 +55,6 @@ const Thumb = (props: ThumbProps) => { gap = 0, secondary, enableShadow, - pointerEvents, isActive } = props; @@ -96,10 +95,10 @@ const Thumb = (props: ThumbProps) => { offset.value = Math.round(offset.value / stepInterpolatedValue.value) * stepInterpolatedValue.value; } }); - gesture.enabled(!disabled && pointerEvents !== 'none'); + gesture.enabled(!disabled); const animatedStyle = useAnimatedStyle(() => { - const active = isPressed.value || isActive?.value; + const active = isPressed.value || isActive.value; const customStyle = active ? activeStyle?.value : defaultStyle?.value; return { ...customStyle, @@ -121,7 +120,6 @@ const Thumb = (props: ThumbProps) => { { onSeekEnd={_onSeekEnd} shouldDisableRTL={shouldDisableRTL} disabled={disabled} - pointerEvents={useRelativeDrag ? 'none' : undefined} - isActive={useRelativeDrag ? isContainerDragging : undefined} + isActive={isContainerDragging} disableActiveStyling={disableActiveStyling} defaultStyle={_thumbStyle} activeStyle={_activeThumbStyle} @@ -460,7 +459,7 @@ const Slider = React.memo((props: Props) => {