w3c / csswg-drafts

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

`::details-content` vs `details::part(content)` #9951

Open tabatkins opened 4 months ago

tabatkins commented 4 months ago

At the F2F, there was some discussion about how to expose the <slot> in the <details> element's shadow DOM that wraps the details' content.

There are two main options: a new pseudo-element (provisionally called ::details-content), or a shadow part (provisionally called ::part(content)).

Arguments for ::details-content:

Arguments for ::part(content)

During the meeting, we seemed to recall that this topic had been previously brought up, and both WebKit engineers and the HTML spec editors disliked it. Our recollection of their reasoning was:

  1. consistency with existing pseudo-elements on special elements (aka the "arguments for" I already listed)
  2. part names are author-controlled, and the language shouldn't impinge on it

I think that (1) is reasonable, tho that's historical precedent from before ::part() even existed. It might be reasonable to change that. For example, we could make all those element-specific pseudo-elements also exposed as ::part(), as that would give you the "export this as one of the wrapping component's parts" ability that I allude to above.

But I will push back on (2) very strongly. It is important to not impinge on author-controlled namespaces; you don't want to accidentally clash with existing author-defined names, and it's not great to have certain special names be illegal for authors to use.

But neither of those apply here. The owner of a given element's ::part() namespace is the owner of that component's shadow DOM. I designed this very carefully to make sure that's 100% true - the owner of a component's shadow can choose to import the names from a sub-component for themselves, but it's always an extremely explicit choice. You'll never get surprised by names showing up in your namespace you don't expect.

So, here, the UA is the owner of the details shadow. If it wants to expose a part name, nothing any author can do will ever interact negatively with that.


¹: That is, if you have a custom component <my-widget> that contains a <details> element in its shadow, you could use <details exportparts=content>, and then my-widget::part(content) works, targeting the details-content box. This is impossible to do with a pseudo-element, at least right now.

Tho we could attack this from the other direction - rather than turn all existing element-specific pseudo-elements into ::part()s so they can be used in exportparts, we could just allow pseudo-elements to be used in exportparts, like <details exportparts="::details-content : content">.

/cc @emilio

nt1m commented 4 months ago

I think the only reason ::part(name) has been designed as it is to avoid author names impeding with future built-ins. The syntax could have been ::--name fwiw. In fact, WebKit internally names some existing pseudo-elements like ::placeholder or ::file-selector-button as "user-agent parts".

If the syntax for ::part(name) was instead ::--name, this issue to me is conceptually equivalent to asking to use ::--details-content instead of ::details-content... I don't think it really makes a lot of sense.

nt1m commented 4 months ago

I like that built-in pseudo-elements have a distinct syntax from non-built-in ones do, it makes it easy to distinguish what the browser provides by default from what the browser doesn't.

emilio commented 4 months ago

I think the only reason ::part(name) has been designed as it is to avoid author names impeding with future built-ins. The syntax could have been ::--name fwiw.

Citation needed?:::part() and the part attribute allows multiple identifiers, not sure how that could be achieved with a syntax like that. Part is strictly more powerful than regular pseudo elements.

In fact, WebKit internally names some existing pseudo-elements like ::placeholder or ::file-selector-button as "user-agent parts".

Sure, but for each we need to define what pseudo classes if any are valid to the right of the pseudo. Also what properties apply to it. All these are answered for for part.

I don't see part as a built in pseudo element, it can expose multiple elements and elements can have multiple parts, it's a more powerful feature. The fact that WebKit uses that mechanism to implement some pseudos is kind of an implementation detail.

emilio commented 4 months ago

We could just allow pseudo-elements to be used in exportparts, like <details exportparts="::details-content : content">.

