4

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?

2 Answers 2

6

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.

9
  • Thank you! I had to do a little tweaking but did get it to work. For some reason, window event listener wasn't working (it would work in the playground but not in my code/browser). Replacing your window event listener with document.eventListener works perfectly. :)
    – babyYoda
    Aug 5, 2020 at 16:31
  • @babyYoda Interesting. Still, that's good to know. I'll edit that in anyways, it's worth having in the main answer.
    – sfdcfox
    Aug 5, 2020 at 16:48
  • This is super clever! I was using event.path to snag the element, but that was a solution only for chrome... Cheers! Aug 6, 2020 at 18:56
  • @sfdcfox -- so this works perfectly when clicking outside the element on anything that is not interactive. However, I just noticed that if the popup is open, and I click on another button on the page it is not closing then. Any ideas on why that would be?
    – babyYoda
    Aug 27, 2020 at 4:32
  • @babyYoda Would need to test. My initial theories: (a) there's a bug in LWC, or (b) the interactive elements are cancelling bubbling for some reason. I'll see if I can replicate and diagnose this. Do you have an example I can start from?
    – sfdcfox
    Aug 27, 2020 at 5:25
1

@sfdcfox here is my tooltip component's code (it was too long to post as a comment reply):

tooltip.html

<template>
<div onclick={insideClick} class={tooltipStyle}>
    <button class="tooltip__button" aria-expanded={showTooltip} aria-haspopup="true"
        aria-label={accessibilityLabel} onclick={toggleTooltip}>
        <span tabindex="-1">
            <slot name="tooltip-trigger">
                <!-- Custom trigger icon and/or text will override this icon -->
                <lightning-icon icon-name="utility:info_alt" size="xx-small"></lightning-icon>
            </slot>
        </span>
    </button>
    <div if:true={checkVisibility} class="tooltip__popup">
        <slot name="tooltip-content">
            <!-- Custom content -->
        </slot>
        <button class="tooltip__popup-close" aria-label="close tooltip" onclick={toggleTooltip}>
            <lightning-icon icon-name="utility:close" size={iconSize}></lightning-icon>
        </button>
        <p class="tooltip__content" if:true={hasText}>{tooltipText}</p>
    </div>
</div>

tooltip.js

import { LightningElement, api } from 'lwc';

export default class HelperTooltip extends LightningElement {
    @api accessibilityLabel = 'Toggle opening tooltip';
    @api size = 'small'; // 'x-small', 'small' or 'large', referring to padding and 'x' size
    @api type; // 'arrow' or default of no arrow.
    @api vPos = 'bottom'; // 'top' or 'bottom'
    @api hPos = 'center'; // 'center', 'left', or 'right'
    @api tooltipText = '';
    hasText = false;
    iconSize = 'xx-small';
    showTooltip = false;
    style = 'tooltip';
    outsideClick;

    get tooltipStyle() {
        let style = this.style + ' ' + this.size + ' has-' + this.type + ' v-' + this.vPos + ' h-' + this.hPos;
        return style;
    }

    get checkVisibility() {
        return this.showTooltip;
    }

    connectedCallback() {
        if (this.type === 'large') {
            this.iconSize = 'x-small';
        }
        if (this.tooltipText !== '') {
            this.hasText = true;
        }
        // Close tooltip when user clicks outside.
        document.addEventListener('click', this.outsideClick = this.closeTooltip.bind(this));
    }

    disconnectedCallback() {
        document.removeEventListener('click', this.outsideClick);
    }

    insideClick(event) {
        // This event is necessary to not trigger close with an inside click
        event.stopPropagation();
        return false;
    }

    closeTooltip() {
        if (this.showTooltip) {
            this.showTooltip = false;
        }
    }

    toggleTooltip(event) {
        let popup = this.template.querySelector('.tooltip__popup');
        let trigger = this.template.querySelector('.tooltip__button');
        // set tooltip to close or open
        if (this.showTooltip) {
            // Close tooltip
            this.showTooltip = false;
            // Reset focus to trigger button
            if (event.detail === 0) {
                trigger.focus();
            }
        } else {
            // Open tooltip
            this.showTooltip = true;

            // Focus on first link or button in modal if opened by keyboard
            if (event.detail === 0) {
                if (popup.querySelector('a') !== null) {
                    popup.querySelector('a:first-of-type').focus();
                } else if (popup.querySelector('button') !== null) {
                    popup.querySelector('button:first-of-type').focus();
                }
            }
        }
    }
}

tooltip.css

.tooltip {
    display: inline-block;
    position: relative;
}

.tooltip__button {
    border: none;
    background: none;
    color: #1d9cd3;
}

.tooltip__button svg { /* (i) icon */
    fill: #1d9cd3;
}

.tooltip__popup {
    padding: 25px 20px;
    box-shadow: 0 1px 5px 0 #dadada;
    border: solid 1px #dadada;
    background-color: #ffffff;
    display: inline-block;
    position: absolute;
    left: 0;
    top: 100%;
    left: -125px;
    width: 250px;
    z-index: 100;
}

.tooltip__popup-close {
    position: absolute;
    background: none;
    border: none;
    right: 5px;
    top: 5px;
}

.tooltip__button {
    color: #1b74a4;
}
/* x-small tooltip */
.x-small .tooltip__popup {
    padding: 16px;
    width: 212px;
}
.large .tooltip__popup-close lightning-icon {
    opacity: .4;
    transform: scale(1.25);
}
.v-center .tooltip__popup {
    top: 50%;
    transform: translateY(calc(-50% + 5px)); /* 5px is to visually center, not sure why 50% isn't working */
}
.h-right .tooltip__popup {
    left: 100%;
}
.h-right.has-arrow .tooltip__popup {
    left: calc(100% + 5px);
}

app.html

<template>
    <div class="app slds-p-around_x-large">
        <c-tooltip
            class="tooltip"
            accessibility-label="toggle showing disconnected help information"
            size="x-small"
            type="arrow"
            v-pos="center"
            h-pos="right">
            <span slot="tooltip-trigger" class="tooltip-trigger">
                Tooltip example
            </span>
            <div slot="tooltip-content">
                some text here.
            </div>
        </c-tooltip>
        <p>
            Leverage agile frameworks to provide a robust synopsis for high level overviews. Iterative approaches to corporate strategy foster collaborative thinking to further the overall value proposition. Organically grow the holistic world view of disruptive innovation via workplace diversity and empowerment.
        </p>
        <c-tooltip
            class="tooltip"
            accessibility-label="toggle showing disconnected help information"
            size="x-small"
            type="arrow"
            v-pos="center"
            h-pos="right">
            <span slot="tooltip-trigger" class="tooltip-trigger">
                Tooltip example
            </span>
            <div slot="tooltip-content">
                some text here.
            </div>
        </c-tooltip>
    </div>
</template>

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .