W3docs

Shadow DOM e Eventos

Aprenda como os eventos se comportam dentro do Shadow DOM: propagação através da fronteira de sombra, redirecionamento de eventos, event.composedPath(), event.composed e despacho de eventos personalizados.

Um web component construído com Shadow DOM mantém sua estrutura interna oculta por trás de uma fronteira de sombra. Essa encapsulação muda como os eventos fluem: alguns eventos cruzam a fronteira e outros não, e os que cruzam são redirecionados para que o mundo externo nunca veja seus internos privados. Este capítulo explica essas regras para que seus componentes disparem eventos que a página host possa realmente usar.

Você vai aprender quatro coisas: como os eventos se propagam pela fronteira de sombra, redirecionamento de eventos, event.composedPath(), a flag event.composed e o despacho de eventos personalizados que escapam da árvore de sombra.

Este capítulo pressupõe que você já conhece o básico de Shadow DOM e a propagação e captura de eventos em geral. Se eventos personalizados são novos para você, leia primeiro Despachando Eventos Personalizados.

Propagação de Eventos no Shadow DOM

A propagação de eventos descreve como um evento se propaga pela árvore DOM: ele dispara no elemento alvo, depois em cada ancestral, até chegar ao document. (Para o quadro completo, veja Propagação e Captura.)

Dentro do Shadow DOM, a questão é: o evento continua se propagando quando alcança a raiz de sombra, saindo para o light DOM do host? Isso depende de o evento ser composed:

  • Eventos composed cruzam a fronteira de sombra e continuam se propagando no light DOM. A maioria dos eventos nativos voltados ao usuário é composed: click, mousedown, keydown, input, pointermove, entre outros.
  • Eventos não-composed param na raiz de sombra e nunca chegam ao host. Exemplos: focus (use focusin/focusout se precisar de eventos de foco compostos), scroll, mouseenter e load.

Para impedir que um evento se propague em qualquer ponto — seja composed ou não — chame event.stopPropagation().

Redirecionamento de eventos

Esta é a parte que surpreende as pessoas. Quando um evento composed cruza a fronteira, o navegador o redireciona: para os listeners no light DOM, event.target aponta para o elemento host, não para o elemento interno que foi realmente clicado.

Isso é intencional. A encapsulação não teria sentido se o código externo pudesse ler os nós privados do seu componente a partir de event.target. Então a página host vê "algo dentro de <my-widget> foi clicado," e não "o terceiro <button> na sua árvore de sombra foi clicado." Dentro da árvore de sombra, event.target ainda aponta para o elemento real.

Se você precisar do caminho real pela árvore de sombra, use event.composedPath() — abordado a seguir.

Utilizando event.composedPath()

Como o redirecionamento oculta o elemento interno de event.target, você precisa de outra maneira de inspecionar o caminho real de propagação. event.composedPath() retorna um array dos nós pelos quais o evento passou, incluindo nós dentro de quaisquer árvores de sombra que cruzou, ordenados do alvo mais interno para fora até window.

Esta é a maneira confiável de responder "qual elemento interno foi realmente clicado?" a partir de um listener no light DOM — mas apenas para componentes cuja raiz de sombra é mode: 'open'. Para uma raiz mode: 'closed', composedPath() para no host e os nós internos são omitidos, preservando a privacidade do componente fechado.

Veja como event.composedPath() pode ser usado para rastrear a propagação de eventos dentro do Shadow DOM:

<div id="outer"></div>
<script>
  const outer = document.getElementById('outer');
  const shadow = outer.attachShadow({ mode: 'open' });
  const inner = document.createElement('div');
  inner.textContent = 'Click me';

  inner.addEventListener('click', event => {
    const composedInfo = document.createElement('p');
    composedInfo.textContent = 'The event composedPath contains the following elements:';
    shadow.appendChild(composedInfo);
    const path = event.composedPath();
    path.forEach((e) => {
      const pathItem = document.createElement('p');
      pathItem.textContent = e.tagName;
      shadow.appendChild(pathItem);
    });
  });

  shadow.appendChild(inner);
</script>

Ao clicar no <div> interno, todos os nós do caminho composto são listados: começa com o DIV clicado, depois DIV (o host #outer), depois BODY, HTML e, por fim, entradas para document e window (que aparecem como undefined por não terem tagName). As primeiras entradas são exatamente o que event.target oculta dos listeners no light DOM.

Entendendo event.composed

A propriedade somente leitura event.composed é um boolean: true se o evento pode cruzar fronteiras de sombra, false se está confinado à sua árvore de sombra. Você não pode defini-la depois — para eventos nativos ela é fixada pela especificação, e para eventos personalizados você a define ao construir o evento por meio da opção composed.

Essa flag importa mais quando você constrói um componente e precisa decidir se seus eventos personalizados devem escapar. Eventos de interação nativos como click são composed por padrão; seus próprios CustomEvents não são composed a menos que você opte por isso.

Veja como event.composed pode ser utilizado na prática:

<div id="outer"></div>

<script>
  const outer = document.getElementById('outer');
  const shadow = outer.attachShadow({ mode: 'open' });
  const button = document.createElement('button');
  button.textContent = 'Click me';

  button.addEventListener('click', event => {
    const composedInfo = document.createElement('p');
    composedInfo.textContent = `Composed: ${event.composed}`;
    shadow.appendChild(composedInfo);
  });

  shadow.appendChild(button);
</script>

Neste exemplo, clicar no botão dentro do shadow DOM dispara um evento de clique. Criamos dinamicamente um elemento <p> para exibir a propriedade event.composed dentro do shadow DOM.

Eventos Personalizados no Shadow DOM

Eventos personalizados permitem que um componente anuncie coisas ao mundo externo — "valor alterado," "item selecionado," "diálogo fechado" — sem expor seus internos. Esta é a maneira padrão de um web component se comunicar com a página que o usa. (Consulte Despachando Eventos Personalizados para ver a API em detalhes.)

Para que um evento personalizado chegue a um listener no elemento host no light DOM, você precisa de duas opções definidas:

  • composed: true — permite que o evento cruze a fronteira de sombra.
  • bubbles: true — permite que ele suba pela árvore para alcançar listeners ancestrais.

Definir apenas bubbles faz o evento se propagar dentro da árvore de sombra, mas ele para na raiz de sombra. Definir apenas composed permite que ele cruze a fronteira, mas não subirá até os ancestrais. Você quase sempre vai querer os dois.

Veja como criar e despachar um evento personalizado dentro de um shadow DOM:

<div id="container"></div>

<script>
  const container = document.getElementById('container');
  const shadow = container.attachShadow({ mode: 'open' });
  const button = document.createElement('button');
  button.textContent = 'Click me';

  button.addEventListener('click', () => {
    const event = new CustomEvent('customEvent', { bubbles: true, composed: true });
    button.dispatchEvent(event);
  });

  shadow.appendChild(button);

  container.addEventListener('customEvent', () => {
    const composedInfo = document.createElement('p');
    composedInfo.textContent = `Custom Event Triggered!`;
    container.appendChild(composedInfo);
  });
</script>

Ao clicar no botão, customEvent é despachado com bubbles: true e composed: true, então ele cruza a fronteira de sombra e se propaga até o listener no host (container) no light DOM. Para passar dados junto com o evento, use a propriedade detail:

button.dispatchEvent(new CustomEvent('customEvent', {
  bubbles: true,
  composed: true,
  detail: { value: 42 }
}));

container.addEventListener('customEvent', (event) => {
  console.log(event.detail.value); // 42
});

Note que mesmo que o evento alcance o host, o redirecionamento ainda se aplica: no listener do container, event.target é o elemento host, não o button interno. Use event.composedPath()[0] se precisar do alvo original.

Referência rápida

Propriedade / métodoO que indica
event.composedtrue se o evento pode cruzar fronteiras de sombra (somente leitura).
event.composedPath()Array de nós pelos quais o evento passa, incluindo árvores de sombra abertas, do mais interno para fora.
event.target (do light DOM)O elemento host, devido ao redirecionamento — nunca o nó interno privado.
opção bubblesPermite que um evento personalizado suba pela árvore.
opção composedPermite que um evento personalizado saia da árvore de sombra.

Armadilhas comuns

  • Esquecer composed: true em eventos personalizados. Um evento personalizado apenas com bubbles silenciosamente morre na raiz de sombra e nunca alcança a página host — um bug frequente de "meu listener não está disparando".
  • Ler event.target de fora. Ele é redirecionado para o host. Use event.composedPath() quando precisar do alvo interno real.
  • focus não é composed. Use focusin/focusout se precisar que mudanças de foco alcancem o host.
  • Raízes de sombra closed. composedPath() não revelará nós dentro de uma raiz mode: 'closed', então não confie nela para inspecionar componentes fechados.

Capítulos relacionados

Conclusão

Os eventos no Shadow DOM seguem algumas regras claras: eventos composed cruzam a fronteira, os não-composed não cruzam, e os eventos composed são redirecionados para o host para que seus internos permaneçam privados. Use event.composed para verificar a capacidade de cruzamento, event.composedPath() para recuperar o caminho real, e CustomEvent com bubbles: true e composed: true para permitir que seus componentes se comuniquem com a página que os hospeda.

Prática

Prática
Qual método fornece uma maneira de recuperar a sequência de elementos DOM que um evento percorre durante sua propagação?
Qual método fornece uma maneira de recuperar a sequência de elementos DOM que um evento percorre durante sua propagação?
Prática
Quais opções um CustomEvent deve ter para que um listener no elemento host no light DOM possa capturá-lo?
Quais opções um CustomEvent deve ter para que um listener no elemento host no light DOM possa capturá-lo?
Prática
De um listener no light DOM, para onde event.target aponta quando um clique acontece dentro de uma árvore de sombra aberta?
De um listener no light DOM, para onde event.target aponta quando um clique acontece dentro de uma árvore de sombra aberta?
Was this page helpful?