How to position nested shadow DOMs on top of each other?
Asked Answered
D

1

0

Is it possible to position nested shadow DOMs on top of each other?

With normal DOM, nested divs just stack by default:

.d1 {
  width: 150px;
  height: 150px;
  background-color: lightseagreen;
}
.d2 {
  width: 100px;
  height: 100px;  
  background-color: darkslateblue;

}
.d3 {
  width: 50px;
  height: 50px;
  background-color: lightgray;
}
<div class="container">
  <div class="d1">
    <div class="d2">
      <div class="d3">
      </div>
    </div>
  </div>
</div>

But with shadow DOM, how can this be done?

id = 0;
depth = 3;
customElements.define('box-component', class extends HTMLElement {
  constructor() {
    super();
    
    if (id > depth) 
        return;
    this.id = id++;
    
    var template = document.getElementById("box-template");
    const shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = template.innerHTML;
    
    // The only goal of all of the following boilerplate is to set 'exportparts' attribute,
    // such that each parent exports its *descendants* (not just *children*) parts.
    // This is done by accessing the shadow dom of the child from its parent.
    // (note: the outermost component doesn't need to export its parts)
    if (id == 0)
        return;
    
    const parts = [...this.shadowRoot.children]     
      .filter(elem => elem.getAttribute('part'))
    
    let thisExportParts = null;
    let childExportParts = null;
    
    thisExportParts = parts.map(elem => [
      [elem.getAttribute('part'), `${elem.getAttribute('part')}${this.id}`].join(':')
    ].join(','));
    
    if (this.shadowRoot && this.shadowRoot.children) {
        const childShadowRoot = 
        [...this.shadowRoot.children].filter(elem => elem.shadowRoot)[0];
      if (childShadowRoot) {
        const fullChildExportParts = childShadowRoot.getAttribute('exportparts');
        if (fullChildExportParts) {
          childExportParts = fullChildExportParts.split(',').map(part => {
            return part.includes(':') ? part.split(':')[1] : part;
          }).join(',');
        }
      }
    }
    this.setAttribute('exportparts', [thisExportParts, childExportParts].filter(_ => _).join(','));
  }
});
box-component::part(box1) {
  width: 150px;
  height: 150px;
  background-color: lightseagreen;
}
box-component::part(box2) {
  width: 100px;
  height: 100px;
  background-color: darkslateblue;
}
box-component::part(box3) {
  width: 50px;
  height: 50px;
  background-color: lightgray;
}
<template id="box-template">
  <style> 
  </style>
  <div part="box"></div> 
  <box-component></box-component>
</template>

<box-component class="container"></box-component>
Dalt answered 12/10, 2022 at 15:58 Comment(1)
Further reading about using exportpartsDalt
D
1

It's possible. In the shadow DOM, the following should be added:

:host { 
  position: absolute; 
  top: 0;
}

This :host rule styles all instances of the custom component element (the shadow host) in the document to be absolutely positioned, while also making the positioning relative to each positioned parent (that's why top: 0 is needed).

In fact, this is similar to changing the normal DOM code in the question to use positioning (note that without positioning, just setting top: 0 won't work (further read)):

.container {
  position: relative;
}
.container div {
  position: absolute;
  top: 0;
}
.d1 {
  width: 150px;
  height: 150px;
  background-color: lightseagreen;
}
.d2 {
  width: 100px;
  height: 100px;  
  background-color: darkslateblue;

}
.d3 {
  width: 50px;
  height: 50px;
  background-color: lightgray;
}
<div class="container">
  <div class="d1">
    <div class="d2">
      <div class="d3">
      </div>
    </div>
  </div>
</div>

So, here's the full code:

id = 0;
depth = 3;
customElements.define('box-component', class extends HTMLElement {
  constructor() {
    super();
    
    if (id > depth) 
        return;
    this.id = id++;
    
    var template = document.getElementById("box-template");
    const shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = template.innerHTML;
    
    // The only goal of all of the following boilerplate is to set 'exportparts' attribute,
    // such that each parent exports its *descendants* (not just *children*) parts.
    // This is done by accessing the shadow dom of the child from its parent.
    // (note: the outermost component doesn't need to export its parts)
    if (id == 0)
        return;
    
    const parts = [...this.shadowRoot.children]     
      .filter(elem => elem.getAttribute('part'))
    
    let thisExportParts = null;
    let childExportParts = null;
    
    thisExportParts = parts.map(elem => [
      [elem.getAttribute('part'), `${elem.getAttribute('part')}${this.id}`].join(':')
    ].join(','));
    
    if (this.shadowRoot && this.shadowRoot.children) {
        const childShadowRoot = 
        [...this.shadowRoot.children].filter(elem => elem.shadowRoot)[0];
      if (childShadowRoot) {
        const fullChildExportParts = childShadowRoot.getAttribute('exportparts');
        if (fullChildExportParts) {
          childExportParts = fullChildExportParts.split(',').map(part => {
            return part.includes(':') ? part.split(':')[1] : part;
          }).join(',');
        }
      }
    }
    this.setAttribute('exportparts', [thisExportParts, childExportParts].filter(_ => _).join(','));
  }
});
box-component::part(box1) {
  width: 150px;
  height: 150px;
  background-color: lightseagreen;
}
box-component::part(box2) {
  width: 100px;
  height: 100px;
  background-color: darkslateblue;
}
box-component::part(box3) {
  width: 50px;
  height: 50px;
  background-color: lightgray;
}
<template id="box-template">
  <style> 
    :host { 
      position: absolute; 
      top: 0px;
    }
  </style>
  <div part="box"></div> 
  <box-component></box-component>
</template>

<box-component class="container"></box-component>
Dalt answered 12/10, 2022 at 15:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.