elementweb-componentshadow-domstenciljstargeting

How to target an element inside a shadow DOM from another web component wrapped inside it


Well, I'm not sure I've been clear by the question title... sorry.

I would like to show a wc-tooltip I'm already using without problems on the DOM:

<div>
  You can use <span class="disk-space" id="help">20GB</span> of space.
  <wc-tooltip target="help">It works only if you are a vampire.</wc-tooltip>
</div>

I'm trying to using it inside a web component in the shadow DOM and it won't work:

<Host>
  <span><slot/></span>
  { this.tip && 
    <wc-icon id="help" name="my/fantastic/help/icon" />
    <wc-tooltip target="help">{ this.tip }</wc-tooltip>
  }
</Host>

This because I'm looking for the element in the DOM, not the shadow DOM:

componentDidRender (): void {
  const caller = document.getElementById(this.target)
  if (caller) {
    this.caller = caller
    this.caller.addEventListener('mouseleave', this.handleVisibility.bind(this, false))
    this.caller.addEventListener('mouseenter', this.handleVisibility.bind(this, true))
    return
  }
  console.error('Warning: property target is undefined.')
}

Obiously, document.getElementById(this.target) can't work.

The problem is that I didn't figured out how to solve this problem, I can use alternative attribute to point shadow DOM elements but how?


Solution

  • IMO designing components that expect their shadow DOM contents to be accessed from outside the component - except via public API such as @Methods - is improper design. In this case I suggest wc-tooltip should contain wc-icon in a slot rather than be a sibling (or create a separate component to contain both), and also hold the tooltip display logic. The actual visible "tooltip" would just be part of the shadow DOM rather than the whole component which is really just a wrapper for the icon (or anything else) that listens for mouse activity to show/hide the visible tooltip. So something like:

    <Host>
      <span><slot/></span>
      { this.tip && 
        <wc-tooltip>
          <wc-icon slot="trigger" id="help" name="my/fantastic/help/icon" />
          <div slot="tooltip">{ this.tip }</div>
        </wc-tooltip>
      }
    </Host>
    

    wc-tooltip:

    @State() tooltipVisible: boolean = false;
    
    @Listen('mouseenter') showTootlip() {
      this.tooltipVisible = true;
    }
    
    @Listen('mouseleave') hideTooltip() {
      this.tooltipVisible = false;
    }
    
    render() {
      return <Host>
        <slot name="trigger" />
        <div style={{ 
          display: this.tooltipVisible ? 'block' : 'none', 
          position: 'absolute', 
          left: ... 
        }}>
          <slot name="tooltip" />
        </div>
      </Host>;
    }