Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can touch out side a View Component be detected in react native?

My React native application screen has View component with few text inputs. How can touch be detected on screen outside that View? Please help.

Thanks

like image 436
user3300203 Avatar asked Oct 17 '16 08:10

user3300203


People also ask

How do you detect touch outside a view in React Native?

As Andrew said: You can wrap your View with TouchableWithoutFeedback and adding a onPress you can detect when the view is tapped. Another way to achieve that is having responses for touch events from the view.

Can we use onPress on view in React Native?

If you are using react native version greater than 0.55. 3 then you can add onPress kind of functionality to any View component using onStartShouldSetResponder prop. This prop makes the view responds on the start of touch- which is strikingly similar to the onPress prop of other components.

How do I turn off touch on view React Native?

To disable a View component in React Native, we can set the pointerEvents prop to 'none' . to set the inner view's pointerEvents prop to 'none' so that users can't interact with the content inside.


3 Answers

As Andrew said: You can wrap your View with TouchableWithoutFeedback and adding a onPress you can detect when the view is tapped.

Another way to achieve that is having responses for touch events from the view.

 /* Methods that handled the events */
handlePressIn(event) {
  // Do stuff when the view is touched
}

handlePressOut(event) {
    // Do stuff when the the touch event is finished
}

...

    <View
      onStartShouldSetResponder={(evt) => true}
      onMoveShouldSetResponder={(evt) => true}
      onResponderGrant={this.handlePressIn}
      onResponderMove={this.handlePressIn}
      onResponderRelease={this.handlePressOut}
    >
         ...
    </View>

The difference between Grant and move is that Grant is just when the user press, and Move is when the user is pressing and moving the position of the press

like image 92
Robert Juamarcal Avatar answered Oct 10 '22 02:10

Robert Juamarcal


I don't take no for an answer, so I dug up a lot to find a solution matching my needs.

  • In my situation I have multiple components which need to collapse when I open another one.
  • This behavior has to be automatic, and easy to code-in by any contributor.
  • Passing parent refs to the children or calling a special global method are not acceptable solutions in my circumstances.
  • Using a transparent background to catch all clicks will not cut it.

This Question perfectly illustrates the need.

Demo

Here is the final result. Clicking anywhere except the component itself will collapse it.

enter image description here

WARNING The solution includes usage of private React components properties. I know the inherent risks of using such an approach and I'm happy to use them as long as my app does what I expect and all other constraints are satisfied. Short disclaimer, probably a smarter, cleaner solution exists out there. This is the best I could do with my own limited knowledge of React.

First we need to capture all click in the UI, both for Web and Native. It seems that this is not easily done. Nested TouchableOpacityseem to allow only one responder at a time. So I had to improvise a bit here.

app.tsx (trimmed down to essentials)

import * as div from './app.style';
import { screenClicked, screenTouched } from './shared/services/self-close-signal.service';
// ... other imports

class App extends React.Component<Props, State> {

    public render() {

        return (
            <div.AppSafeArea 
                onTouchStart={e => screenTouched(e)}
                onClick={e => screenClicked(e)}>

                {/* App Routes */}
                <>{appRoutes(loginResponse)}</>

            </div.AppSafeArea>
        );
    }
}

self-close-signal.service.ts This service was built to detect all clicks on the app screen. I use reactive programming in the entire app so rxjs was employed here. Feel free to use simpler methods if you want. The critical part here is detecting if the clicked element is part of the hierarchy of an expanded component or not. When I write a mess like this I usually fully document why this was built this way in order to protect it from "eager" developers doing cleanups.

import { AncestorNodeTrace, DebugOwner, SelfCloseEvent } from '../interfaces/self-close';
import { GestureResponderEvent } from 'react-native';
import { Subject } from 'rxjs';

/**
 * <!> Problem:
 * Consider the following scenario:
 * We have a dropdown opened and we want to open the second one. What should happen?
 * The first dropdown should close when detecting click outside.
 * Detecting clicks outside is not a trivial task in React Native.
 * The react events system does not allow adding event listeners.
 * Even worse adding event listener is not available in react native.
 * Further more, TouchableOpacity swallows events.
 * This means that a child TouchableOpacity inside a parent TouchableOpacity will consume the event.
 * Event bubbling will be stopped at the responder.
 * This means simply adding a backdrop as TouchableOpacity for the entire app won't work.
 * Any other TouchableOpacity nested inside will swallow the event.
 *
 * <!> Further reading:
 * https://levelup.gitconnected.com/how-exactly-does-react-handles-events-71e8b5e359f2
 * https://stackoverflow.com/questions/40572499/touchableopacity-swallow-touch-event-and-never-pass
 *
 * <!> Solution:
 * Touch events can be captured in the main view on mobile.
 * Clicks can be captured in the main view on web.
 * We combine these two data streams in one single pipeline.
 * All self closeable components subscribe to this data stream.
 * When a click is detected each component checks if it was triggered by it's own children.
 * If not, it self closes.
 *
 * A simpler solution (with significant drawbacks) would be:
 * https://www.jaygould.co.uk/2019-05-09-detecting-tap-outside-element-react-native/
 */

/** Combines both screen touches on mobile and clicks on web. */
export const selfCloseEvents$ = new Subject<SelfCloseEvent>();

