LWC Shadow DOM – Check if User Click is Outside of Component

I have a simple tooltip (not modal) that after it is opened if the user clicks outside of the component it should close.

In aura, we were able to do this pretty simply by adding a function in the doInit that would add an event listener on document click, then check if that click was in the component. However, in LWC I'm at a loss at how to make this work because when you add an event listener in the JS (not the template) it returns event.target as the topmost parent component element (in our case the great grandparent element), not the actual element that triggered the event. Is there any way in the JS to get an accurate event.target value?

I've seen the "hack" where I could add an element in my tooltip's HTML that would be placed behind my component and adding an onclick function to that element that would close on it. However, that is not a solution that will work for me, because a tooltip does not take the entire screen so it would be unexpected for a user to not be able to click anything else until the tooltip is closed.

I know that running this.addEventListener('click') instead of document.addEventListener('click') runs anytime a user clicks inside the component, is there any way to leverage that to know when something is a click outside?

Best Answer

You can do this with window listeners, apparently. Here's a quick playground I wrote up (but we can't save, so you'll have to copy-paste):

tooltip.js

import { LightningElement } from 'lwc';

export default class Tooltip extends LightningElement {
    _handler;
    connectedCallback() {
        document.addEventListener('click', this._handler = this.close.bind(this));
    }
    disconnectedCallback() {
        document.removeEventListener('click', this._handler);
    }
    ignore(event) {
        event.stopPropagation();
        return false;
    }
    close() { 
        console.log('we should close now');
    }
}

tooltip.html

<template>
    <div onclick={ignore}>
        Click Me
    </div>
</template>

We use an onclick handler inside the component to call event.stopPropagation(), which keeps our top-level handler from executing and closing the component (this just logs instead of closing, but you get the idea). A click anywhere else results in closing the component. Other arrangements are also possible, but this probably what I'd do in normal circumstances.