w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.44k stars 656 forks source link

[css-scoping-1] Dynamic changes of state and attributes referenced by :host-context rules are pretty hard to handle efficiently. #1914

Open emilio opened 6 years ago

emilio commented 6 years ago

In particular, there's no way to look into which shadow host is affected by a host-context selector without actually going through all the descendant shadow hosts of the element where the change is detected (and that'd be slow).

Blink supports this correctly restyling the whole subtree, link below, but I think that's pretty unfortunate, because that means that every change to any class, attribute, or any other state referenced in a host-context selector needs to do very expensive work that would otherwise be unnecessarily.

Additionally, you need to store the host-context rules out of band (outside of the shadow host style), because of the same reason, which is also not great, I think. This means that stylesheet data in shadow trees is no longer self-contained.

I was looking into implementing this (and other bits of Shadow DOM / CSS scoping) in Gecko, and I'm not opposed to taking the same approach, but I'm not sure if this is intentional or not, and I'd want it to discuss it before...

emilio commented 5 years ago

FWIW, this was discussed in the F2F, and the conclusion was that Blink will add counters and try to drop it, and that we should get feedback from other Apple people more familiar with Shadow DOM.

I guess given #3699 the second part is already there, and given it only has support from one implementation it doesn't really have a good reason to be in the spec.

emilio commented 5 years ago

Ah, apparently the IRC logs went to https://github.com/w3c/csswg-drafts/issues/1915#issuecomment-467590316 instead of here.

emilio commented 5 years ago

Anyhow, I'm in favor of dropping it.

rniwa commented 5 years ago

To repeat myself once again, Apple's WebKit team doesn't think changing the style of an element based on where it appears is a good practice to design a re-usable element in general, and this particular feature poses a significant implementation complexity & cost as pointed out by @emilio.

Support dropping this feature from the specification as we've previously requested.

lilles commented 5 years ago

Added separate use counters for live and snapshot profiles in Chromium M75.

pr3tori4n commented 5 years ago

2 use cases I'd like to suggest this supports is high level theming of a suite of components, and high level right-to-left or left-to-right localization detection.

The most basic example would be setting text-direction or a theme on the html element. <html class="darktheme dir="rtl">...</html> :host-context(".darktheme") <selector> { /*make it dark*/ } :host-context([dir="rtl"]) { /invert the margin for sides, etc./ } Beyond that, it would be nice to be able to sandbox a theme or text direction somewhere inside the body of a page, similar to what using an iframe can give you.

