How to style a custom HTML element based on child node selector(s), using a shadow DOM attached stylesheet?

49 views Asked by At

I have a custom HTML element which may group button elements:

<custom-element>
    <button>Foo</button>
    <button>Bar</button>
    <button>Baz</button>
</custom-element>

I link a stylesheet to the element using the adoptedStylesheets property on the shadow root (shadowRoot property on the element, in the "open" mode).

The button's are "slotted" (assigned to slot elements I have in the shadow tree).

I have a "prompt" element in the shadow tree of the custom element:

<span class="prompt">Use one of the following button(s)</span> 

I'd like to toggle displaying of the above element based on whether all of the button element children of their custom-element parent have each their hidden attribute set or not -- if all button(s) are thus hidden, prompt shall not be displayed (display: none), otherwise it shall be (display: revert).

Hiding .prompt by default -- assuming there are only hidden buttons, was easy:

.prompt { display: none; }

I am unable, however, to come up with a selector that will do the opposite -- show .prompt when at least one button is not hidden (inverse condition of "all buttons are hidden").

I've tried a number of selector permutations, but none seem to do what I want:

:host(:has(::slotted(button:not([hidden])))) .prompt {
    display: revert;
}
:host(:has(button:not([hidden]))) .prompt {
    display: revert;
}
:host(:has(::slotted(button:not([hidden])))) .prompt {
    display: revert;
}

Is it even possible to select a descendant of the custom element along a branch of the shadow tree, based on a predicate applying to the custom element along the light DOM tree, specifically one like "contains button children that do not feature the hidden attribute"?

The problem seems to lie in the :has selector -- I am unable to use :host(:has(...)) for any .... I have seen mention that :host is featureless, yet :host .prompt works as expected, for example, so I am further confused about exactly which features whatever is selected by :host or :host(...), has.

Anyway, I am curious as to my immediate practical problem -- is CSS alone able to pull this off? Otherwise I am using ResizeObserver to respond to changes in the element's bounding box, taking these as indirect signs of toggling of hidden attribute on the buttons. Its not a bad alternative, but ideally I'd like CSS alone to do this.

I have half a month old Firefox, Edge and Chrome releases I have been testing with.

1

There are 1 answers

7
Danny '365CSI' Engelman On

:host (inside shadowDOM) selects the element itself, :host .prompt selects elements inside shadowDOM

:host(x) selects attributes on the element

You will not get to your buttons (in lightDOM) there

I am no CSS guru, don't think it can be done with CSS
+ matches 2 siblings, you can't match 3?

Isn't it easier to have the element register one click handler on itself and query over the 3 buttons, setting an attribute on the element so CSS in shadowDOM can be used

My interpretation

button + button can only select two elements

button + button + button does not work

So you have to always check the state of your buttons:

<script>
  customElements.define("three-buttons", class extends HTMLElement {
    constructor() {
      super().attachShadow({mode: "open"})
        .innerHTML = `<style>`+
        `:host { padding:1em }` +
        `:host([three]){background:green}`+
        `::slotted([hidden]) { background: red; display: revert }` +
        `</style><slot/>`;
    }
    connectedCallback() {
      setTimeout(() => { // wait till buttons are parsed
        this.addEventListener("click", (e) => {
          let btn = e.target;
          if (btn.id == "reset") {
            this.querySelectorAll("*").forEach(b =>  b.hidden = false);
          } else {
            btn.hidden = !btn.hidden;
            let hiddenbuttons = this.querySelectorAll("button[hidden]");
            this.toggleAttribute("three", hiddenbuttons.length == 3);
          }
        });
      })
    }
  });
</script>
<three-buttons>
  <button>b1</button><button>b2</button>
  <button>b3</button><button>b4</button>
  <button id="reset">reset</button>
</three-buttons>