WICG / construct-stylesheets

API for constructing CSS stylesheet objects
Other
138 stars 25 forks source link

Should adoptedStyleSheets be ordered before other style sheets in the tree, instead of after? #93

Open bicknellr opened 5 years ago

bicknellr commented 5 years ago

From https://wicg.github.io/construct-stylesheets/#using-constructed-stylesheets:

The user agent must include all style sheets in the DocumentOrShadowRoot's adopted stylesheets inside its document or shadow root CSS style sheets.

These adopted stylesheets are ordered after all the other style sheets (i.e. those derived from styleSheets).

Is there a particular reason that style sheets in adoptedStyleSheets are ordered after style sheets from <style>s in the associated tree? AFAICT, adoptedStyleSheets is being designed primarily as a mechanism for sharing style sheets amongst many elements, but it feels strange that shared styles would take precedence over styles that are certainly only applicable to a particular instance (i.e. <style> in the associated tree).

p.s. Yes, you could just add your instance-specific style sheets to the end of the instance's adoptedStyleSheets instead of inserting them into the tree, but why is that necessary?

tabatkins commented 5 years ago

They have to be put somewhere, and putting them before all the stylesheets coming from link/etc seems odder than putting them after.

bicknellr commented 5 years ago

Sounds like this is a subjective issue, so here's my preferred color for the bike shed:

I conceptually think of the style sheets in a single root as if they were concatenated into a single style sheet since they pretty much work this way. Adding a style sheet to adoptedStyleSheets seems akin to adding @import statements to the concatenated style sheet. When importing a style sheet with @import, I'd usually put it at the top so that my inline rulesets will override anything that I'm getting from the import.

So, if we use this example:

const sheet1 = new CSSStyleSheet();
sheet1.replaceSync(".foo { color: red; }");

const div = document.createElement("div");
const shadowRoot = div.attachShadow({ mode: "open" });
shadowRoot.adoptedStyleSheets = [sheet1];
shadowRoot.innerHTML = `
<style>.foo { color: blue; }</style>
<span class="foo">Hello!</span>
`;

It feels more like it should be:

@import 'data:text/css,.foo { color: red; }';
.foo { color: blue; }

than:

.foo { color: blue; }
@import 'data:text/css,.foo { color: red; }';

I think this sentiment becomes even more applicable if CSS Modules become a thing. But I totally recognize that it's not critical since I could just stop adding inline <style> elements and switch everything over to using adoptedStyleSheets to get the same effect.

tabatkins commented 5 years ago

I see where you're coming from within shadow roots, but consider the issue on the outer page, where .styleSheets will usually only include stylesheets coming from <link> in the <head>. It seems a lot weirder (to me, at least) for the JS-created adopted stylesheet to default to being earlier in the cascade than the <link rel=stylesheet>.

If we could somehow put it in the middle, where everything in the head came first, then adopted stylesheets, then style in body, I think that would be the best solution. But all markup-based stylesheets are bodged together into .styleSheets, so we instead have to choose to put the adopted sheets before or after all of them. :/

Putting JS-managed stuff after HTML-managed stuff is the general pattern for this sort of thing, so that's what we went with.

tabatkins commented 5 years ago

(Really, what we want is the ability to tag the adopted stylesheet as part of a specific Cascade Origin, so you could set things up as being "user-agent" level, and thus automatically overridden by anything at author-level, such as style contents. But that's not currently possible; perhaps in a v2?)

bicknellr commented 5 years ago

What if adoptedStyleSheets was moved from DocumentOrShadowRoot to HTMLStyleElement? Instead of attaching style sheet objects to a single bucket that applies to the entire root and requires making the 'before vs. after' decision, you would attach them to a specific node instead and inherently be able to decide the placement yourself.

Main document:

const externalStyles = document.createElement('style');
externalStyles.adoptedStyleSheets = [a, b, c];
// If you want adopted styles after everything else you've imported:
document.head.appendChild(externalStyles);

In a ShadowRoot:

const div = document.createElement("div");
const shadowRoot = div.attachShadow({ mode: "open" });
shadowRoot.innerHTML = `
<style>.foo { color: blue; }</style>
<span class="foo">Hello!</span>
`;
const externalStyles = document.createElement('style');
externalStyles.adoptedStyleSheets = [a, b, c];
// If you want adopted styles before everything in the root:
shadowRoot.insertBefore(externalStyles, shadowRoot.firstChild);

You could put them in multiple places if that's what you needed:

<style id="alwaysLoses"></style>
<style>.foo { color: red; }</style>
<style id="alwaysWins"></style>
<script>
document.getElementById('alwaysLoses').adoptedStyleSheets = [a, b];
document.getElementById('alwaysWins').adoptedStyleSheets = [c, d];
</script>

edit: Removed an incorrect line in the second example.

domenic commented 5 years ago

If you're going to give them before/after locations in the DOM tree, they should just be DOM nodes. For example, HTMLStyleElement.

bicknellr commented 5 years ago

If you're going to give them before/after locations in the DOM tree, they should just be DOM nodes. For example, HTMLStyleElement.

CSSStyleSheet could have a method that produces a new HTMLStyleElement associated with the style sheet. Using createLinkedStyleElement as the name, then this example (that I updated to use prepend)

const div = document.createElement("div");
const shadowRoot = div.attachShadow({ mode: "open" });
shadowRoot.innerHTML = `
<style>.foo { color: blue; }</style>
<span class="foo">Hello!</span>
`;
const externalStyles = document.createElement('style');
externalStyles.adoptedStyleSheets = [a, b, c];
shadowRoot.prepend(externalStyles);

would become

const div = document.createElement("div");
const shadowRoot = div.attachShadow({ mode: "open" });
shadowRoot.adoptedStyleSheets = [sheet1];
shadowRoot.innerHTML = `
<style>.foo { color: blue; }</style>
<span class="foo">Hello!</span>
`;
shadowRoot.prepend(...[a, b, c].map(sheet => sheet.createLinkedStyleElement()));

Is that kind of what you were thinking?

domenic commented 5 years ago

Yes, although I'm not sure there's too much value in indirecting through CSSStyleSheet instead of just creating a HTMLStyleElement directly.

bicknellr commented 5 years ago

I was thinking that the node created by createLinkedStyleSheet would continue to be associated with the CSSStyleSheet rather than containing a copy of the style sheet at the time they were created - replace, etc. would 'update' all of the associated nodes.

domenic commented 5 years ago

I was thinking even simpler: just use HTMLStyleElement only, don't ever use a CSSStyleSheet apart from htmlEl.sheet.

bicknellr commented 5 years ago

I'm not sure I follow - could you give an example?

manucorporat commented 5 years ago

document.adoptedStylesheet will be used most of the time by web component authors to provide the default styles of components, leaving the users the ability to customizable and override the styles using link or inlined <style>. This is pretty much broken, when they are applied after.

In fact, I thought it behaved like user agent's stylesheets.

While this problem is more important in the document case, it's too for shadow-dom, where developers should be able to add a