# Atelier 03 — Shadow DOM & Encapsulation

> **Prérequis :** Atelier 02 — tu sais créer des Custom Elements et faire communiquer des composants  
> **Durée estimée :** 3 à 4 jours  
> **Ce que tu sauras faire :** Encapsuler styles et structure dans un Shadow DOM, maîtriser les slots, exposer une API de theming via CSS Custom Properties

---

## Introduction

Jusqu'ici, tes composants utilisent `this.innerHTML` pour rendre leur structure. Leurs styles sont définis dans `global.css` ou en inline. Ce modèle a un défaut : **rien n'est encapsulé**.

Un `<h3>` dans ton composant peut être affecté par un `.card h3 { color: red }` écrit ailleurs dans la page. Et inversement, les styles définis par ton composant peuvent fuiter vers l'extérieur.

Le **Shadow DOM** résout ce problème en créant un sous-arbre DOM isolé attaché à ton élément. Les styles internes n'affectent que ce sous-arbre. Les styles externes ne le pénètrent pas (sauf via des mécanismes explicites).

---

## 1. Light DOM vs Shadow DOM

**Light DOM** : le DOM "normal" que tu connais. Visible dans les DevTools, accessible par `document.querySelector()`.

**Shadow DOM** : un sous-arbre DOM isolé. Dans les DevTools, il apparaît sous `#shadow-root`. Les sélecteurs CSS et JavaScript du document principal **ne le traversent pas**.

```
document
  └── <task-card>          ← Light DOM
        └── #shadow-root   ← Racine du Shadow DOM
              ├── <style>
              └── <div class="card">
                    └── <slot>
```

---

## 2. Attacher un Shadow Root

```js
class TaskCard extends HTMLElement {
  constructor() {
    super();
    // Attache le Shadow Root en mode 'open'
    // mode: 'open'   → accessible via element.shadowRoot (JS externe peut le lire)
    // mode: 'closed' → element.shadowRoot retourne null (hermétique)
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.shadowRoot.innerHTML = `
      <style>
        .card {
          padding: 1rem;
          border: 1px solid #ddd;
          border-radius: 4px;
        }
        h3 { margin: 0; color: #333; }
      </style>
      <div class="card">
        <h3>Ma carte</h3>
      </div>
    `;
  }
}
```

**Changements importants par rapport aux ateliers précédents :**

| Avant (sans Shadow DOM) | Avec Shadow DOM |
|---|---|
| `this.innerHTML = ...` | `this.shadowRoot.innerHTML = ...` |
| `this.querySelector(...)` | `this.shadowRoot.querySelector(...)` |
| Styles dans `global.css` | `<style>` dans `shadowRoot.innerHTML` |

---

## 3. Styles et encapsulation

### Ce que le Shadow DOM encapsule

```css
/* global.css */
h3 { color: red; }           /* N'affecte PAS les <h3> dans un shadow root */
.card { padding: 0; }        /* N'affecte PAS le .card dans un shadow root */
```

```html
<!-- Dans le shadow root -->
<style>
  h3 { color: blue; }       /* N'affecte PAS les <h3> hors du shadow root */
</style>
```

### Le sélecteur `:host`

`:host` cible **l'élément hôte** — c'est-à-dire l'élément Custom Element lui-même (`<task-card>`), vu depuis l'intérieur du Shadow DOM.

```css
:host {
  display: block;        /* Par défaut, les Custom Elements sont inline */
  margin-bottom: 1rem;
}

:host([status="done"]) {
  opacity: 0.5;          /* Si l'hôte a l'attribut status="done" */
}

:host(:hover) {
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
```

---

## 4. Slots : composer avec le contenu externe

Les **slots** permettent au contenu fourni dans le **Light DOM** (par l'utilisateur du composant) d'apparaître à des emplacements définis dans le **Shadow DOM**.

Sans slots, le contenu enfant d'un Custom Element avec Shadow DOM serait invisible.

### Slot par défaut

```js
// Composant
this.shadowRoot.innerHTML = `
  <div class="card">
    <slot></slot>  <!-- Le contenu enfant s'affiche ici -->
  </div>
`;
```

```html
<!-- Utilisation -->
<task-card>
  <p>Ce texte apparaîtra dans le slot</p>
</task-card>
```

### Slots nommés

```js
this.shadowRoot.innerHTML = `
  <article class="card">
    <header>
      <slot name="title">Titre par défaut</slot>  <!-- Contenu de repli -->
    </header>
    <main>
      <slot></slot>  <!-- Slot par défaut pour tout le reste -->
    </main>
    <footer>
      <slot name="actions"></slot>
    </footer>
  </article>
`;
```

```html
<!-- Utilisation -->
<task-card>
  <h2 slot="title">Ma tâche importante</h2>
  <p>Description de la tâche...</p>
  <div slot="actions">
    <button>Marquer comme fait</button>
  </div>
</task-card>
```

**Important :** Le contenu slotté reste dans le Light DOM — il est visuellement projeté dans le Shadow DOM, mais `document.querySelector()` le trouve toujours. C'est une projection, pas un déplacement.

---

## 5. CSS Custom Properties : l'API de theming

Les styles externes ne pénètrent pas le Shadow DOM — **sauf les CSS Custom Properties (variables CSS)**. Elles traversent les shadow boundaries. C'est le mécanisme officiel pour exposer une API de personnalisation.

```js
// À l'intérieur du shadow root
this.shadowRoot.innerHTML = `
  <style>
    .card {
      background: var(--task-card-bg, #ffffff);       /* valeur de repli */
      color: var(--task-card-color, #333);
      border-color: var(--task-card-border, #ddd);
    }
  </style>
  <div class="card">...</div>
`;
```

```css
/* global.css — personnalisation depuis l'extérieur */
task-card {
  --task-card-bg: #e8f4fd;
  --task-card-border: #2196f3;
}

/* Variante thématique */
task-card[status="urgent"] {
  --task-card-bg: #fff3f3;
  --task-card-border: #f44336;
}
```

Cette approche est préférable à `mode: 'closed'` : elle encapsule tout en offrant des points d'extension explicites et documentables.

---

## 6. `adoptedStyleSheets` — la méthode moderne

`shadowRoot.innerHTML = '<style>...'` parse la CSS à chaque instanciation. Si tu crées 50 `<task-card>`, la CSS est parsée 50 fois.

L'alternative moderne : **`CSSStyleSheet` constructable** et `adoptedStyleSheets`.

```js
// Créer la feuille de styles une seule fois (module scope)
const sheet = new CSSStyleSheet();
sheet.replaceSync(`
  :host { display: block; }
  .card { padding: 1rem; border-radius: 4px; }
`);

class TaskCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    // Partager la feuille de styles — aucune duplication mémoire
    this.shadowRoot.adoptedStyleSheets = [sheet];
  }

  connectedCallback() {
    this.shadowRoot.innerHTML = `<div class="card"><slot></slot></div>`;
    // Réappliquer après innerHTML (innerHTML réinitialise adoptedStyleSheets)
    this.shadowRoot.adoptedStyleSheets = [sheet];
  }
}
```

**Baseline :** disponible dans Chrome depuis longtemps, Baseline 2024. Tu peux l'utiliser sans restriction dans ton contexte.

**Référence :** [MDN — CSSStyleSheet constructable](https://developer.mozilla.org/fr/docs/Web/API/CSSStyleSheet/CSSStyleSheet)

---

## 7. Événements et Shadow DOM : le rôle de `composed`

Rappel de l'atelier 2 : `composed: true` permet à un événement de traverser les Shadow DOM boundaries.

```js
// À l'intérieur du Shadow Root
this.shadowRoot.querySelector('button').addEventListener('click', () => {
  // Sans composed: true, cet événement ne remontera pas hors du shadow root
  this.dispatchEvent(new CustomEvent('card-action', {
    detail: { ... },
    bubbles: true,
    composed: true,  // Nécessaire si émis depuis l'intérieur du shadow root
  }));
});
```

**Note :** Si tu dispatches depuis `this` (l'hôte, pas depuis un élément interne au shadow root), `composed` n'est pas nécessaire pour traverser la boundary. L'hôte est dans le Light DOM.

---

## 8. Debugging dans les DevTools Chrome

Chrome a un support excellent pour inspecter le Shadow DOM.

1. **Elements** → L'élément Custom Element affiche un `#shadow-root (open)` dépliant
2. **Styles** → Le panneau CSS montre les styles scopés (tu verras `:host { }`)
3. **Console** → `document.querySelector('task-card').shadowRoot` retourne le shadow root en mode `open`
4. **Computed** → Montre la valeur résolue des CSS Custom Properties

**Activer l'affichage du Shadow DOM :** Dans les DevTools, ⚙️ Settings → Preferences → "Show user agent shadow DOM" pour voir aussi les shadow roots des éléments natifs (`<input>`, `<video>`, etc.).

---

## Exercices

### Exercice 3.1 — Premier Shadow Root

**Objectif :** Migrer un composant de l'atelier précédent vers le Shadow DOM.

Reprends le `<task-card>` de l'atelier 1 et :
1. Attache un Shadow Root en mode `open`
2. Déplace le rendu vers `this.shadowRoot.innerHTML`
3. Ajoute des styles dans un `<style>` dans le shadow root (fond, bordure, padding)
4. Vérifie dans les DevTools que les styles globaux ne l'affectent pas

**Test de validation :**
```css
/* global.css — ajoute ceci */
* { color: red !important; }
```
Le texte à l'intérieur du `<task-card>` avec Shadow DOM doit **résister** à cette règle.

---

> **Indice 1** — `attachShadow` doit être appelé dans le `constructor`, pas dans `connectedCallback`. `attachShadow` ne peut être appelé qu'une seule fois par élément.

> **Indice 2** — Après migration, `this.querySelector(...)` ne trouvera plus rien à l'intérieur du composant. Remplace par `this.shadowRoot.querySelector(...)`.

> **Indice 3** — Le sélecteur `* { color: red !important }` dans `global.css` affectera l'hôte (`<task-card>`) mais pas son contenu dans le shadow root. Observe ce comportement dans les DevTools.

---

### Exercice 3.2 — `:host` et états via attributs

**Objectif :** Utiliser `:host` pour styler le composant selon ses attributs.

Crée un composant `<status-card>` qui accepte un attribut `type` parmi `info`, `warning`, `error`.

**Rendu visuel attendu :**
- `info` → fond bleu clair, bordure bleue
- `warning` → fond jaune, bordure orange
- `error` → fond rouge clair, bordure rouge
- Aucun attribut → styles neutres

Utilise uniquement `:host([type="..."])` dans les styles du shadow root, sans aucune logique JS pour les couleurs.

---

> **Indice 1** — Les sélecteurs `:host([type="info"])`, `:host([type="warning"])` etc. dans la `<style>` du shadow root réagissent automatiquement aux attributs de l'hôte. Tu n'as pas besoin de `observedAttributes` pour ça.

> **Indice 2** — La base du composant (shadow root simple, slot) peut tenir en 15 lignes. La vraie logique est toute dans le CSS.

> **Indice 3** — Teste en changeant l'attribut depuis la console : `document.querySelector('status-card').setAttribute('type', 'error')`. Le style doit changer immédiatement sans re-render JS.

---

### Exercice 3.3 — Slots nommés

**Objectif :** Construire un composant de layout flexible via des slots nommés.

Crée un composant `<modal-dialog>` avec la structure suivante dans son shadow root :

```
[header] — slot nommé "title"
[body]   — slot par défaut
[footer] — slot nommé "actions"
```

**Utilisation attendue :**
```html
<modal-dialog>
  <h2 slot="title">Confirmation</h2>
  <p>Êtes-vous sûr de vouloir supprimer cet élément ?</p>
  <div slot="actions">
    <button id="cancel">Annuler</button>
    <button id="confirm">Confirmer</button>
  </div>
</modal-dialog>
```

**Comportement supplémentaire :**
- Le composant affiche un attribut `visible` (boolean) — si absent ou `false`, le dialog est caché (`display: none` via `:host`)
- Un bouton "fermer" (×) dans le shadow root émet un événement `dialog-close` avec `composed: true`

---

> **Indice 1** — Pour l'attribut booléen `visible` : en HTML, la simple présence de l'attribut signifie `true`. Vérifie avec `this.hasAttribute('visible')`. En CSS : `:host(:not([visible])) { display: none; }`.

> **Indice 2** — Le bouton "fermer" est dans le shadow root. Pour que son événement soit reçu à l'extérieur du composant, il doit avoir `composed: true`. Dispatche l'événement depuis `this` (pas depuis le bouton interne) pour simplifier.

> **Indice 3** — Le contenu des slots reste dans le Light DOM. Pour y accéder depuis JS : `this.querySelector('[slot="title"]')` — note que c'est `this.querySelector`, pas `this.shadowRoot.querySelector`.

---

### Exercice 3.4 — CSS Custom Properties comme API

**Objectif :** Exposer des points de personnalisation explicites via des variables CSS.

Crée un composant `<progress-bar>` qui :
- Affiche une barre de progression (attribut `value` de 0 à 100)
- Expose ces variables CSS personnalisables :
  - `--progress-bar-height` (défaut : `12px`)
  - `--progress-bar-bg` (défaut : `#e0e0e0`)
  - `--progress-bar-fill` (défaut : `#4caf50`)
  - `--progress-bar-radius` (défaut : `6px`)

**Test de personnalisation dans `global.css` :**
```css
progress-bar {
  --progress-bar-fill: #2196f3;
  --progress-bar-height: 20px;
}

progress-bar[data-theme="danger"] {
  --progress-bar-fill: #f44336;
}
```

**Critère de succès :** La barre se personnalise uniquement via CSS, sans modifier le composant JS.

---

> **Indice 1** — Pour que la largeur de la barre reflète `value`, tu as besoin de CSS inline sur l'élément interne : `fillEl.style.width = this.getAttribute('value') + '%'`. Le CSS custom properties ne peuvent pas lire des attributs directement.

> **Indice 2** — Structure suggérée pour le shadow root : `<div class="track"><div class="fill"></div></div>`. Le `track` est le fond, le `fill` est la barre colorée.

> **Indice 3** — Observe dans les DevTools → Computed : les variables CSS résolues dans le shadow root. Tu verras comment la valeur de `--progress-bar-fill` traverse la boundary.

---

### Exercice 3.5 — `adoptedStyleSheets`

**Objectif :** Refactoriser un composant existant pour utiliser des styles partagés.

Reprends `<task-card>` ou `<status-card>` et :
1. Extrais les styles CSS dans un `CSSStyleSheet` construit avec `new CSSStyleSheet()`
2. Utilise `sheet.replaceSync()` pour y injecter les styles
3. Assigne-le via `this.shadowRoot.adoptedStyleSheets = [sheet]`

**Test de performance (qualitatif) :**
Crée 20 instances du composant dans la page. Vérifie dans les DevTools → Memory qu'il n'y a pas 20 copies des règles CSS.

---

> **Indice 1** — La variable `sheet` doit être déclarée **hors de la classe**, au scope du module. Ainsi, elle est créée une seule fois quand le module est importé, et partagée par toutes les instances.

> **Indice 2** — `innerHTML` sur le `shadowRoot` réinitialise `adoptedStyleSheets`. Si tu utilises `innerHTML` dans `connectedCallback`, réapplique `adoptedStyleSheets` après.

> **Indice 3** — Alternative à `innerHTML` pour éviter ce problème : utilise `document.createElement()` et `appendChild()` pour construire la structure DOM manuellement, sans passer par `innerHTML`.

---

## Checklist de sortie

- [ ] Tu sais attacher un Shadow Root et y rendre du HTML
- [ ] Tu comprends la différence entre Light DOM et Shadow DOM
- [ ] Tu sais utiliser `:host`, `:host([attr])`, `:host(:state)` pour styler l'hôte
- [ ] Tu sais créer des slots (défaut et nommés) pour la composition
- [ ] Tu comprends que les slots projettent sans déplacer le contenu
- [ ] Tu sais exposer une API de theming via CSS Custom Properties
- [ ] Tu comprends pourquoi `composed: true` est nécessaire pour les événements émis depuis l'intérieur d'un shadow root
- [ ] Tu connais `adoptedStyleSheets` et son avantage en performance

---

## Références

| Sujet | URL |
|---|---|
| Shadow DOM — MDN | https://developer.mozilla.org/fr/docs/Web/API/Web_components/Using_shadow_DOM |
| web.dev — Shadow DOM v1 | https://web.dev/articles/shadowdom-v1 |
| CSS Custom Properties — MDN | https://developer.mozilla.org/fr/docs/Web/CSS/CSS_cascading_variables |
| :host — MDN | https://developer.mozilla.org/fr/docs/Web/CSS/:host |
| CSSStyleSheet constructable — MDN | https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet/CSSStyleSheet |
| Slots — MDN | https://developer.mozilla.org/fr/docs/Web/API/Web_components/Using_templates_and_slots |