# Events that Cross Shadow DOM Boundary

[Web Components](/web/component.md) ⟩ [Shadow DOM](/web/component/shadow-dom.md) ⟩ [Events](/web/component/shadow-dom/events.md) ⟩

{% tabs %}
{% tab title="📗 參考" %}

* JS.info ⟩ [Custom events](https://javascript.info/shadow-dom-events#custom-events) in shadow DOM
* Google ⟩ [The Shadow DOM event model](https://developers.google.com/web/fundamentals/web-components/shadowdom#events)
  {% endtab %}

{% tab title="👉 相關" %}

* [string.escapeHTML()](/web/js/val/prim/str/method/escapehtml.md)
* [Custom Events](/web/browser/event/custom.md)
  {% endtab %}

{% tab title="📘 手冊" %}

* [event.composed](https://developer.mozilla.org/en-US/docs/Web/API/Event/composed)
  {% endtab %}
  {% endtabs %}

The events that **do** cross the **shadow boundary** are:

| category    | events                                                 |
| ----------- | ------------------------------------------------------ |
| focus       | blur, focus, focusin, focusout                         |
| mouse       | click, dblclick, mousedown, mouseenter, mousemove, ... |
| wheel       | wheel                                                  |
| input       | beforeinput, input                                     |
| keyboard    | keydown, keyup                                         |
| composition | compositionstart, compositionupdate, compositionend    |
| drag        | dragstart, drag, dragend, drop, ...                    |

## Built-in Events

{% hint style="info" %}
Most (**built-in**) **events** successfully **bubble through** a **shadow DOM boundary** (with  [event.composed](https://javascript.info/shadow-dom-events#event-composed) == **true**).&#x20;

There are few events that do not:

* `mouseenter`, `mouseleave` (they do not bubble at all),
* `load`, `unload`, `abort`, `error`
* `select`
* `slotchange`

These events can be caught only on elements **within the same DOM**, where the event target resides.
{% endhint %}

### Custom Events

{% hint style="info" %}
如果要讓 **CustomEvent** 跨過 shadow DOM boundary，必須要設定：\
\&#xNAN;**{bubbles: true, composed: true} ⭐️**
{% endhint %}

If **composed: false** (default), consumers **won't** be able to **listen** for the event **outside** of your shadow root.

{% tabs %}
{% tab title="1️⃣" %}
{% embed url="<https://codepen.io/lochiwei/pen/oNwyONy>" %}

{% endtab %}

{% tab title="js" %}

```javascript
// attach shadow root (⭐️ so outer's light DOM is not shown)
outer.attachShadow({mode: 'open'});

let inner = document.createElement('div');
outer.shadowRoot.append(inner);

/*
div#outer
  #shadow-dom
    div#inner
*/

// ⭐️ listen for 'test' event
document.addEventListener('test', e => log(lightDOM, e.detail));
outer.shadowRoot.addEventListener('test', e => log(shadowDOM, e.detail));

// ⭐️ composed custom event
function sendComposedEvent(){
  inner.dispatchEvent(new CustomEvent('test', {
    bubbles: true,
    composed: true,       // ⭐️ composed
    detail: "'composed' event from inner <div>"
  }));
}

// ⭐️ not composed custom event
function sendNonComposedEvent(){
  inner.dispatchEvent(new CustomEvent('test', {
    bubbles: true,
    composed: false,       // ⭐️ not composed
    detail: "not composed"
  }));
}


```

{% endtab %}

{% tab title="helpers" %}

```javascript
// ⭐️ escape HTML special characters
String.prototype.escapeHTML = function () {
    return this 
        .replace(/&/g, '&amp;' )    // ampersand (must be first❗️)
        .replace(/>/g, '&gt;'  )    // greater than
        .replace(/</g, '&lt;'  )    // less than
        .replace(/"/g, '&quot;')    // double-quote
        .replace(/'/g, '&#39;' )    // single-quote
        .replace(/`/g, '&#96;' );   // backtick
}

// log
function log(dom, msg) {
  dom.innerHTML += msg.escapeHTML() + '<br>';
}
```

{% endtab %}

{% tab title="html" %}

```markup
<!-- ⭐️ element with shadow DOM -->
<div id="outer">outer div</div>

<div id="buttons">
  <button onclick="sendComposedEvent()">composed event</button>
  <button onclick="sendNonComposedEvent()">non-composed event</button>
</div>

<!-- console log -->
<div id="lightDOM" class="output">
  <p>Light DOM event logs:</p>
</div>

<div id="shadowDOM" class="output">
  <p>Shadow DOM event logs:</p>
</div>
```

{% endtab %}

{% tab title="css" %}

```css
.output {
  border: 1px solid black;
  padding: 8px;
  background: black;
  color: white;
}

.output p {
  margin: -8px;
  margin-bottom: 8px;
  padding: 8px;
  background: tomato;
}

#lightDOM p {
  background: hsl(225, 80%, 60%);
}

#buttons {
  margin-bottom: 8px;
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
In case of **nested components**, one shadow DOM may be nested into another. In that case **composed events** bubble through **all shadow DOM boundaries**. So, if an **event** is intended only for the **immediate enclosing component**, we can:

* **dispatch** it on the **shadow host**&#x20;
* set **composed: false**.&#x20;

Then it’s out of the component shadow DOM, but won’t bubble up to higher-level DOM.
{% endhint %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://lochiwei.gitbook.io/web/component/shadow-dom/events/composed.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