I think that'd be super weird, to target something sometimes as part and sometimes as pseudo-element. Also we probably don't want to do this for all pseudo elements, it's unclear what exposing a first-line should do, for example? I think that's not great alternative, and if we go for a pseudo element we should just tell people to export the whole details element for such use case (which might not be what they want, necessarily, but would allow you to do ::part(details)::details-content the same way you can do ::part(input)::placeholder nowadays.

dbaron commented 4 months ago

Previous discussions on this topic:

Note that a big chunk of the Open UI discussion was the week after the Cupertino F2F discussion, and the combination of those two made me give up on trying to push for ::part() at the time, even though I would have preferred it.

rniwa commented 4 months ago

I don't think we should be using ::part syntax for details. The whole point of ::part syntax was to namespace shadow DOM / custom elements so that they won't conflict with builtin pseudo elements.

I'm immensely annoyed that this topic is getting re-litigated again.

dbaron commented 4 months ago

There are many things we could say about what an individual participant in a discussion thought the motivations for something were at the time. For example, I could point out the post I over wrote 21 years ago describing one of the core motivations for wanting XBL (early in a decade-long standards journey that eventually led to web components) as being able to describe builtin controls and allow them to be styled. But I don't think either of those opinions represent clearly documented consensus, and even if they did, new evidence can lead reopening of issues where there was previous consensus.

annevk commented 4 months ago

I consider ::details-content the only viable option.

Rationale:

  1. ::part() is a web developer namespace. This would be akin to standardizing data-* attributes. While it is true we are not taking away names web developers can use, it is still utterly confusing.
  2. It creates an inconsistent API with ::placeholder, ::marker, ::file-selector-button, ::thumb/::slider-thumb, etc.
  3. ::part() is very much tied to open/closed shadow trees. For built-in elements the way they work should remain up to implementations, even if we decide to define their subtree in terms of internal shadow trees (not open/closed) in the specification. Using ::part() however deviates from that as that's specific syntax tied to open/closed shadow trees. I'm very much opposed to start exposing implementation details.
Loirooriol commented 4 months ago

::part() is a web developer namespace

It's a namespace for the creator of the shadow, which can be the author or the UA. It's different to data-* attributes or CSS custom properties, where the author and the UA share a single namespace.

For built-in elements the way they work should remain up to implementations

But the spec mandates a shadow tree, and this is already exposed to authors. E.g. if they use details::before, it appears before the summary, not at the beginning of the details content.

kbrilla commented 4 months ago

I think discussion :part() vs :custom-pseudo was held many times in recent year always in favor of custom pseudo.

emilio commented 4 months ago
1. While it is true we are not taking away names web developers can use, it is still utterly confusing.

I'm not sure I agree, it is confusing the first time because it's new, but long term it's IMO less confusing, specially for developers already familiar with parts.

2. It create an inconsistent API with `::placeholder`, `::marker`, `::file-selector-button`, `::thumb`/`::slider-thumb`, etc.

Sure, though same thing, that's a one-time issue. That's also fixable (e.g., you can expose ::file-selector-button as an alias of ::part(selector-button) and so on, conceptually).

3. `::part()` is very much tied to open/closed shadow trees. For built-in elements the way they work should remain up to implementations, even if we decide to define their subtree in terms of internal shadow trees (not open/closed) in the specification. Using `::part()` however deviates from that as that's specific syntax tied to open/closed shadow trees. I'm very much opposed to start exposing implementation details.

I'm not sure I get this argument. We already define <details> in terms of a shadow tree. Can a user agent implement using something else? Lot of effort, but sure. But you could also make ::part work in that (hypothetical) set-up.


An aside / another topic worth looking into, is that there's another difference between ::part() and pseudos that I think might be worth discussing, which is which element do they inherit from.

Pseudo-elements always inherit from its originating element, while ::part() uses the usual flat tree rules. For ::details-content in particular it might not be an issue, because those would end up being the same (the <details> element), but for newer form controls or things like switch, where you may want to define e.g. the thumb as a child of the track, it can cause confusing behavior:

input { color: red }
input::slider-track { color: green }
input::slider-thumb { background-color: currentColor; } /* red! */

Alternatively, we might want to have a weird "part-like pseudo-element" that behaves like this... But that's a new CSS concept.


I think that if we're going to define new controls in terms of shadow trees, using ::part() would be the right thing to use to expose the inner elements. It's more powerful, way less special.

The big drawback that I see with ::part() is feature-detection / not being able to use @supports selector(::details-content) or so.

tabatkins commented 4 months ago

@nt1m

I think the only reason ::part(name) has been designed as it is to avoid author names impeding with future built-ins.

No, that was one reason. The other reason was to allow the class-like semantics, where an element can be exposed and/or targeted via multiple part names. (As the co-author of the spec, I can speak definitively on this.)

@rniwa

The whole point of ::part syntax was to namespace shadow DOM / custom elements so that they won't conflict with builtin pseudo elements.

It was to namespace, yes, but the conclusion you're subsequently drawing from that (that the UA should never be allowed to use ::part()) is incorrect. The ::part() namespace of a component is owned by the owner of the component's shadow DOM. Arbitrary authors cannot interfere with a given component's names (unlike many other namespaces we explicitly carved out for authors, like custom properties). So, since the details shadow is owned by the UA, the ::part() namespace for it is owned by the UA as well. It is 100% safe for the UA to expose parts.

I'm immensely annoyed that this topic is getting re-litigated again.

This isn't particularly appropriate to express in this venue.

@annevk

::part() is a web developer namespace. This would be akin to standardizing data-* attributes. While it is true we are not taking away names web developers can use, it is still utterly confusing.

I sympathize with "confusing", but this is still a meaningfully distinct situation from data-* attributes, or most other author namespaces we've carved out.

It create an inconsistent API with ::placeholder, ::marker, ::file-selector-button, ::thumb/::slider-thumb, etc.

This is the big argument against ::part(), I think.

::part() is very much tied to open/closed shadow trees. For built-in elements the way they work should remain up to implementations, even if we decide to define their subtree in terms of internal shadow trees (not open/closed) in the specification. Using ::part() however deviates from that as that's specific syntax tied to open/closed shadow trees. I'm very much opposed to start exposing implementation details.

I'm not sure what "open/closed" has relevance here, but yes, it does expose the fact that the element has a shadow tree (or at least masquerades as having one). This is already supported in the spec in any case - authors aren't allowed to install a shadow tree of their own on details.

@emilio

[allow exporting a pseudo-element as a part] I think that'd be super weird, to target something sometimes as part and sometimes as pseudo-element.

Well right now the choice is "target it as a pseudo-element" (if you have access to the originating element) or "you can't target it at all, sucks to be you" ^_^. I think allowing the second situation to become "target as a part" is an improvement; it allows people to, say, wrap a details in their image-spoiler component and still expose the contents bit. Or have a form component that wraps an <input type=file> and exposes its ::file-chooser-button or whatever.

Also we probably don't want to do this for all pseudo elements, it's unclear what exposing a first-line should do, for example?

Right, it would need to be tree-abiding pseudos only. Non-tree-abiding are virtually always an exception.

I think that's not great alternative, and if we go for a pseudo element we should just tell people to export the whole details element for such use case (which might not be what they want, necessarily, but would allow you to do ::part(details)::details-content the same way you can do ::part(input)::placeholder nowadays.

Yeah, it's possible, but awkward. ^_^ It forces the author to expose the fact that something is implemented as a details, rather than hand-rolled. This isn't strictly necessary to avoid, but we do try to avoid exposing such implementation details when we can.

dbaron commented 3 months ago

I think there is a plan to discuss this at the WHATNOT meeting tomorrow; see https://github.com/whatwg/html/issues/10200 . (This meeting is exactly 24 hours after the CSS call.)

emilio commented 3 months ago

Ok, so things that I want discussed after chatting with @smaug---- (putting here for reference and so I don't forget):

annevk commented 3 months ago

I think we should also discuss:

astearns commented 3 months ago

I will find out how to link to the minutes of the joint meeting we just had with WHATNOT, but the conclusion was:

Proposed resolution: We will use pseudo-elements for exposing styling of pieces of built-in controls. We would like these pseudo-elements to be as close to ::part() as possible (for example, in terms of what selectors work and how inheritance works) and intend to further explore the details of how to do this.

With this proposed resolution passing a meeting poll

dbaron commented 3 months ago

The joint discussion we had concluded with the following resolution:

RESOLUTION: We will use pseudo-elements for exposing styling of pieces of built-in controls. We would like these pseudo-elements to be as close to ::part() as possible (for example, in terms of what selectors work and how inheritance works) and intend to further explore the details of how to do this.

(somebody else may have a "more official" copy of this, not sure)

My intent with this resolution is that we'd be able to move towards defining a concept of "part-like pseudo-element" so that we can share definitions between the places in specs that define such pseudo-elements.

There are a few other things that we discussed that require some further exploration:

A few other notes from the discussion were that:

but the consensus of the discussion moved away from doing either of these.

tabatkins commented 3 months ago

Raised the "part-like pseudo-element" issue in https://github.com/w3c/csswg-drafts/issues/10083

astearns commented 3 months ago

Slightly more detail in meeting notes here: https://github.com/whatwg/html/issues/10200#issuecomment-1998676547

LeaVerou commented 2 months ago

Citation needed?:::part() and the part attribute allows multiple identifiers, not sure how that could be achieved with a syntax like that. Part is strictly more powerful than regular pseudo elements.

Every use case I’ve seen where multiple part names are used, only one is actually a true part name, and the others are states which are better addressed as custom pseudo-classes.

yisibl commented 1 month ago

Pseudo-elements always inherit from its originating element

I'm rather curious as to why there is no <details-content> element in HTML, and if it existed, wouldn't there no longer be a need for the ::details-content pseudo-element?

As a CSS author, API consistency is important, so I support ::details-content.

yisibl commented 2 weeks ago

@dbaron From my testing, Chrome(128.0.6544.0) doesn't currently support ::details-content:hover and ::details-content::before, do you have plans to implement them before shipping?

dbaron commented 2 weeks ago

@dbaron From my testing, Chrome(128.0.6544.0) doesn't currently support ::details-content:hover and ::details-content::before, do you have plans to implement them before shipping?

Yes.