在 Lit-Elements 上实现外部点击

Implementing outside clicks on Lit-Elements

我正在使用 LitElements 制作自定义下拉 Web 组件,在实现点击外部下拉关闭功能时,我注意到一些意外行为阻碍了我。在下面的 async firstUpdated() 函数中,我将事件侦听器创建为 recommended in their documentation。注销 dropdown 按预期工作,但每次注销 target returns 根 <custom-drop-down>...</custom-drop-down> 元素,虽然在一定程度上准确,但不够具体。如果有更好的方法来处理外部点击事件,我会洗耳恭听。

作为参考,这是我正在尝试复制的已经构建的内容(尽管源代码是隐藏的):https://www.carbondesignsystem.com/components/dropdown/code/

async firstUpdated() {
  await new Promise((r) => setTimeout(r, 0));
  this.addEventListener('click', event => {
    const dropdown = <HTMLElement>this.shadowRoot?.getElementById('dropdown-select');
    const target = <HTMLElement>event.target;
    if (!dropdown?.contains(target)) {
      console.log('im outside');
    }
  });
}    


@customElement('custom-drop-down')
public render(): TemplateResult {
  return html`
    <div>
      <div id="dropdown-select" @action=${this.updateValue}>
        <div class="current-selection" @click=${this.toggleDropdown}>
          ${this.value}
        </div>
        <div class="dropdown-options" id="dropdown-options">
          ${this.dropdownItems(this.options)}
        </div>
      </div>
    </div>
  `;
}

click event's target property is set to the topmost event target.

The topmost event target MUST be the element highest in the rendering order which is capable of being an event target.

因为影子DOM封装了它的内部结构,所以事件是retargeted to the host element。在您的示例中,click 事件的 target 被重定向到 custom-drop-down,因为这是能够成为目标的第一个也是最高的元素。

如果您需要将自定义元素内的某个元素作为目标,您可以采用以下方法之一:

  1. 在您的元素中注册一个事件侦听器。
  2. 使用 <slot> 元素用在 DOM.
  3. 中声明的元素填充您的自定义元素
  4. 使用 composedPath on events with composed: true 确定您的事件从哪个内部元素冒泡。

这是我相信您正在寻找的功能示例。如果您单击 Custom Element,它会切换其活动状态(类似于切换下拉菜单)。如果您单击页面上的任何其他元素,自定义元素将自行停用(类似于在失去焦点时隐藏下拉菜单)。

// Define the custom element type
class MySpan extends HTMLSpanElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    
    const text = 'Custom Element.';
    const style = document.createElement('style');
    style.textContent = `
      span {
        padding: 5px;
        margin: 5px;
        background-color: gray;
        color: white;
      }
      
      .active {
        background-color: lightgreen;
        color: black;
      }      
    `;
    this.span = document.createElement('span');
    this.span.innerText = text;
    this.span.addEventListener('click', (event) => { 
      console.log('CUSTOM ELEMENT: Clicked inside custom element.'); 
      this.handleClick(event);
    });
    
    this.shadowRoot.append(style, this.span);
  }
  
  handleClick(event) {
    // If the event is from the internal event listener,
    // the target will be our span element and we can toggle it.
    // If the event is from outside the element, 
    // the target cannot be our span element and we should
    // deactiviate it.
    if (event.target === this.span) {
      this.span.classList.toggle('active');
    }
    else {
      this.span.classList.remove('active');
    }
  }
}
customElements.define('my-span', MySpan, { extends: 'span' });

// Insert an instance of the custom element
const div1 = document.querySelector('#target');
const mySpan = new MySpan();
div1.appendChild(mySpan);

// Add an event listener to the page
function handleClick(event) {
  // If we didn't click on the custom element, let it
  // know about the event so it can deactivate its span.
  if (event.target !== mySpan) {
    console.log(`PAGE: Clicked on ${ event.target }. Notifying custom element.`);
    mySpan.handleClick(event);
  }
}
document.body.addEventListener('click', (event) => handleClick(event));
p {
  padding: 5px;
  background-color: lightgray;
}
<p id="target">This is a paragraph.</p>
<p>This is a second paragraph.</p>
<p>This is a third paragraph.</p>
<p>This is a fourth paragraph.</p>