Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detecting 'outside' events using the same logic as with portals #10962

Closed
jamiewinder opened this issue Sep 29, 2017 · 4 comments
Closed

Detecting 'outside' events using the same logic as with portals #10962

jamiewinder opened this issue Sep 29, 2017 · 4 comments

Comments

@jamiewinder
Copy link

I was surprised (pleasantly so) to read that events bubble through portals and up the component tree rather than the DOM tree. Obviously this means I can detect clicks that occur within my component or child components without having to worry about whether they happen to be portal hosted.

However, another fairly common thing you may want to do is detect whether an event occurred outside of a component or child components. There are plenty of libraries out there that do this, but they all (AFAIK) use the DOM tree so don't have this same behaviour.

I was curious whether or not there is anything new, but unadvertised, in React 16 that can help with this part of the puzzle?

@jquense
Copy link
Contributor

jquense commented Sep 29, 2017

There isn't anything new specifically for this. events in portals sort of "just work" due to how they are implemented, not because there is anything new in the event code. The way it works in a nutshell is that when React sees an event at the top level (on document) it finds the component associated with that events target and then manually walks up and down the component tree triggering event handlers, mimicking the capture and bubble phases of real DOM events. Previously subtree rendering (old portals) were different component trees so react couldn't "see" events between the portal and parent, now Portals are properly part of the same react component tree as their parent, so when React walks down and up to fire events it can now cross portal boundaries

@jamiewinder
Copy link
Author

jamiewinder commented Sep 29, 2017

Thanks for the info.

I have some code that tries to do this using the existing event system. I'll leave it here in case anyone happens to find it useful, but it is a bit messy:

import * as React from 'react';

export interface IDetectExternalEventsProps {
    component?: string;
    reactEvents: Array<string>;
    domEvents: Array<string>;
    onExternalEvent: (event: Event) => void;
}

export class DetectExternalEvents extends React.Component<IDetectExternalEventsProps> {
    // Fields
    private _disposeTopLevelEvents: (() => void) | null = null;
    private _lastInterceptedEvent: Event | null = null;

    // Methods (React.Component)
    public render() {
        const {
            children,
            component = 'div',
            reactEvents
        } = this.props;
        const events = reactEvents.reduce((obj, eventName) => {
            obj[eventName] = this._handleWrapperEvent;
            return obj;
        }, {} as { [key: string]: any });
        return React.createElement(component, events, children);
    }

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

    public componentDidUpdate() {
        this._setupTopLevelEvents();
    }

    public componentWillUnmount() {
        if (this._disposeTopLevelEvents) {
            this._disposeTopLevelEvents();
            this._disposeTopLevelEvents = null;
        }
    }

    // Methods
    private _setupTopLevelEvents() {
        if (this._disposeTopLevelEvents) {
            this._disposeTopLevelEvents();
            this._disposeTopLevelEvents = null;
        }
        const { domEvents } = this.props;
        domEvents.forEach((eventName) => {
            window.addEventListener(eventName, this._handleTopLevelEvent);
        });
        this._disposeTopLevelEvents = () => {
            domEvents.forEach((eventName) => {
                window.removeEventListener(eventName, this._handleTopLevelEvent);
            });
        };
    }

    // Event Handlers
    private _handleWrapperEvent = (event: React.SyntheticEvent<Element>) => {
        this._lastInterceptedEvent = event.nativeEvent;
    }

    private _handleTopLevelEvent = (event: Event) => {
        const { onExternalEvent } = this.props;
        const wasEventIntercepted = this._lastInterceptedEvent === event;
        if (!wasEventIntercepted) {
            onExternalEvent(event);
        }
        this._lastInterceptedEvent = null;
    }
}

Usage:

<DetectExternalEvents domEvents={['click']} reactEvents={['onClick']} 
    onExternalEvent={() => console.log('Clicked outside')}>
    <div>Any content, maybe including portals</div>
</DetectExternalEvents>

Basically, if we detect the event before it reaches window then we know it was internal. If not, it was external.

Unfortunately it:

  • needs a wrapper component (to attach React events to)
  • needs you to duplicate the event names for the DOM and React formats

@gaearon
Copy link
Collaborator

gaearon commented Oct 3, 2017

There’s nothing new on that front, but you can follow #285 in case we add something.

@amanmahajan7
Copy link

I ended up using the following solution if it helps anyone. Here is an explanation of the approach

export default class ClickOutside extends React.Component {
  static propTypes = {
    children: PropTypes.element.isRequired,
    onClickOutside: PropTypes.func.isRequired
  };

  isClickedInside = false;

  componentDidMount() {
    document.addEventListener('click', this.handleDocumentClick);
  }

  componentWillUnmount() {
    document.removeEventListener('click', this.handleDocumentClick);
  }

  handleDocumentClick = () => {
    if (this.isClickedInside) {
      this.isClickedInside = false;
      return;
    }

    this.props.onClickOutside();
  };

  handleClick = () => {
    this.isClickedInside = true;
  };

  render() {
    return React.cloneElement(
      React.Children.only(this.props.children), {
        onClickCapture: this.handleClick
      }
    );
  }
}

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

No branches or pull requests

4 participants