> For the complete documentation index, see [llms.txt](https://lochiwei.gitbook.io/web/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://lochiwei.gitbook.io/web/component/shadow-dom/events/composed.md).

# 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，必須要設定：\
**{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 %}
