Skip to content

Commit ddf9ffa

Browse files
authored
[Web | iOS] Fix Switch component (#4112)
## Description This PR aims to align `Switch` behavior across platforms. ### iOS On `iOS` only first tap on switch would work, later calls would be suppressed. Turns out that `UISwitch` triggers only `UIControlEventTouchUpInside` and `UIControlEventValueChanged` callbacks - no `UIControlEventTouchDown`. This was verified on standalone, native iOS app in `SwiftUI`. To fix that, I've moved Gesture Handler events flow to `UIControlEventValueChanged`. ### web On web there's a problem with hierarchy - current implementation assumes that our `view` is `Switch`. In reality it is `div` wrapper that contains `input` and other stuff that build this component. ## Test plan <details> <summary>Tested on the following example</summary> ```tsx import { useState } from 'react'; import { StyleSheet, Switch } from 'react-native'; import { GestureDetector, GestureHandlerRootView, Switch as GestureSwitch, useNativeGesture, } from 'react-native-gesture-handler'; export default function App() { const g = useNativeGesture({ onBegin: () => { console.log('gesture begin'); }, onActivate: () => { console.log('gesture activate'); }, onUpdate: () => { console.log('gesture update'); }, onDeactivate: () => { console.log('gesture deactivate'); }, onFinalize: () => { console.log('gesture finalize'); }, }); const [enabled, setEnabled] = useState(false); return ( <GestureHandlerRootView style={styles.container}> <Switch value={enabled} onValueChange={setEnabled} style={{ backgroundColor: 'red' }} /> <GestureDetector gesture={g}> <Switch value={enabled} onValueChange={setEnabled} style={{ backgroundColor: 'green' }} /> </GestureDetector> <GestureSwitch onBegin={() => { console.log('gesture begin'); }} onActivate={() => { console.log('gesture activate'); }} onUpdate={() => { console.log('gesture update'); }} onDeactivate={() => { console.log('gesture deactivate'); }} onFinalize={() => { console.log('gesture finalize'); }} value={enabled} onValueChange={setEnabled} style={{ backgroundColor: 'blue' }} /> </GestureHandlerRootView> ); } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', }, }); ``` </details>
1 parent b18dbd1 commit ddf9ffa

2 files changed

Lines changed: 60 additions & 22 deletions

File tree

packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -139,22 +139,33 @@ - (void)bindToView:(UIView *)view
139139
// for properties like `disallowInterruption` to work.
140140
if ([view isKindOfClass:[UIControl class]]) {
141141
UIControl *control = (UIControl *)view;
142-
[control addTarget:self action:@selector(handleTouchDown:forEvent:) forControlEvents:UIControlEventTouchDown];
143-
[control addTarget:self
144-
action:@selector(handleTouchUpOutside:forEvent:)
145-
forControlEvents:UIControlEventTouchUpOutside];
146-
[control addTarget:self
147-
action:@selector(handleTouchUpInside:forEvent:)
148-
forControlEvents:UIControlEventTouchUpInside];
149-
[control addTarget:self action:@selector(handleDragExit:forEvent:) forControlEvents:UIControlEventTouchDragExit];
150-
[control addTarget:self
151-
action:@selector(handleDragInside:forEvent:)
152-
forControlEvents:UIControlEventTouchDragInside];
153-
[control addTarget:self
154-
action:@selector(handleDragOutside:forEvent:)
155-
forControlEvents:UIControlEventTouchDragOutside];
156-
[control addTarget:self action:@selector(handleDragEnter:forEvent:) forControlEvents:UIControlEventTouchDragEnter];
157-
[control addTarget:self action:@selector(handleTouchCancel:forEvent:) forControlEvents:UIControlEventTouchCancel];
142+
143+
// Pressing UISwitch triggers only touchUp and valueChanged callbacks. In order to align its behavior
144+
// with other UIControls, we have to dispatch full Gesture Handler events flow in one callback, as
145+
// touchesDown is not executed.
146+
if ([view isKindOfClass:[UISwitch class]]) {
147+
_pointerType = RNGestureHandlerTouch;
148+
[control addTarget:self action:@selector(handleSwitch:) forControlEvents:UIControlEventValueChanged];
149+
} else {
150+
[control addTarget:self action:@selector(handleTouchDown:forEvent:) forControlEvents:UIControlEventTouchDown];
151+
[control addTarget:self
152+
action:@selector(handleTouchUpOutside:forEvent:)
153+
forControlEvents:UIControlEventTouchUpOutside];
154+
[control addTarget:self
155+
action:@selector(handleTouchUpInside:forEvent:)
156+
forControlEvents:UIControlEventTouchUpInside];
157+
[control addTarget:self action:@selector(handleDragExit:forEvent:) forControlEvents:UIControlEventTouchDragExit];
158+
[control addTarget:self
159+
action:@selector(handleDragInside:forEvent:)
160+
forControlEvents:UIControlEventTouchDragInside];
161+
[control addTarget:self
162+
action:@selector(handleDragOutside:forEvent:)
163+
forControlEvents:UIControlEventTouchDragOutside];
164+
[control addTarget:self
165+
action:@selector(handleDragEnter:forEvent:)
166+
forControlEvents:UIControlEventTouchDragEnter];
167+
[control addTarget:self action:@selector(handleTouchCancel:forEvent:) forControlEvents:UIControlEventTouchCancel];
168+
}
158169
} else {
159170
[super bindToView:view];
160171
}
@@ -191,6 +202,27 @@ - (void)sendActiveStateEventIfChangedForView:(UIView *)sender extraData:(RNGestu
191202
[self sendEventsInState:RNGestureHandlerStateActive forViewWithTag:sender.reactTag withExtraData:extraData];
192203
}
193204

205+
- (void)handleSwitch:(UIView *)sender
206+
{
207+
[self sendEventsInState:RNGestureHandlerStateBegan
208+
forViewWithTag:sender.reactTag
209+
withExtraData:[RNGestureHandlerEventExtraData forPointerInside:YES
210+
withNumberOfTouches:1
211+
withPointerType:_pointerType]];
212+
[self sendEventsInState:RNGestureHandlerStateActive
213+
forViewWithTag:sender.reactTag
214+
withExtraData:[RNGestureHandlerEventExtraData forPointerInside:YES
215+
withNumberOfTouches:1
216+
withPointerType:_pointerType]];
217+
[self sendEventsInState:RNGestureHandlerStateEnd
218+
forViewWithTag:sender.reactTag
219+
withExtraData:[RNGestureHandlerEventExtraData forPointerInside:YES
220+
withNumberOfTouches:1
221+
withPointerType:_pointerType]];
222+
223+
[self reset];
224+
}
225+
194226
- (void)handleTouchDown:(UIView *)sender forEvent:(UIEvent *)event
195227
{
196228
[self setCurrentPointerTypeForEvent:event];

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type IGestureHandler from './IGestureHandler';
1414

1515
export default class NativeViewGestureHandler extends GestureHandler {
1616
private buttonRole!: boolean;
17+
private switchRole!: boolean;
1718

1819
// TODO: Implement logic for activation on start properly
1920
private shouldActivateOnStart = false;
@@ -49,6 +50,7 @@ export default class NativeViewGestureHandler extends GestureHandler {
4950

5051
this.restoreViewStyles(view);
5152
this.buttonRole = view.getAttribute('role') === 'button';
53+
this.switchRole = view.querySelector('input[role="switch"]') !== null;
5254
}
5355

5456
public override updateGestureConfig(config: Config): void {
@@ -101,7 +103,11 @@ export default class NativeViewGestureHandler extends GestureHandler {
101103
const view = this.delegate.view as HTMLElement;
102104
const isRNGHText = view.hasAttribute('rnghtext');
103105

104-
if ((this.buttonRole && this.shouldActivateOnStart) || isRNGHText) {
106+
if (
107+
(this.buttonRole && this.shouldActivateOnStart) ||
108+
this.switchRole ||
109+
isRNGHText
110+
) {
105111
this.activate();
106112
}
107113
}
@@ -114,11 +120,11 @@ export default class NativeViewGestureHandler extends GestureHandler {
114120
const dy = this.startY - lastCoords.y;
115121
const distSq = dx * dx + dy * dy;
116122

117-
if (
118-
distSq >= this.minDistSq &&
119-
!this.buttonRole &&
120-
this.state === State.BEGAN
121-
) {
123+
if (this.switchRole || this.buttonRole) {
124+
return;
125+
}
126+
127+
if (distSq >= this.minDistSq && this.state === State.BEGAN) {
122128
this.activate();
123129
}
124130
}

0 commit comments

Comments
 (0)