Skip to content

Commit b15a63f

Browse files
authored
[General] Pass null hitSlop instead of coercing it to zero (#4356)
## Description `Touchable` normalized a missing `hitSlop` into `{ top: 0, left: 0, bottom: 0, right: 0 }`, so a button with no `hitSlop` was still configured with an explicit — if empty — one. That forces the handler to hit-test against a rect identical to its frame instead of keeping its unset default, and it makes removing the prop indistinguishable from setting it to `0`. `hitSlop` is now passed through as `null` when unset, and every platform treats that as "reset to the default": - **Android** — `GestureHandler.setHitSlop` accepts `null` and restores `DEFAULT_HIT_SLOP`; `handleHitSlopProperty` checks `config.isNull` first. The button view manager forwards the `null` to the managed handler instead of skipping the update entirely. - **iOS/macOS** — an `NSNull` in the config resets `_hitSlop` to `RNGHHitSlopEmpty`. The Fabric component view keeps skipping the key entirely when all edges are `0`, since the generated props struct cannot carry null (added a comment explaining why). - **Web** — `config.hitSlop ?? undefined`, so `undefined` still means "not part of this update" while `null` clears it. Prop types on `GestureHandlerButton` and the web `Config` were widened to allow `null`. ## Test plan - Ran a `Touchable` with and without `hitSlop`, and toggling the prop on/off at runtime, on Android, iOS and web — the touch area matches the frame when the prop is removed. - `yarn ts-check`, `yarn lint:js`, `yarn test`
1 parent 8958861 commit b15a63f

9 files changed

Lines changed: 33 additions & 17 deletions

File tree

packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandler.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,13 @@ open class GestureHandler {
197197
}
198198
}
199199

200-
fun setHitSlop(padding: Float) = setHitSlop(padding, padding, padding, padding, HIT_SLOP_NONE, HIT_SLOP_NONE)
200+
fun setHitSlop(padding: Float?) {
201+
if (padding == null) {
202+
hitSlop = DEFAULT_HIT_SLOP
203+
} else {
204+
setHitSlop(padding, padding, padding, padding, HIT_SLOP_NONE, HIT_SLOP_NONE)
205+
}
206+
}
201207