export const screenTouched = (e: GestureResponderEvent) => {
    selfCloseEvents$.next(e);
};

export const screenClicked = (e: React.MouseEvent) => {
    selfCloseEvents$.next(e);
};

/**
 * If the current host component ancestors set contains the clicked element,
 * the click is inside of the currently verified component.
 */
export const detectClickIsOutside = (event: SelfCloseEvent, host: React.Component): boolean => {
    let hostTrace = getNodeSummary((host as any)._reactInternalFiber);
    let ancestorsTrace = traceNodeAncestors(event);
    let ancestorsTraceIds = ancestorsTrace.map(trace => trace.id);

    let clickIsOutside: boolean = !ancestorsTraceIds.includes(hostTrace.id);
    return clickIsOutside;
};

// ====== PRIVATE ======

/**
 * Tracing the ancestors of a component is VITAL to understand
 * if the click originates from within the component.
 */
const traceNodeAncestors = (event: SelfCloseEvent): AncestorNodeTrace[] => {
    let ancestorNodes: AncestorNodeTrace[] = [];
    let targetNode: DebugOwner = (event as any)._targetInst; // <!WARNING> Private props

    // Failsafe
    if (!targetNode) { return; }

    traceAncestor(targetNode);

    function traceAncestor(node: DebugOwner) {
        node && ancestorNodes.push(getNodeSummary(node));
        let parent = node._debugOwner;
        parent && traceAncestor(parent);
    }

    return ancestorNodes;
};

const getNodeSummary = (node: DebugOwner): AncestorNodeTrace => {
    let trace: AncestorNodeTrace = {
        id: node._debugID,
        type: node.type && node.type.name,
        file: node._debugSource && node._debugSource.fileName,
    };

    return trace;
};

interfaces/self-close.ts - Some boring typescript interfaces to help with project maintenance.

import { NativeSyntheticEvent } from 'react-native';

/** Self Close events are all the taps or clicks anywhere in the UI. */
export type SelfCloseEvent = React.SyntheticEvent | NativeSyntheticEvent<any>;

/**
 * Interface representing some of the internal information used by React.
 * All these fields are private, and they should never be touched or read.
 * Unfortunately, there is no public way to trace parents of a component.
 * Most developers will advise against this pattern and for good reason.
 * Our current exception is an extremely rare exception.
 *
 * <!> WARNING
 * This is internal information used by React.
 * It might be possible that React changes implementation without warning.
 */
export interface DebugOwner {
    /** Debug ids are used to uniquely identify React components in the components tree */
    _debugID: number;
    type: {
        /** Component class name */
        name: string;
    };
    _debugSource: {
        /** Source code file from where the class originates */
        fileName: string;
    };
    _debugOwner: DebugOwner;
}

/**
 * Debug information used to trace the ancestors of a component.
 * This information is VITAL to detect click outside of component.
 * Without this script it would be impossible to self close menus.
 * Alternative "clean" solutions require polluting ALL components with additional custom triggers.
 * Luckily the same information is available in both React Web and React Native.
 */
export interface AncestorNodeTrace {
    id: number;
    type: string;
    file: string;
}

And now the interesting part. dots-menu.tsx - Trimmed down to the essentials for the example

import * as div from './dots-menu.style';
import { detectClickIsOutside, selfCloseEvents$ } from '../../services/self-close-signal.service';
import { Subject } from 'rxjs';
// ... other imports

export class DotsMenu extends React.Component<Props, State> {

    private destroyed$ = new Subject<void>();

    constructor(props: Props) {
        // ...
    }

    public render() {
        const { isExpanded } = this.state;

        return (
            <div.DotsMenu ...['more props here'] >

                {/* Trigger */}
                <DotsMenuItem expandMenu={() => this.toggleMenu()} ...['more props here'] />

                {/* Items */}
                {
                    isExpanded &&
                    // ... expanded option here
                }

            </div.DotsMenu>
        );
    }

    public componentDidMount() {
        this.subscribeToSelfClose();
    }

    public componentWillUnmount() {
        this.destroyed$.next();
    }

    private subscribeToSelfClose() {
        selfCloseEvents$.pipe(
            takeUntil(this.destroyed$),
            filter(() => this.state.isExpanded)
        )
            .subscribe(event => {
                let clickOutside = detectClickIsOutside(event, this);

                if (clickOutside) {
                    this.toggleMenu();
                }
            });
    }

    private toggleMenu() {
        // Toggle visibility and animation logic goes here
    }

}

Hope it works for you as well. P.S. I'm the owner, feel free to use these code samples. Hope you will enjoy this answer and check Visual School for future React Native tutorials.

like image 28
Adrian Moisa Avatar answered Oct 10 '22 03:10

Adrian Moisa


Put your View inside of TouchableWithoutFeedback, expand TouchableWithoutFeedback fullscreen and add onPress handler to it.

<TouchableWithoutFeedback 
  onPress={ /*handle tap outside of view*/ }
  style={ /* fullscreen styles */}
>
    <View>
     ...
    </View
</TouchableWithoutFeedback>
like image 45
Andrew Kovalenko Avatar answered Oct 10 '22 04:10

Andrew Kovalenko