I'd advocate that a performant implementation of this selector could be viable if we limit the legal parents this could go on. For example, it could be limited to <html>, <body>, or the <shadowHostElement>. Introducing a limited implementation will go much farther in identifying usage numbers than simply checking how many people use it in Chrome today, because in professional sites browser support heavily impacts usage of any features - especially if it's not possible to polyfill them (please provide a polyfill if one exists but I've found none). This would eliminate the need to search the entire domTree for a valid selector in favor of a few targeted look ups.

rniwa commented 5 years ago

The directionality use case is best served by :dir pseudo selector. The dark theme case is best served via CSS custom variables. Note there is no guarantee that your component isn't used where the theme is defined so :host-context is awfully unsuited for theming purposes.

george-steel commented 4 years ago

A lot of its use cases could be replaced more efficiently by having a pseudo-class which tests whether an inherited (to avoid cycles) variable has a particular value.

danbeam commented 4 years ago

I agree with @rniwa on this specific matter -- :host-context([dir=rtl]) means "any ancestor with dir == 'rtl' above, even if that's not the effective directionality[1]" (which is why :dir exists). I get that it's better than nothing, but it's not bulletproof (and you can use a CSS var instead).

re: theming -- i think recent @media (prefers-color-scheme: light/dark) {...} is fine to embed inside of a component such that it adapts to system UI theme. I think you're able to specify your own themes as well, but it generally reduces the generic nature of web components the more you make them aware of their housing/surroundings. it's better to expose "parts" which can be styled externally, IMO.

[1] it'd be easy for a closer [dir="ltr"] ancestor to be ignored, as would be a direction: rule in CSS.

hober commented 4 years ago

Is there anything blocking this being removed from the spec? Given that Gecko and WebKit don't intend to implement, I don't think there's a path forward for this.

prantlf commented 3 years ago

Directionality and theming given as examples above are special cases, which have dedicated support in CSS (pseudo-selector and variables) already. But what would you suggest for specialising a style of a web component depending on its ancestor when both web components are in a single library?

This has been addressed by cascading before Shadow DOM and host-context made it available among web components. When a big widget is exposed as a web component, it will not mater, because such web component is not supposed to be affected by the rest of the page But what about web components in UI libraries, which are intended to be used as a composite of nested elements?

Example

Let us have three levels of heading (block) elements:

<e-h1>Heading 1</e-h1>
<e-h2>Heading 2</e-h2>
<e-h3>Heading 3</e-h3>

Styled by following stylesheets in their shadow roots:

/* e-h1, e-h2 and e-h3 */
:host {
  display: block;
  line-height: 1.2;
  margin: 0.75rem 0.5rem;
  font-weight: 500;
}

:host { font-size: 1.9rem } /* e-h1 only */
:host { font-size: 1.6rem } /* e-h2 only */
:host { font-size: 1.3rem } /* e-h3 only */

And rendered like this:

Bildschirmfoto 2021-05-28 um 13 22 59

Then, let us have an inline element for a small text:

<p>This is a normal text. <e-small>This is a small text.</e-smalll></p>

Styled by following stylesheet in its shadow root:

:host { font-size: 0.75em } /* e-small */

And rendered like this:

Bildschirmfoto 2021-05-28 um 13 29 02

Finally, let us have a subheading for each of the three levels of heading elements, implemented by specialising the small-text element:

<e-h1>Heading 1 <e-small>Subheading</e-small></e-h1>
<e-h2>Heading 2 <e-small>Subheading</e-small></e-h2>
<e-h3>Heading 3 <e-small>Subheading</e-small></e-h3>

Styled by adding the following styles to its shadow root:

/* e-small */
:host { font-size: 0.75em }

:host-context(e-h1), :host-context(e-h2), :host-context(e-h3) {
  display: block;
  margin-top: -0.25rem;
}

And rendered like this:

Bildschirmfoto 2021-05-28 um 13 22 32

Alternatives

If host-context is bad practice, how to avoid the need for cascading? Neither of the options below, that I was able to come up with, is without serious problems.

Dedicated Elements

Instead of overriding the style for a specific ancestor, dedicated descendants can be introduced, for example:

<e-h1>Heading 1 <e-sub-h1>Subheading</e-sub-h1></e-h1>
<e-h2>Heading 2 <e-sub-h2>Subheading</e-sub-h2></e-h2>
<e-h3>Heading 3 <e-sub-h3>Subheading</e-sub-h3></e-h3>

Increases the web component count unnecessarily, because e-sub-h* is obviously a child of a e-h*. Allows wrong using by putting e-sub-h2 to e-h1 or other combinations.

Additional Attributes

Instead of overriding the style for a specific ancestor, the specifics can be represented by attributes, for example:

<e-h1>Heading 1 <e-small heading=1>Subheading</e-small></e-h1>
<e-h2>Heading 2 <e-small heading=2>Subheading</e-small></e-h2>
<e-h3>Heading 3 <e-small heading=3>Subheading</e-small></e-h3>

Increases the verbosity unnecessarily, because e-small is obviously a child of a e-h*. Allows wrong using by using heading=2 withing e-h1 or other combinations.

Extra Stylesheet

Instead of overriding the style for a specific ancestor, the style override can be applied from the outer document, if it affects only the host element for example:

<e-h1>Heading 1 <e-small>Subheading</e-small></e-h1>
<e-h2>Heading 2 <e-small>Subheading</e-small></e-h2>
<e-h3>Heading 3 <e-small>Subheading</e-small></e-h3>
e-h1 e-small, e-h2 e-small, e-h3 e-small {
  display: block;
  margin-top: -0.25rem;
}

Unexpected, requiring a global stylesheet to be imported in addition to the encapsulated stylesheets within the web components.

LeaVerou commented 2 years ago

Seconding @prantlf, the use case of styling a component descendants based on the parent is a pretty big one. If it's only one level you can use :host(whatever) ::slotted(), but that doesn't work if there's a deeper hierarchy.

E.g. suppose you have <data-entry> that contains <data-point> elements, both of which behave differently in a <bar-chart> compared to a <pie-chart>, akin to how in HTML <option> behaves differently depending on whether it's inside a <select> or a <datalist>. How to implement that without :host-context()?

Westbrook commented 2 years ago

I think :host-context is yet another API that we've not been able to attain quality patterns or metrics around due to a lack of implementation. You can't really leverage it today unless you're shipping to Chrome only, otherwise the lengths that you would need to go to in order to support similar in Safari and Firefox would make it much more practical to use that same pattern in Chrome. In that way, maybe it is time to simply remove it.

However, in the intervening years, CSS at large has attained a componentization pattern that mimics this in the form of complex .el:is(...) selectors which amounts to the same functionality in a non-Custom Element context. In this way, I think the real question here is not "what :host-context() patterns would lead us to believe we shouldn't take this away?" and actually "why do we think that custom element users are not as deserving of the capabilities we've recently shipped cross browser to other developers via the .el:is(...) syntax?"

Thinking in this way, @prantlf's example might look like the following: https://codepen.io/Westbrook/pen/wvqWJrK

<e-h1>
  <template shadowroot="open">
    <style>
      :host {
        font-size: 3em;
        display: block;
      }
    </style>
    <slot></slot>
  </template>
  Heading 1
  <e-small>
     <template shadowroot="open">
       <style>
         :host {
           font-size: 0.75em;
           display: block;
         }
         :host(:is(e-h1 e-small)) {
           margin-top: -0.25em;
         }
       </style>
       <slot></slot>
    </template>
    Subheading
  </e-small>  
</e-h1>

As opposed to the similar no custom elements approach which is possible today via: https://codepen.io/Westbrook/pen/YzxWZjb

<style>
.e-small {
  display: block;
  font-size: 0.75em;
}

.e-small:is(.e-h1 .e-small, .e-h2 .e-small, .e-h3 .e-small) {
  margin-top: -0.25em;
}
<style>
<h1 class="e-h1">
    Heading 1
    <small class="e-small">
        Subheading
    </small>
</h1>
<h2 class="e-h2">
    Heading 2
    <small class="e-small">
        Subheading
    </small>
</h2>
<h3 class="e-h3">
    Heading 3
    <small class="e-small">
        Subheading
    </small>
</h2>

I'd really like to see :host-context() sanded out of the spec if it won't ever receive cross browser implementation, but the spec should support custom elements developers at the same level as it would non-custom elements developers. A great way to do that would be to support common CSS practices directly on :host so that it no longer requires the special casing that :host-context() affords.

emilio commented 2 years ago

If the host and the elements in nested trees are coordinated (as in both the "chart" and the "subheading" use cases) the outer component can definitely pass the relevant information to the inner components, right? Or what am I missing?

For the first example in thread it would look like:

--sub-heading-display: {inline, block};
--sub-heading-margin: <something>;

On the heading elements, then e-small would use those variables for example.

emilio commented 2 years ago

Also, in that case, the e-h* components could use ::slotted(e-small) { ... } (unless I'm also missing something). (Well, upon reflection that doesn't handle nested elements, so nevermind)

heyMP commented 2 years ago

If the host and the elements in nested trees are coordinated (as in both the "chart" and the "subheading" use cases) the outer component can definitely pass the relevant information to the inner components, right? Or what am I missing?

@emilio Definitely a fair point and in a design system it would make sense for the authors to make a sharable "context" between nested components. The downside is that you're recreating CSS cascading in javascript. Components would be required to tie into a shared context/provider API.

For example, our color context in Patternfly Elements. Our components are responsible for telling child components what background context they reside on. We do this by providers adding the on attribute to consumers.

<pfe-band color="dark">
  <section>
    <pfe-clipboard on="dark"></pfe-clipboard>
  </section>
</pfe-band>

pfe-clipboard.css

:host([on="dark"]) {
  --pfe-clipboard--icon--Color:  #fff;
  --pfe-clipboard--icon--Color--hover: #fafafa;
}

If pfe-clipboard had access to the parent context, it would eliminate the need for a proprietary context provider/consumer. We could target a parent attributes and inherit all of the cascading goodness:

pfe-clipboard.css

:host-context([color="dark"]) {
  --pfe-clipboard--icon--Color:  #fff};
  --pfe-clipboard--icon--Color--hover: #fafafa;
}
LeaVerou commented 2 years ago

If the host and the elements in nested trees are coordinated (as in both the "chart" and the "subheading" use cases) the outer component can definitely pass the relevant information to the inner components, right? Or what am I missing?

For the first example in thread it would look like:

--sub-heading-display: {inline, block};
--sub-heading-margin: <something>;

On the heading elements, then e-small would use those variables for example.

These are not always about setting the same property to different values, but one may need to set entirely different properties. I suppose you could pass down values for the union of all properties that may need to be set, but it gets awkward fast, and if you have multiple interesecting variations you could get a combinatorial explosion.

Westbrook commented 2 years ago

On the heading elements, then e-small would use those variables for example.

What happens when the parent element cannot be informed about the child element?

In the case of the <e-small> that could exist in an <h1>, how would I prepare the <h1> to pass that custom property? It's likely that known custom element parents isn't the clearest of examples here. This really looks to allow an HTML developer to understand the contexts in which they can place a custom element without having to reach into the definition of anything above that element (whether via JS or CSS) in their HTML.

In the case that this sort of location based property setting was customizable, documenting a Custom Property for a consumer to leverage could definitely be a possibility, but if it were meant to only be enumerable (possessing a fixed number of values) then documenting a class or :host-context 😉 for consumers to establish would allow this to be set once and cascade to all of the <e-small> elements in that context without the consumer having to go to each <e-small> and enumerate it directly.

b-houghton commented 2 years ago

I'd really like to see :host-context() sanded out of the spec if it won't ever receive cross browser implementation, but the spec should support custom elements developers at the same level as it would non-custom elements developers. A great way to do that would be to support common CSS practices directly on :host so that it no longer requires the special casing that :host-context() affords.

This would indeed be valuable. Has there been any consensus and progress on this?

woody-li commented 1 year ago

Another solution to replace host-context maybe container style queries. Use CSS variable to define state you wanted, and use the inherited variable to query state in anywhere.

Browsers:

https://caniuse.com/css-container-queries-style

certainlyakey commented 7 months ago

@woody-li I was indeed able to implement a dark mode scenario (i needed to manually enable dark/light mode in a Shadow DOM based component) using container style queries, by adding a CSS variable to an ancestor. This removed a need for host-context().

However the lack of support of other vendors is somewhat worrying.