-
Notifications
You must be signed in to change notification settings - Fork 375
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
idea: Allow light DOM to delegate styles/behavior to shadow DOM #883
Comments
What does "defer its styling to the shadow root's styles" mean? How does shadow root specifies styles on those elements? Also, are you talking about assigned nodes or any descendent of assigned nodes? Because if it's latter, then there is a problem with ambiguity. A node can have N ancestor nodes each of which can have its own shadow root. |
I am imagining the latter and had assumed that more local shadow roots would have higher specificity (that is, lower in the cascade). |
I'm not clear what is meant here. Can you provide code examples (with bikesheddable naming)? |
Sure thing, what I'm thinking is that a light DOM author might want to give control to part of its markup to some parent. Imagine this being like a design system implementation: const sheet = new CSSStyleSheet();
sheet.replace(`
::control(ds-input) {
border: 2px solid tomato;
background: gainsboro;
}
`);
class DesignSystem extends HTMLElement {
constructor() {
super();
const root = this.attachShadow({ mode: 'open' });
root.adoptedStyleSheets = [sheet];
root.innerHTML = `<slot></slot>`;
// other stuff maybe
}
}
customElements.define('x-ds', DesignSystem); then in the HTML <x-ds>
<h1>Whatever</h1>
<form>
<label for="whatever">
<input id="whatever" name="whatever" control="ds-input">
</form>
</x-ds> That's a really simple example, but it illustrates the idea. Let's make it a bit more concrete, imagine you're defining a specialized dialog component that your users might have a need to control the close button but still want it to be styled by the shadow host: const sheet = new CSSStyleSheet();
sheet.replace(`
::control(close-btn) {
background: url(someSVGThatLooksLikeAnX) no-repeat;
}
`);
class Dialog extends HTMLElement {
constructor() {
super();
const root = this.attachShadow({ mode: 'open' });
root.adoptedStyleSheets = [sheet];
root.innerHTML = `<slot></slot>`;
if (this.controls['close-btn']) {
this.controls['close-btn'].addEventListener('click', this.close.bind(this));
}
}
close() { /* magic */ }
} Then there are two uses of the above component:
Let's take scenario two further, say our dialog also has special heading styles, it doesn't matter how deeply nested the headings might be in the DOM structure, an API like this could allow the shadow host to style those and still not break encapsulation because the child has opted in to the change rather than having it forced. <x-dialog>
<heading>
<h1 control="dialog-h1">Hello world</h1>
<button control="close-btn" aria-label="close" onclick="someCallback"></button>
</heading>
<section>
<h2 control="dialog-h2">How are you doing?</h2>
<p>Lorem ipsum dolor sit amet</p>
</section>
</x-dialog> |
Ah
Hah! Funny! That's very similar to what I wanted to do and asked about on StackOverflow yesterday: https://stackoverflow.com/questions/62842418 The example from that question is <div>
<p><span>test</span></p>
<p><span>test</span></p>
<p><span>test</span></p>
<p><span>test</span></p>
<p><span>test</span></p>
</div>
<script>
const d = document.querySelector('div')
const r = d.attachShadow({mode: 'open'})
r.innerHTML = `
<style>
/* This doesn't work as I was hoping: */
::slotted(p) span {
border: 1px solid deeppink;
}
/* This doesn't work (and I wouldn't expect it to), but I tried it anyways: */
::slotted(span) {
border: 1px solid deeppink;
}
/* This doesn't work either: */
:host span {
border: 1px solid deeppink;
}
/* This works, but not what I'm trying to do. */
::slotted(p) {
background: #f9f9f9
}
</style>
<slot></slot>
`
</script> But you can also imagine how it would work with a custom element that creates its own I think I like the Here's what your example would look like if what I was asking about worked (as one would intuitively expect it to): const sheet = new CSSStyleSheet();
sheet.replace(`
::slotted(*) .ds-input {
border: 2px solid tomato;
background: gainsboro;
}
`);
class DesignSystem extends HTMLElement {
constructor() {
super();
const root = this.attachShadow({ mode: 'open' });
root.adoptedStyleSheets = [sheet];
root.innerHTML = `<slot></slot>`;
// other stuff maybe
}
}
customElements.define('x-ds', DesignSystem); and the user would write <x-ds>
<h1>Whatever</h1>
<form>
<label for="whatever">
<input id="whatever" name="whatever" class="ds-input">
</form>
</x-ds> This is interesting because the user can write familiar markup using What I find unintuitive is why Your idea with this.shadowRoot.querySelector('::slotted(*) .close-btn').addEventListener('click', this.close.bind(this)); I think these things should be on this.shadowRoot.controls['close-btn'].addEventListener('click', this.close.bind(this)); This is especially important because if the shadow root mode is
|
I forgot that we can programmatically select nodes with Although we can currently select nodes using that Your last end-user example would be as follows if the custom element could rely on <x-dialog>
<heading>
<h1 class="dialog-h1">Hello world</h1>
<button class="close-btn" aria-label="close" onclick="someCallback"></button>
</heading>
<section>
<h2 class="dialog-h2">How are you doing?</h2>
<p>Lorem ipsum dolor sit amet</p>
</section>
</x-dialog> |
@rniwa Now that @calebdwilliams cleared it up with examples, he is referring to styling descendants of assigned nodes. We both proposed a way to do it, and also ways to make it easier to select those elements.
@rniwa what did you mean by that? And now that the examples are clear, does that concern still hold? I would imagine that the styling (and selection) would only apply to elements in the custom element's light tree, and does not penetrate into any shadow roots of that light tree. However I can imagine no-penetration being undesirable for certain cases. For this I think we can incorporate the existing If we improve Perhaps For example,
It seems to me like this would be great and super useful. |
A reason why
From Google Developer Documentation:
I prefer to teach <slot> content is reflected to shadowDOM. <div>
<p><span>test</span></p>
<p><span>test</span></p>
</div>
<script>
document.querySelector('div').attachShadow({mode: 'open'}).innerHTML = `
<style>
/* This doesn't work as I was hoping: */
::slotted(p) span {
border: 1px solid deeppink;
}
</style>
<slot></slot>`
</script>
<style>
/* This correctly styles the reflected (slotted) content: */
p span {
border: 1px solid deeppink;
}
</style> Yes, that means you need a container element with shadowDOM, if you do not want to bleed CSS to your main DOM It also means content from multiple sources can reflect to one slot: <todo-list>
<to-do slot=todo><span>One</span></to-do>
<to-do slot=todo><span>Two</span></to-do>
<to-do slot=todo><span>Three</span></to-do>
<to-do slot=done><span>42</span></to-do>
<to-do slot=todo><span>Five</span></to-do>
</todo-list> I presume that makes a more advanced More: https://stackoverflow.com/questions/61626493/slotted-css-selector-for-nested-children |
@Danny-Engelman hence the proposal to introduce an inverted part. I don’t pretend to understand all of the things that go into selector optimization, but I imagine that introducing a new API could fix that but also address the concerns of not breaking encapsulation. |
Key here is slotted content is not a copy, and not the original, but a reflection You want the mirror (shadowDOM slot) to style the displayed reflection (slotted lightDOM) That was tried in V0 of the spec with The main takeway from the W3C standards discussions (@hayatoito #331 here and #745 here) is: |
@Danny-Engelman Thanks for the thoughts!
That selector would do what it says verbatim: select the sibling If the sibling It would also be convenient for getting references: The code for const d = document.querySelector('div')
const r = d.attachShadow({mode: 'open'})
r.innerHTML = `
<div>
<slot></slot>
</div>
`
const start = performance.now()
// Emulate querySelector('::slotted(*) span')
const slots = r.querySelectorAll('slot')
const queryResult = []
// outer loops gets result for `::slotted(*)`
for (let i = 0, l = slots.length, slot, elems; i < l; i += 1) {
slot = slots[i]
elems = slot.assignedElements()
// inner loop gets result for ` span`
for (let j = 0, l = elems.length; j < l; j += 1) {
queryResult.push(...elems[j].querySelectorAll('span'))
}
}
const end = performance.now()
const total = end - start
console.log(`The query took ${total} milliseconds.`)
console.log(...queryResult) and similar for polyfilling other queries. Putting it this way, it is easy to imagine how queryResult.push(...elems[j].querySelectorAll('.bar')) with if (elems[j].nextElementSibling?.tagName === 'P')
queryResult.push(elems[j].nextElementSibling)
The performance claims are speculative. 🤔 As with any selector, users should save the results to variables rather than executing them over and over (f.e. in an animation).
I'm not sure what you mean by "anywhere in the AST, not just a sub-tree", but what I propose is that the postfix combinators run in the light tree (which to me intuitively makes sense). What I've proposed actually doesn't change the performance of Here's a live example of the above code emulating https://codepen.io/trusktr/pen/0a71670d0da693a4758bc4f36463452f?editors=1010 On my computer the output is I really don't think there's a performance issue here that is any different than all other existing selectors. I don't think we can really use this claim against allowing combinators postfixed to I believe that performance claim is a moot point because we can easily make selectors that will perform a lot worse than
There's a problem with this: this is how a user styling can be applied to content that is passed to a custom element. This does not allow a custom element author to perform the styling. Allowing custom element authors to perform the styling of the light tree (without users having to think about it) is a valid use case and makes a variety of things possible (f.e. user provides markup and the output looks good without the user also having to stick additional styles or stylesheets outside of the custom element).
I think teaching people using the standard spec terminology is the best way to go, so that the concepts match up across articles and guides.
But the terminology that you use really has no bearing on the topic; it only describes what currently exists. If the spec were expanded to allow the combinators postfixed after On a similar note, https://codepen.io/trusktr/pen/d4a45f1efc9eccc0fdb20164566bada4?editors=1010 (shows In my opinion there's really not a good reason why a custom element author should not be able to style deeper elements within slotted elements (and also select them with |
It seems to me that any performance issue is specific to the particular vendor implementation. I really can't imagine why a To those people re-running selectors 60 times per second during an animation: just don't do that! Instead save the result to a variable before the animation. |
I'm not really talking about an extension of |
@calebdwilliams Basically what you're proposing is an alternative that (supposedly) avoids perf costs of what I am describing, right? I asked at #745 (comment) and #889 (comment) about the actual performance metrics. It would be helpful if browser authors could point to some performance evidence. The use case that I showed the snippet of code for in #889 (comment) seems very valid, and I doubt it would cause any performance issue, for example. Of course, I also don't have performance metrics, but then again I'm not authoring the web engine. What I can do is try to make a polyfill, and see how that performs. |
@trusktr thats exactly right. Based on previous discussions in this repo I’m led to believe that complex |
I have an idea that is tangentially related to the "allow complex selectors in ::slotted: and related issues (as referenced in #745, #881 and some others), but I felt the concept was different enough to warrant its own post (if the community disagrees, I'm happy to delete this issue and move it to one of those other threads).
I would like an API that is an inversion of the
::part
API (I don't have a good name for it right now), but one where slotted content (even if it is nested within a DOM structure) could defer its styling to the shadow root's styles (like a dialog styling certain buttons to look a specific way but keeping them visible to the light DOM for integration testing, removal, etc.).This same API could potentially be used to allow custom elements to control the behaviors attached to certain slotted elements (again, a dialog might be able to say a slotted button with a certain attribute might act as a close button).
This is different from previous proposals in that all content within the light DOM isn't available to the shadow root, but only those elements the light DOM author chooses to defer to the shadow root..
The text was updated successfully, but these errors were encountered: