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(usefocusin/focusoutse precisar de eventos de foco compostos),scroll,mouseentereload.
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étodo | O que indica |
|---|---|
event.composed | true 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 bubbles | Permite que um evento personalizado suba pela árvore. |
opção composed | Permite que um evento personalizado saia da árvore de sombra. |
Armadilhas comuns
- Esquecer
composed: trueem eventos personalizados. Um evento personalizado apenas combubblessilenciosamente 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.targetde fora. Ele é redirecionado para o host. Useevent.composedPath()quando precisar do alvo interno real. focusnão é composed. Usefocusin/focusoutse precisar que mudanças de foco alcancem o host.- Raízes de sombra
closed.composedPath()não revelará nós dentro de uma raizmode: 'closed', então não confie nela para inspecionar componentes fechados.
Capítulos relacionados
- JavaScript Shadow DOM — o que é a árvore de sombra e como anexar uma.
- Shadow DOM Slots e Composição — projetando o light DOM na árvore de sombra.
- Estilização do Shadow DOM — estilos com escopo dentro de um componente.
- Elementos Personalizados — definindo seus próprios elementos HTML.
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.