202208
fun setInteractionController(controller: GestureHandlerInteractionController?) {
203209
interactionController = controller
@@ -989,7 +995,9 @@ open class GestureHandler {
989995
private const val KEY_CANCELS_JS_RESPONDER = "cancelsJSResponder"
990996

991997
private fun handleHitSlopProperty(handler: GestureHandler, config: ReadableMap) {
992-
if (config.getType(KEY_HIT_SLOP) == ReadableType.Number) {
998+
if (config.isNull(KEY_HIT_SLOP)) {
999+
handler.setHitSlop(null)
1000+
} else if (config.getType(KEY_HIT_SLOP) == ReadableType.Number) {
9931001
val hitSlop = PixelUtil.toPixelFromDIP(config.getDouble(KEY_HIT_SLOP))
9941002
handler.setHitSlop(
9951003
hitSlop,

packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerButtonViewManager.kt

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -137,14 +137,12 @@ class RNGestureHandlerButtonViewManager :
137137
@ReactProp(name = "gestureHitSlop")
138138
override fun setGestureHitSlop(view: ButtonViewGroup, gestureHitSlop: ReadableMap?) {
139139
view.managedHandlerHitSlop = gestureHitSlop
140-
if (gestureHitSlop != null) {
141-
updateManagedHandlerConfig(
142-
view,
143-
Arguments.createMap().apply {
144-
putMap("hitSlop", gestureHitSlop)
145-
},
146-
)
147-
}
140+
updateManagedHandlerConfig(
141+
view,
142+
Arguments.createMap().apply {
143+
putMap("hitSlop", gestureHitSlop)
144+
},
145+
)
148146
}
149147

150148
@ReactProp(name = "hasLongPressHandler")

packages/react-native-gesture-handler/apple/RNGestureHandler.mm

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ - (void)updateConfig:(NSDictionary *)config
169169
prop = config[@"hitSlop"];
170170
if ([prop isKindOfClass:[NSNumber class]]) {
171171
_hitSlop.left = _hitSlop.right = _hitSlop.top = _hitSlop.bottom = [prop doubleValue];
172+
} else if ([prop isKindOfClass:[NSNull class]]) {
173+
_hitSlop = RNGHHitSlopEmpty;
172174
} else if (prop != nil) {
173175
_hitSlop.left = _hitSlop.right = RNGH_HIT_SLOP_GET(@"horizontal");
174176
_hitSlop.top = _hitSlop.bottom = RNGH_HIT_SLOP_GET(@"vertical");

packages/react-native-gesture-handler/apple/RNGestureHandlerButtonComponentView.mm

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,10 @@ - (NSDictionary *)buildManagedHandlerConfig:(const RNGestureHandlerButtonProps &
273273
config[@"testID"] = RCTNSStringFromString(props.gestureTestID);
274274
}
275275

276+
// `gestureHitSlop` is optional on the JS side, but the generated props struct cannot carry
277+
// null — an unset (or explicitly nulled out) prop arrives zero-initialized, i.e. 0 on every
278+
// edge. That is equivalent to no hit slop, so the key is left out of the config entirely
279+
// and the handler keeps its unset default instead of hit-testing against an identical frame.
276280
if (props.gestureHitSlop.top != 0 || props.gestureHitSlop.left != 0 || props.gestureHitSlop.bottom != 0 ||
277281
props.gestureHitSlop.right != 0) {
278282
config[@"hitSlop"] = @{

packages/react-native-gesture-handler/src/components/GestureHandlerButton.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export interface ButtonProps extends ViewProps, AccessibilityProps {
3333
bottom?: number | undefined;
3434
right?: number | undefined;
3535
}
36+
| null
3637
| undefined;
3738

3839
/**

packages/react-native-gesture-handler/src/components/GestureHandlerButton.web.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ type ButtonProps = ViewProps & {
4848
bottom?: number;
4949
right?: number;
5050
}
51+
| null
5152
| undefined;
5253
onButtonPress?:
5354
| ((event: NativeSyntheticEvent<ButtonEvent>) => void)

packages/react-native-gesture-handler/src/v3/components/Touchable/Touchable.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -162,12 +162,12 @@ export const Touchable = (props: TouchableProps) => {
162162
const tvProps = getTVProps(rest);
163163

164164
const hitSlop =
165-
typeof props.hitSlop === 'number' || props.hitSlop == null
165+
typeof props.hitSlop === 'number'
166166
? {
167-
top: props.hitSlop ?? 0,
168-
left: props.hitSlop ?? 0,
169-
bottom: props.hitSlop ?? 0,
170-
right: props.hitSlop ?? 0,
167+
top: props.hitSlop,
168+
left: props.hitSlop,
169+
bottom: props.hitSlop,
170+
right: props.hitSlop,
171171
}
172172
: props.hitSlop;
173173

packages/react-native-gesture-handler/src/web/handlers/GestureHandler.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -791,8 +791,10 @@ export default abstract class GestureHandler implements IGestureHandler {
791791
public updateGestureConfig(config: Partial<Config>): void {
792792
const enabledChanged = this.maybeUpdateEnabled(config.enabled);
793793

794+
// An explicit `null` clears the hit slop (no expansion on any edge),
795+
// `undefined` means the property was not part of this update.
794796
if (config.hitSlop !== undefined) {
795-
this.hitSlop = config.hitSlop;
797+
this.hitSlop = config.hitSlop ?? undefined;
796798
this.validateHitSlops();
797799
}
798800

packages/react-native-gesture-handler/src/web/interfaces.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export interface Config extends Record<string, ConfigArgs> {
4949
simultaneousHandlers?: Handler[] | null | undefined;
5050
waitFor?: Handler[] | null | undefined;
5151
blocksHandlers?: Handler[] | null | undefined;
52-
hitSlop?: HitSlop | undefined;
52+
hitSlop?: HitSlop | null | undefined;
5353
shouldCancelWhenOutside?: boolean | undefined;
5454
userSelect?: UserSelect | undefined;
5555
activeCursor?: ActiveCursor | undefined;

0 commit comments

Comments
 (0)