Skip to content

[Web | iOS] Fix Switch component#4112

Merged
m-bert merged 7 commits intomainfrom
@mbert/switch
Apr 23, 2026
Merged

[Web | iOS] Fix Switch component#4112
m-bert merged 7 commits intomainfrom
@mbert/switch

Conversation

@m-bert
Copy link
Copy Markdown
Collaborator

@m-bert m-bert commented Apr 23, 2026

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

Tested on the following example
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',
  },
});

Copilot AI review requested due to automatic review settings April 23, 2026 09:24

export default class NativeViewGestureHandler extends GestureHandler {
private buttonRole!: boolean;
private switchRole!: boolean;
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally I'd like to keep this as one variable. It doesn't make much sense now, but I'll investigate it later (possibly with our TextInput) and move it to one variable to keep role in one place, not using this "one-hot" tactic.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Aligns Switch gesture-handler behavior across iOS and web by adapting platform-specific event/hierarchy differences so RNGH can consistently activate and dispatch its state flow for Switch.

Changes:

  • Web: Detect Switch via an input[role="switch"] within the wrapper element and treat it like a “button-like” control for immediate activation.
  • iOS: Special-case UISwitch by dispatching RNGH state transitions from a control event callback instead of relying on TouchDown.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
packages/react-native-gesture-handler/src/web/handlers/NativeViewGestureHandler.ts Adds switch-role detection and adjusts activation/move handling to support the web Switch DOM structure.
packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm Adds UISwitch special handling by routing gesture event flow through a new handleSwitch callback.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm Outdated
Comment thread packages/react-native-gesture-handler/apple/Handlers/RNNativeViewHandler.mm Outdated
@m-bert m-bert requested a review from j-piasecki April 23, 2026 10:30
@m-bert m-bert merged commit ddf9ffa into main Apr 23, 2026
5 checks passed
@m-bert m-bert deleted the @mbert/switch branch April 23, 2026 13:38
m-bert added a commit that referenced this pull request Apr 27, 2026
## Description

In #4112 I've adjusted `Native` gesture for `Switch` component on web.
Using `querySelector` on `view` broke our example app, as now it looks
for `input` element in the whole subtree.

To fix this, I've added [`:scope
>`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:scope#using_scope_in_javascript)
which limits `querySelector` only to direct children.

## Test plan

`expo-example` on web
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants