Letharion
Letharion

Reputation: 4229

Detect if click was inside react component or not in typescript

I have roughly the following:

componentDidMount() {                                                                                                                                                                                           
  document.querySelector('body')!.addEventListener('click', this.click);
} 

click = (e: Event) => {                                                                                                                                                                                         
  if (this.state.toggled) {
    if (!ReactDom.findDOMNode(this.someRef).contains(e.target)) {
      this.setState({ toggled: false });
    }
  }
};

render() {
  return (<CustomElement
    ref={(e) => { this.someRef = e; }}                                                                                                                                                                   
  />)
}

This code correctly detects whether the user clicks inside or outside the CustomElement, so far so good.

However, tsc isn't happy with this at all:

error TS2345: Argument of type 'EventTarget' is not assignable to parameter of type 'Node'.
  Property 'attributes' is missing in type 'EventTarget'.

Looking at node_modules/typescript/lib/lib.d.ts this makes sense, since e.target is an EventTarget which appears to only define the functions for adding and removing event handlers. However MDN says e.target is "A reference to the object that dispatched the event." which sounds closer to what I want.

So how do I retain the currently working functionality, while also making tsc happy (as opposed to just silencing the error)?

Upvotes: 44

Views: 21472

Answers (3)

morteza
morteza

Reputation: 738

this is what works for me the point is you should also provide a type for your ref :

 const wrapperRef = useRef<HTMLDivElement>(null);
  const handleClickOutside = (event: React.MouseEvent) => {
    if (
      wrapperRef?.current?.contains(event.target as Node) &&
      isFocusLockDisabled
    ) {
      return foo(false);
    }
  };

Upvotes: 8

Letharion
Letharion

Reputation: 4229

I found a similar discussion in the typescript issue queue which says in this case a type assertion is probably unavoidable:

Basically EventTarget is the most general type, of which Element is a subtype, and HTMLElement is a subtype of that. If you get back a thing from the DOM, we generally have no idea which it is, and you should add a type assertion to "add in" the specific external knowledge that you have about the structure of your particular DOM layout.

It's possible, for example, that the .target of an event is not an Element (you can add event listeners to XMLHttpRequest, for example, and XMLHttpRequest does not have a getBoundingClientRect method).

So because target may or may not be a Node like I want, TS can't infer the truth, and I need to assert.

contains(e.target as Node) appears to be the right solution.

Upvotes: 85

Amid
Amid

Reputation: 22352

How about defining your click handler like this:

private click = (e: Event) =>
{
    if (e.target instanceof HTMLElement && !ReactDOM.findDOMNode(this.someRef).contains(e.target))
    {
        if (this.state.toggled)
        {
            this.setState({ toggled: false });
        }
    }
}

Sample pen.

Upvotes: 15

Related Questions