-
Notifications
You must be signed in to change notification settings - Fork 693
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
[css-shadow-parts] Unifying ::part() and ::--foo #4900
Comments
I'm in favor of this change and also favor 3. I was considering something along these lines as a migration from the current syntax to I still have issues with the current design of shadow parts using multiple part names as an ersatz class/pseudo-class mechanism. I'd much rather see us fix selectors, e.g. your example could be I don't have an issue with parts having multiple names, but that should only mean it can be selected as either of those names, e.g. <custom-day part="--day --weekend"> matches both |
Credit where credit's due: I think the idea to unify these was from @justinfagnani. |
I disagree. First, a more abstract objection: I don't think the tagname/classname distinction we currently have in the DOM is a useful distinction. It's something we inherited from DOM, but there's no meaningful difference between the two semantically; we even invented two "pretend I don't have a tagname" elements ( So I don't think we should be trying to introduce the same distinction for custom elements. Elements have a set of idents with class-like semantics; there's no need for a tagname-like semantic, and forcing authors to decide which of their several possible idents qualify as "most tagname-like" is a decision we shouldn't force authors to make. We want to offer a tagname-like syntax for convenience, nothing more. Second, a more concrete objection: having a custom element expose its part semantics as pseudo-classes clashes with the existing custom pseudo-class proposal. As I discussed in #7, I've come to believe that custom elements are indeed the most appropriate place in the DOM to expose custom pseudo-classes; in particular, I think there is no need to add them to arbitrary elements in the light dom. Custom Elements have their own, component-controlled semantics that need to be deployed as a separate namespace from the author-level semantic (classnames), so a pseudo-class with a distinct syntax space works out fine. Exposed parts hit the same conflict: the part itself might be a custom element, which exposes its own custom pseudo-classes. The outer component already has a place to indicate class-like semantics on its elements, in the form of the part name; merging the two means there can be clashes between two not-necessarily-cooperating elements, same as a custom element exposing its states by manipulating its host's (In this, I am assuming that the part's custom pseudo-classes will be available for matching, same as most UA-defined pseudo-classes are. I don't think this is an encapsulation break.) |
So I gave some more serious thought to what it would look like to let parts expose pseudo-classes as well, and yes, I'm pretty certain it introduces unresolveable problems. So here's my sketch of how this all would work (forgive me for being somewhat stream-of-consciousness with it, tldr at the end): First, a component can, itself, expose custom pseudo-classes declaring its state to the consumer of the component. This uses the API we're already talking about, no elaboration needed here. Then, the component can expose parts of itself as pseudo-elements. Since our current concept of "part names" are class-like, but the pseudo-element syntax is restricted to not allow that semantic, we change things up a bit - custom elements can just expose one or more (If we allow the author to denote one of the keywords as the "name" of the pseudo, so the user could instead write But the part could itself be a custom element, which can expose both (a) its own pseudo-classes, and (b) its own parts. If the inner component's pseudo-classes are automatically exposed to users of the outer components, we have an obvious potential for clashes, causing upgrade hazards we've tried hard to avoid; the If the inner component's pseudo-classes are not automatically exposed, but rather have to be forwarded by the outer component, this is avoided, but it means more work for the component author: you can't just use a But that's not all, the inner component can also have parts to expose, which, per our existing resolutions, need to be explicitly forwarded as well. So the outer component has to have a separate mechanism to filter/forward the parts. But now we have an interesting question: how? By the pseudo-classes it exposes, presumably? Or does it distinguish between the (external) part-defined pseudo-classes and the (internal) component-defined ones, and only filter on the former? And then how do you decide which of the forwarded part's pseudo-classes should be exposed? Filtering is necessary at every level, after all; there's no good reason you should be able to filter your own parts but not any forwarded parts. Do we need a third forwarding mechanism to handle those? How, exactly, do we identify which forwarded parts are having their pseudoclasses filtered? The only way to refer to them is by their pseudo-classes, after all; this isn't like our own parts, where the element being filtered is the one we are adding the attributes to. So instead we're now dealing with filtering sets of forwarded parts, and have to deal with single parts showing up in multiple such sets and having clashing forwarding advice, too. (Luckily we don't recurse further; any components nested further within the inner component have already been forwarded and filtered by the inner component, and we just have to handle its results.) So yeah, ultimately the issue is that we're collapsing levels, something which is notoriously fraught. Part names and component pseudo-classes are distinct namespaces because they're controlled by different authors, but if we push them into the same namespace, avoiding clashes becomes a lot more complicated, to the point that I don't think authors can reasonably navigate it. So I'm pretty strongly still of the opinion that part names and custom pseudo-classes, despite their partial overlap in semantics, should be kept separate. And that custom pseudo-classes should be exposed on parts by default, without any filtering mechanism. |
I think you mean WICG/custom-state-pseudo-class#7. |
Now that CSS WG has resolved to rename It looks like the conversation wandered to many tangentially related things, but the crux of this issue is renaming |
The conversation has some critical points though: This change is not a simple rename. There is no custom-pseudo-element equivalent to |
If that’s the case, it’s sad that consistency is not possible, and that impossibility of consistency was not really considered when resolving to do the |
@othermaciej We did consider that. The initial post in this issue is the proposal as it stands - you should be able to match a part name like "--foo" as There's an open design question in the OP; opinions would be appreciated! |
Having two names for the same thing is kind of lame. I wonder if |
As a library maintainer, I find the new syntax questionable in terms of developer experience. ::part(form-field):state(pristine) 👍 human friendly ::--form-field:--pristine 👎 confusing Off-topic: what if we make it even more BEM-ish? 🤔 ::__form-field:--pristine |
Hmm, it would work parsing-wise — that's a It looks a little clunky to me, but that might be a kneejerk. I suppose I'm not opposed to it. Is that something you want to champion, then? @web-padawan: Note how, if these were built-in pseudos, you'd spell them |
I don't know. It looks weird to me, but all the colon/dash prefixed things look weird to me, so that doesn't mean much. I do think it would be nice if we could avoid two aliases for the same thing. Side note: is it useful to do what ::part(foo bar:baz) does for custom parts for built-in pseudo-elements? I could totally imagine a native date picking exposing parts similar to what you had for your custom calendar, but it would apparently have no way to solve the same problem. If so, perhaps what's really missing is a generic CSS syntax for pseudo-element multi matching? |
(Alternately, maybe what's needed there is parts with states. Then, in that example, |
Right. ^_^ There's the issue of forcing the author to make a fairly arbitrary choice between a particular name being "tagname-like" or "class-like" for a given part, which I don't think is usually a very meaningful distinction. There's also the issue of syntax - if "states" here are expressed as custom pseudo-classes, then we run into the syntax collision I outline in #4900 (comment), where two non-cooperating parties (the outer component author and the inner component author) are both trying to expose custom pseudo-classes on the same element. And if they're not expressed as custom pseudo-classes, then how? Either we scope the names into a pseudo-class function, so you have I might be okay with that last option, btw! It still has the "tagname vs class" artificial distinction that I don't like, but I could live with it if others are happier. (Alternately, we just use
Yes, it probably would be useful. And luckily, if we go with option 3 from my original post (author-defined part names must start with (...I guess that means |
This is actually very close to what I've been thinking lately. I'm not happy with the conflation of tag-like and class-like behavior in part names. I strongly feel that shadow parts should really just be custom pseudo-elements and have a single tag-like name, just like built-in pseudo elements do. I want to expose built-in behavior not create something special and different. I do see the value in the additional class-like behavior, and my current thinking is that could be exposed as classes of the pseudo-element to selectors. There's no way for anything else to assign classes to pseudo-elements so why not? Note that actual classes assigned to a part should not break the encapsulation of the custom element and be visible outside it. e.g.
(I don't care what the 'part-class' attribute is really called) A weekend holiday would be selected via: The 'internal-class' can be used by stylesheets attached to the <custom-calendar> but is not exposed to anything that contains a <custom-calendar>. If the <custom-day> itself exposes custom state or additional parts, those would simply be exposed and could be selected like: I'd be ok with parts having multiple part names, but those would be an 'or', e.g. you could select <custom-day part='foo bar'> as either Alternatively we could define the first part name to be the tag-like name and others to be class-like, but I think that's problematic when you start manipulating them, e.g. toggle the first one off and you just redefined the tag-like name. If the class syntax doesn't work for some reason, my fallback would be exposing the part's class-like names as param of the pseudo-element, e.g. Either approach gets rid of |
Note that that's the motivation behind my proposal in the OP; preserve the more useful behavior of class-like names, but also preserve the looks-like-builtin nature of pseudo-element names.
Recall that we (intentionally) don't allow ::part() nesting - if you want to expose a part of your part, you "forward" their part into your part-map. This hides details of the internal structure of the element. (It also avoids the oddity of a sub-component exposing nested parts, but |
But by using Again, I agree that class-like behavior on shadow parts is very useful, but we can expose that as classes of pseudo-elements rather than inventing something new and different. Another thing that occurred to me, I think it was Amelia who suggested that since custom state now uses a '--' prefix, we can potentially allow the API to set a safe-listed set of built-in pseudo-class names in the future, e.g. 'active', 'checked', etc. The same applies to custom pseudo-elements, by putting all the custom pseudo-elements behind a '--' prefix, in principle we can allow certain built-in pseudo-elements as well, e.g. allow a custom element to have a 'placeholder' or 'marker' shodow part that can be selected via |
If you think of pseudo-elements and pseudo-classes as analogies for elements and classes, it's the case that an element has only one tag name and multiple class names. So perhaps it is right that any given element should only have up to one part name, and optionally multiple state names. Going back to the calendar example, you'd probably have to do it as Taking that direction, there'd be no need for |
To be clear, under my current proposal, your example would be: And it would be selected as: I believe this satisfies all the use cases, brings shadow part selector syntax into alignment with custom pseudo-classes, and allows the extension I mentioned above of allowing custom elements to potentially create built-in pseudo elements as well. And, like custom state is now really 'custom pseudo-classes', it turns shadow parts into 'custom pseudo-elements'. |
No, in the bit you quoted for this response I was referring to my OP (original post), where
I think it's important not to blindly hew too close to DOM structures - examine why we have them and what value they bring. Tagnames exist because elements have to have a particular identity and behavior; you can't mix-and-match them. It's a required bit of creating an element, so the browser knows whether the element should be link-like or button-like or list-like, etc. But we're not doing that; we're providing ways for component users to select elements. And from Selector's POV, there is no meaningful difference between a tagname and a class name - they're both just idents associated with an element. Because DOM only allows an element to have one tagname (for the aforementioned reason), tagname selectors are given a syntax that doesn't allow you to specify multiple of them together in a compound selector. But other than that, identical. So Selectors doesn't provide any reason to create a tagname-like semantic. Is there any value besides that to specially choosing one of the part names as the "tagname" equivalent and making the rest "class name" equivalents? I don't think there is; it seems just as reasonable to select a And note that this corresponds well to the existing selectors authors write for their own component hierarchies, which are solely class-based. They don't privilege one name with a special syntax.
I'll refer you again to #4900 (comment) - letting a component give custom pseudo-classes to its parts is going to be very fraught, because the sub-component itself (written by a non-cooperating party) is also trying to give custom pseudo-classes to itself. Unless you want to navigate the briar patch I outline in the linked comment, selecting based on states coming from the outer component and states from the inner component must use a different syntax. So, since we're already on the hook for two syntaxes (part names from the outside, custom pseudo-classes from the inside), I think we need a really good justification for increasing that to three (two slightly different notions of "part name"). |
It's also important to leverage the existing architecture of the platform and not invent new things when what we have already works.
And pseudo-elements already have a particular identity and behavior, giving a pseudo-element a name is part of creating a new pseudo-element.
There are differences in the selectors, specificity. There's also a semantic component in the tag name and pseudo-element name.
The value is matching the existing platform rather than inventing something new for no good reason. We already have tag names, pseudo-element names, and classes. Why invent a new 'part name' which is sort of like a class but isn't a class, and isn't selected like a class?
Because a 'monday' is a kind of 'day' as is a 'weekend' day, or a 'holiday', there is semantic value here. Allow authors to express that. Pseudo-elements are also structural, just not structure that's exposed in the DOM, but structure nonetheless that's exposed to selectors. Structural nodes have type names, they may additionally have classes. Class names are not structural, they're descriptive. Given your calendar widget example, imagine week parts as well as day parts. You could select the third day of the second week as: The other aspect here is cow-path paving. Custom elements are supposed to allow web authors to develop new elements that act as much like native elements as possible. Shadow parts as currently proposed only allow custom element authors make a specific kind of pseudo-element (a 'part', which is as semantically valuable as a div) and then use a new and unique way of selecting them. Some of these inventions are intended to be brought into the platform. If your example calendar widget becomes part of HTML, it would have a Yes, exposing classes on top of pseudo-elements is something new (tho it's not a new concept, like part names is). There's also no reason that native pseudo-elements can't do the same. I expect once we start thinking in those terms we'll come up with several uses, e.g.
You're already invented a third thing with the existing part name proposal, I think the disconnect we have is that you're thinking of this feature as "just a mechanism to select parts of a custom element, who cares how", and I'm thinking of it as a way of creating custom pseudo-elements (and explaining the built-in ones). |
Again, nesting parts isn't possible right now and we probably don't want to make it possible. It exposes internal structure that we probably want to hide.
::part() isn't a third thing for the authors of the component to decide on (or for the users of the component to have to remember which values correspond to it). It's a syntax hack over the pseudo-element name.
As far as I can tell, my proposal is perfectly compatible with this idea? What makes you think it isn't? |
While it doesn't have to be a V1 feature, there's no reason to not allow custom element authors to expose whatever structure they want to, so long as it's a choice, not a side-effect of implementation. As I showed in my example, that structure can be useful. I'm not trying to expand this issue into nested parts, but it's worth seeing how these selectors play in a world where nested parts exist.
But part names are a completely new thing. I expect the majority of custom pseudo-element will only have a single name, so no additional cognitive load on authors. If they're designing something with multiple names, it may be a good idea to make them think about why and what those names mean, they are creating an implementation contract after all. Also, custom element authors already understand the concept of minting element names, and using tags vs attributes (e.g. should it be <foo> and <bar> or <thing type="foo"> vs <thing type="bar">, same concept). I expect classes won't be a surprise to them either. Tag names and classes also aren't anything new for the consumers of those elements. Whereas part names that are sort of tag-like and sort-of class-like are a new concept are are more likely to cause confusion than clarity, especially if they have a new syntax to boot. I know the idea was surprising to me, and I've been doing this for a while... All existing pseudo-elements are clearly explainable as having a single tag-like name. Class-like part names don't directly map onto anything else in the platform. And I'm glad we agree that
I suppose it is, I admit I'm having trouble keeping all the proposals straight. In my head, your proposal is still conflating part names with pseudo-element type and a class-like thing, so while doable, it feels hacky to me. In mine the pseudo-element type is explicit and only used for that. In my experience, when we mix concepts we tend to get bitten somewhere down the line. |
That would require substantial changes to the selector grammar, and at that point, it might be easier to convert Footnotes |
Since |
While several of the larger changes are likely frozen out of possibility now, the OP's suggestion of allowing |
But is that worth doing if you're not also solving |
tldr: We should allow part names to also be selected as full custom pseudo-elements names, a la
my-component::--heading
in addition tomy-component::part(heading)
.In the Web Components virtual f2f today, we discussed custom pseudo-classes. In the thread about
:state(foo)
vs:--foo
syntax, Maciej brought up the reasonable point that switching away from:state()
to:--foo
, but leaving parts as::part()
, is inconsistent.I explained the reason behind maintaining
::part()
: due to CSS's bad syntax choices in the past, pseudo-element syntax implicitly contains a combinator, moving the subject of the selector into the element's "pseudo children"; as a result, you can't stack pseudo-elements to apply multiple conditions to a single pseudo-element. However, we want to allow that for parts - we want a calendar widget to be able to expose day elements for styling with something likepart="day weekend saturday week-1 us-holiday"
, and let stylesheets select on a combination of those qualities, like::part(day weekend us-holiday)
However, Maciej's point is still valid! Also, people have wanted ::--foo syntax, to mirror normal pseudo-elements, and things like UA pseudo-elements (
::-webkit-scrollbar-thumb
, for example).Jan's idea was to unify these - allow parts to be selected either with
::part(foo)
or with::--foo
; if you only need to select on one property, you can use the more direct form, but::part()
remains if you have more complex selecting needs. This received general approval in the call, and I think it's a good idea personally. General thoughts?I think there's one important detail to decide, which is how to handle existing part='' content. In general, CSSWG has made the decision that dashed-idents should be used consistently thruout APIs: you reference the
--foo
custom property withvar(--foo)
, notvar(foo)
, etc. In other words, the--
is part of the name, not a context-specific indicator of custom-ness.Currently, tho,
part=''
allows any ident, not just dashed-idents. How should we deal with this? I see a few options:(I don't like this, it's bad, but for completeness:)
part
is unchanged. If you definepart="foo"
, you can reference it as::part(foo)
or::--foo
. In other words, the -- is a context-specific indicator of custom-ness, but is not used when defining it, or referencing in::part()
. (Note: if you saidpart="--foo", it would be referenceable as
::part(--foo)or
::----foo`.)This is backwards-compatible with existing code, but breaks consistency with how dashed-ident features are used everywhere else in CSS, both currently and planned. The component author doesn't make a choice on how to expose things, but the component user is presented with two fairly different names to refer to.
part
is unchanged. If you define a part as a dashed-ident, it can be referred to as a top-level pseudo-element. That is, givenpart="foo --bar"
, you can use::part(foo)
,::part(--bar)
, or::--bar
validly.::--foo
will not match, nor will::foo
.This is backwards-compatible with existing code, and consistent with CSS design principles for dashed-idents. However, it imposes a choice on component authors of whether they want to expose parts as top-level pseudos or only within
::part()
. (There might be a good reason to make this distinction? Unsure, but I suspect not.)We restrict
part
to only allow dashed-idents.part="foo --bar"
only defines one part name,--bar
, which can be referenced as either::part(--bar)
or::--bar
. (The "foo" is ignored, as part of our forward-compatible error handling that silently drops things that aren't understood.)This is not backwards-compatible; virtually all existing content would become invalid and stop defining parts. However, it means there's no choice on the component author's side, and the component user has a closer relationship in the two syntaxes.
I slightly lean toward 3, but I'm unsure how much back-compat pain we want to take on. I'm fine with 2 if it's preferred by others. I'll fight against 1, it's terrible.
The text was updated successfully, but these errors were encountered: