Skip to content
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] What's the purpose of multiple idents in ::part()? #4412

Open
plinss opened this issue Oct 11, 2019 · 14 comments
Open

[css-shadow-parts] What's the purpose of multiple idents in ::part()? #4412

plinss opened this issue Oct 11, 2019 · 14 comments

Comments

@plinss
Copy link
Member

plinss commented Oct 11, 2019

I get multiple elements having the same part name, as well as multiple part names on a single element. But what's the point of being able to select parts by multiple names?

The example in the spec: ::part(tab active) would be better addressed by ::part(tab):state(active) rather than conflating state and structure. Are there any non-state use cases?

FWIW, the spec should also be clear about the difference between state and structure and recommend ::part() for structure and ::state() for state (and not give a state example for a structural feature). Pseudo-classes and pseudo-elements are different for a reason, let's not break that.

@emilio
Copy link
Collaborator

emilio commented Oct 11, 2019

If we're going to retract the multiple-parts thing it'd be great to sort it out sooner rather than later, as WebKit's shadow parts implementation does support it, and I have a patch to support it on Gecko (https://bugzilla.mozilla.org/show_bug.cgi?id=1548718).

cc @tabatkins @rniwa

@tabatkins
Copy link
Member

This was discussed pretty heavily at TPAC. ^_^

Yes, the "tab active" example is better as a part/state combo, now that we're doing states. That's just an obvious first example I came up with, tho.

There are a million reasons why something would have two classes, neither of which are really "states", that you'd want to select on the intersection of. An example that came up during TPAC is styling a calendar widget - each of the days is exposed with a part=day, and then individual days can have part=monday, part=weekend, part=holiday, etc. If you're styling holidays different from normal days, and weekends and weekdays a little differently from each other, then you probably want to style holiday weekends and weekdays a little differently too.

And to head an obvious interjection off at the pass: yes, you could encode these as states. Or you could encode all of them as part names. The semantic distinction between the categories is very fuzzy, but as long as have both, the obvious distinction is that parts are permanent and part of the identity of the element, while states are transitory and might change. Under this distinction, all of the names in my calendar example are definitely part names, not states.

So yeah, I agree that the spec examples and explanation need to be updated now that :state() exists.

@plinss
Copy link
Member Author

plinss commented Oct 11, 2019

In my mind the distinction between pseudo elements and pseudo classes has always been about structure. Pseudo elements are things that would otherwise be elements if they were exposed to DOM. Pseudo classes are either expressions of state or some kind of other 'class-like' distinction that isn't expressed as a regular class. A pseudo class isn't necessarily transitory (tons of examples of non transitory pseudo classes, and yeah, many of those are somewhat structural, but they're more descriptive of structure, not structural type).

So under that distinction, your holiday example would clearly fit into the nature of a pseudo class. And having multiple pseudo element names feels as weird to me as having multiple element names would be on a DOM node. Note that I'm not necessarily objecting to this feature, I'm questioning where the line should be drawn and if this feature makes sense given where the line falls.

I see ::part as a custom pseudo element, and :state as a custom pseudo class. Splitting pseudo classes into 'class-like' and 'state-like' could be interesting, but shouldn't necessarily push class-like things into the category of pseudo elements.

@tabatkins
Copy link
Member

tabatkins commented Oct 11, 2019

You're drawing a weird distinction here. The tag-name like part of ::part() is the "part" string; effectively, a shadow host has a bunch of "part" elements in its pseudo tree, alongside its "before"/"after"/etc children.

What we do in the parentheses is totally unrelated. We could pretend that it's like a tagname and apply the same rules (both part="" and ::part() only allow a single ident), and then move the "acts like a classname" part to a new :state() pseudo-class, but what's the point? Except for DOM's restriction on how many tagnames an element can have (one), there's literally no difference between a tagname and a classname; both are idents that categorize an element somehow. I don't know what's been gained by drawing such a distinction.

(insert Spongebob meme with Patrick moving things one spot to another)

[deleted some stuff about state which I misremembered about]

@tabatkins
Copy link
Member

Okay, re-found the :state() explainer https://github.com/w3c/webcomponents/blob/gh-pages/proposals/custom-states-and-state-pseudo-class.md

Note the further distinction between ::part() and :state() - ::part() is about classifying a sub-component that's inside of you, :state() is about classifying yourself (in a way that is guaranteed to not clash with whatever your parent component is doing to your attributes). They're playing in similar spaces, but doing quite different things, and should not be understood as competing in any way.

@plinss
Copy link
Member Author

plinss commented Oct 11, 2019

So you're considering ::part as a kind of pseudo element in itself, where I see the part itself as a custom pseudo element, e.g. ::part(partname) as morally equivalent to ::--partname (and I'm also somewhat in favor of that syntax, so long as we align :state(statename) as :--statename). I'm not sure the 'part kind of pseudo element' distinction adds value (happy to hear your thoughts tho).

Note: I'm still fine with the part attribute of a shadow node having multiple part names, I see that as valuable where a single node can play multiple roles.

The point in drawing the distinction between ::part and :state is the same point as the distinction between pseudo elements and pseudo classes. They behave differently, they have different roles/rules in selectors, and different specificity, etc. Again, I see ::part as a mechanism to explain pseudo elements and enable them for custom elements, not a new kind of pseudo element. The same goes for :state and pseudo classes.

Here's a concrete example, using your calendar widget, I have day nodes that can be weekends or weekdays, and also holidays or not. I want to color weekends yellow, and holidays green:

::part(day):state(weekend) { color: yellow: }
::part(day):state(holiday) { color: green; }

but holidays on weekends I want blue:

::part(day):state(weekend):state(holiday) { color: blue; }

That's simple, explainable, fits with regular CSS specificity rules, and plays nice with regular classes.

Using just parts that'd be:

::part(day weekend) { color: yellow; }
::part(day holiday) { color: green; }
::part(day holiday weekend) { color: blue; }

which I suppose can work (depending on the outcome of #3995) but has a very different specificity than an element with two classes and is something new you have to explain.

Using :state also enables things like:

::part(day):not(:state(weekend)) { color: white; }

where with just part names:

::part(day):not(::part(weekend)) { color: white; }

doesn't work because :not() can't contain pseudo elements.

@tabatkins
Copy link
Member

I want to stress again tho that :state() is managed internally by the component, while part is managed externally by the shadow tree holding an element. Using :state() for holidary/weekend/etc means that the ::part(day) element itself (a) has to be a custom element, and (b) has to manage the fact that it's a holidary or weekend or something on its own. That's more complexity than I think we want to require for anything that wants to put a sub-element in more than one category.

doesn't work because :not() can't contain pseudo elements.

:matches() allows pseudos; the only reason :not() doesn't is because with the previously-existing set of pseudos, there's absolutely nothing it could do. (That's no longer true now that we allow :hover and such on pseudo-elements, and it was never true for UA-prefixed pseudo-elements that sometimes had custom pseudo-classes for themselves, so we need to fix this.)

There's nothing wrong, semantically, with ::part(day):not(::part(weekend)); it works the same as :matches(::part(day), ::part(weekend)).

@plinss
Copy link
Member Author

plinss commented Oct 11, 2019

Ok, valid point about :state() and internal vs external. But to me that just means that in your calendar widget example, the weekend/holiday status should be a class, rather than a pseudo class (or a part).

I get your argument about it just being over here rather than there, but there's a distinction between classnames and tagnames, just like there's a distinction between pseudo elements and pseudo classes. And I think we're doing authors a disservice by conflating them.

The :not() point wasn't meant as a 'gotcha', classes and pseudo classes have a different specificity and different behaviors than tagnames and pseudo elements. You didn't address the differences in specificity between the examples.

At the end of the day, yes, we can mix in the notions of class into parts, but the question here is: should we? I really want ::part to just be a custom pseudo element, that acts like a built in pseudo element, and explains the built in pseudo elements, not something new with its own magic and its own rules. Especially when the 'new magic' is something authors already have via other existing mechanisms.

@tabatkins
Copy link
Member

But to me that just means that in your calendar widget example, the weekend/holiday status should be a class, rather than a pseudo class (or a part).

I mean, yes, but we purposely don't expose the classes of a shadow element to the light dom, so the component can freely use classes internally for its own purposes without worrying about the component-user relying on them.

And that's why I designed part names to have class semantics in the first place, rather than tagname semantics. ^_^

You didn't address the differences in specificity between the examples.

Correct; I don't feel it's a very important difference. This is generally always going to be "CSS in the small", where specificity doesn't matter as much and you can lean a lot harder on "just put them in the order you want". Especially now that we have the :where() escape hatch when necessary.

At the end of the day, yes, we can mix in the notions of class into parts, but the question here is: should we? I really want ::part to just be a custom pseudo element, that acts like a built in pseudo element, and explains the built in pseudo elements, not something new with its own magic and its own rules. Especially when the 'new magic' is something authors already have via other existing mechanisms.

If we don't, we'll still need to expose a class-like semantic somehow. CSS made an early awkward choice for pseudo-element syntax that we're stuck with now; I'm just working within those bounds to expose the semantics we need. The tagname limitation isn't doing any useful work here, imo; the class semantic is actually something we need.

@plinss
Copy link
Member Author

plinss commented Oct 12, 2019

Ok, I think we understand each other here (and as an aside, thanks, this has been a helpful and productive conversation for me).

I agree that we need to expose class-like semantics of parts, I just think we should also be exposing a tag-like semantic.

I also agree that using regular classes within a shadow tree should stay within the shadow tree, and that we need to improve handling of pseudo element selectors in general (which has come up multiple times in the past).

The current model is the moral equivalent of using divs for markup and differentiating them purely by classes. It works (and many people do it), but it breaks the semantic model of HTML and has second-order costs.

I'd like to see the part model extended so that a part exposes a tag-like label, that carries structural and semantic meaning, which is exposed via a pseudo element, and additionally can expose class-like information. There's value in the tag semantic, and I want to see that preserved for parts. Among other things, I'm thinking about how screen readers would process these (presuming no additional accessibility info was exposed by the custom element).

(I also think the specificity difference is valuable, even tho I agree it's not likely to be a considerable factor for most. It's more of a 'why throw out the cascade here?' issue. Just because we have an escape hatch, there's no reason to not have a regular door.)

I don't much care how this is expressed, two possibilities off the top of my head would be for the first part name to be the 'tag', and any additional names would be 'classes' (tho that feels awkward and then precludes the part having multiple part names, which I think is valuable in itself, allowing a single part to play multiple roles, but I can be convinced otherwise), or a separate attribute to declare part classes. Either way, we could expose the part classes to CSS as regular class selectors that only apply to the pseudo element.

Something like:

<calendar-widget>
  <day-element part="day" partclass="weekend holiday">1</day-element>
</calendar-widget>

calendar-widget::part(day).weekend.holiday { color: green; }

Yes, it adds a bit of complexity, but it gives the part semantic information in addition to class information (and clearly differentiates them) as well as makes custom parts fit in with the rest of the model more cleanly IMO.

Also, I think if the part itself is a custom element it should be able to expose it's own :state. It's not clear to me if that's been accounted for yet. (And possibly it's own ::parts, tho probably the containing custom element should be involved there, not sure how that'd work yet, but food for thought).

@tabatkins
Copy link
Member

The current model is the moral equivalent of using divs for markup and differentiating them purely by classes. It works (and many people do it), but it breaks the semantic model of HTML and has second-order costs.

Can you explain a little more why you think the tagname is valuable?

In HTML, tagname is worth something because there's a lot of other stuff, DOM and interactivity and a11y and semantics, that keys off of it automatically, and it would be confusing/impossible to apply multiple sets of those things (something can't be both a textarea and a video). Classes have none of that and are purely for author extensibility.

In parts, both "class-like" names and a "tagname-like" name are exactly identical in behavior: they're author-defined and meant for author-level extensibility, with zero UA-defined difference. The only inherent difference is that an element would have 0-Infinity "class-like" names, and exactly 1 "tagname-like" name.

What's the significant difference between an ident denoted as "class-like" and an ident denoted as "tagname-like" that justifies adding more syntax and concepts to the model, versus just sticking with a single concept and leaning on convention when necessary?

If it's purely a (custom to the page) semantic distinction, note that I've had several occasions in my webdev life when I've wanted to give an element multiple IDs, so I could address it differently in different contexts. The fact that DOM happens to impose a "0 or 1" semantic on IDs is a relic of the model; there's nothing semantically wrong with having multiple of them. As a result, I've moved something from being an ID to being a class just so I can give it multiple "names".

In other words, I think the "exactly 1" or even "0 or 1" name restriction ends up at best neutral and at worst harmful to usability, when it's not backed by actual behavior requiring such a distinction.

@plinss
Copy link
Member Author

plinss commented Oct 16, 2019

Can you explain a little more why you think the tagname is valuable?

My concern is more architectural than functional. (Tho there are functional differences in the cascade order.)

To be clear (to ensure we're on the same page and for anyone catching up with the thread), the current model allows custom elements to surface internal stylable components (shadow parts) as a single new type of pseudo-element, a ::part() that has 1 to n labels. What I'd rather see is the ability for custom elements to surface internal components as custom pseudo-elements themselves, where the custom element declares the pseudo-element type. I agree that a pseudo-element type by itself doesn't expose sufficient information and think that custom pseudo-elements should also be able to expose class and custom state (:state()).

Exposing part names in a ::part() type pseudo-element is something new. The part names behave similarly to classes for selector purposes, but they're not classes. They don't expose the part names in APIs like classes. They also don't fit into the cascade like classes.

By exposing a single pseudo-element type, and an optional set of classes, the shadow parts behave like regular pseudo-elements, and don't add anything new to the platform (except the fact that they exist). They fit into the selector model in a predictable and standard way. They'd also fit into any future Houdini APIs that expose pseudo-elements as their own type of pseudo-element without needing any additional API surface to expose the 'part name' concept.

One of the fundamental purposes of custom elements was to give authors the ability to do things that the platform does natively. I'd like to see shadow parts be a true custom pseudo-element, that look, act, and feel like regular pseudo-elements, rather than a new kind of pseudo-element that has different behavior and API surface.

We also expect custom-element authors to 'pave the cow paths' and create new elements that can be added to the platform later as they become popular and show user need. By making shadow parts a different kind of pseudo-element, with different behavior, we make it harder to add them to the platform with equivalent semantics and behavior. I also expect common custom pseudo-element types to evolve where semantic meaning can be derived from their type, where their classes (or part names) would remain purely something with meaning only to the page author.

Frankly, I'd also like to see custom elements expose shadow parts as standard pseudo elements as well. What can't a custom input element expose a ::placeholder?

If it's purely a (custom to the page) semantic distinction, note that I've had several occasions in my webdev life when I've wanted to give an element multiple IDs, so I could address it differently in different contexts. The fact that DOM happens to impose a "0 or 1" semantic on IDs is a relic of the model; there's nothing semantically wrong with having multiple of them. As a result, I've moved something from being an ID to being a class just so I can give it multiple "names".

Note that I'm not precluding the shadow part from actually having multiple types, e.g. a part could be a ::part(placeholder) and a ::part(label), similar to the way grid lines can have multiple names. The user of the component doesn't need to know that the two roles are played by the same part in one element, where they may be played by different parts in another (or another state of the same element).

@astearns astearns removed the Agenda+ label Oct 16, 2019
@emilio
Copy link
Collaborator

emilio commented Oct 17, 2019

They don't expose the part names in APIs like classes.

They do, right? https://drafts.csswg.org/css-shadow-parts/#idl

By exposing a single pseudo-element type, and an optional set of classes, the shadow parts behave like regular pseudo-elements, and don't add anything new to the platform (except the fact that they exist). They fit into the selector model in a predictable and standard way. They'd also fit into any future Houdini APIs that expose pseudo-elements as their own type of pseudo-element without needing any additional API surface to expose the 'part name' concept.

Well, that ship kinda sailed as soon as you can match multiple elements with the same part name... ::slotted also has a similar issue. So there will be zero-to-one "built-in" pseudo-elements (::before / ::after / etc), but zero-to-n ::slotted and ::part pseudo-elements, regardless of there being multiple parts in the ::part pseudo.

@MrHBS
Copy link
Contributor

MrHBS commented Sep 13, 2024

I guess it is too late to deprecate multi ident parts now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants