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

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

Open
tabatkins opened this issue Feb 13, 2024 · 20 comments
Open

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

tabatkins opened this issue Feb 13, 2024 · 20 comments

Comments

@tabatkins
Copy link
Member

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:

  • Consistent with the collection of existing pseudo-elements exposed on various "special" elements, like input::file-selector-button

Arguments for ::part(content)

  • Clearer about what other things are allowed, like ::part(content):hover works by default
  • Integrates nicely with other shadow-parts functionality, like the ability to export it as one of the parts of a wrapping component. ¹

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
Copy link
Member

nt1m commented Feb 17, 2024

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
Copy link
Member

nt1m commented Feb 17, 2024

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
Copy link
Collaborator

emilio commented Feb 17, 2024

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
Copy link
Collaborator

emilio commented Feb 17, 2024

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
Copy link
Member

dbaron commented Feb 21, 2024

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
Copy link

rniwa commented Mar 8, 2024

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
Copy link
Member

dbaron commented Mar 8, 2024

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
Copy link
Member

annevk commented Mar 8, 2024

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
Copy link
Contributor

::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
Copy link

kbrilla commented Mar 8, 2024

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

@emilio
Copy link
Collaborator

emilio commented Mar 8, 2024

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
Copy link
Member Author

@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
Copy link
Member

dbaron commented Mar 13, 2024

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

@emilio
Copy link
Collaborator

emilio commented Mar 14, 2024

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

  • API consistency (::pseudo is more consistent with existing built-ins). But that might be a one-time thing.
  • Style inheritance (::pseudo has different inheritance behavior than ::part(), which is relevant if we want people to rely on the tree shape).
  • Flexibility (as things stand right now ::part() is more flexible than ::pseudo, specially combined with existing shadow trees).
  • Property restrictions (some existing ::pseudos like placeholder have specific restrictions). My understanding is that we don't want this in the future, but it's hard (not impossible I guess) to bolt this into parts.

@annevk
Copy link
Member

annevk commented Mar 14, 2024

I think we should also discuss:

  • Feature testing.
  • Consistency with pseudo-classes (for those :state() is the free-for-all, which is also pretty explicitly different from ordinary pseudo-classes; however, ordinary pseudo-classes can also be supported by custom elements sometimes, e.g., when they're form-associated).

@astearns
Copy link
Member

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
Copy link
Member

dbaron commented Mar 14, 2024

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:

  • There was interest in building a mechanism for web components to be able to export (as parts) the part-like pseudo-elements on elements in their shadow trees, just like web-components can re-export parts of elements in their shadow trees. This would need to be proposed and reviewed.
  • We need to investigate whether the part-like pseudo-elements would have the same restrictions as parts. For example, right now it's apparently possible to write element::part(part-name)::pseudo-element selectors but it is not allowed to write element::part(part-name)::part(nested-part-name). If we allowed the first syntax for part-like pseudo-elements we'd need to define the relative cascading behavior between that syntax and styling the same thing via a re-exported part (see previous point). We'd also need to consider the compatibility implications of forbidding that syntax on our ability to convert any existing pseudo-elements to be part-like.

A few other notes from the discussion were that:

  • there was a little bit of discussion of exposing a separate part syntax such as ::ua-part() for builtin parts
  • we had some discussion of allowing more than one syntax (that is, both a pseudo-element syntax and a part syntax)

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

@tabatkins
Copy link
Member Author

Raised the "part-like pseudo-element" issue in #10083

@astearns
Copy link
Member

Slightly more detail in meeting notes here: whatwg/html#10200 (comment)

dbaron added a commit to dbaron/html that referenced this issue Apr 9, 2024
This makes a number of related changes to specify the rendering of the
<details> element:

* It specifies the structure of the user agent shadow tree.  This
  appears largely interoperable between implementations with the
  exception of the style or link element for the default summary styles:
  Gecko uses a link element as the first child, Chromium uses a style
  element as the last child, and WebKit does not use a style element
  (see below).  This specifies a style or link as the first child.
* It specifies the existence of the default summary and the presence of
  UA dependent text inside of it.  This is present in all of Chromium,
  Gecko, and WebKit.
* It specifies the styles needed for the default summary.  These match
  Chromium and Gecko.  (These are not present in WebKit because WebKit's
  mechanism for styling the marker does not match the existing spec.)
* It removes the restriction that <details> is a block and cannot be
  changed.  This is prototyped in Chromium and Gecko.  This fixes whatwg#9830.
* It specifies that the summary element is display: block by default.
  This matches all of Chromium, Gecko, and WebKit.
* It specifies which element matches the ::details-content
  pseudo-element.  (TODO: This needs to be specified in CSS)  This is
  prototyped in Chromium.

See w3c/csswg-drafts#9879 and
w3c/csswg-drafts#9951 for more background.
@LeaVerou
Copy link
Member

LeaVerou commented May 8, 2024

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.

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

10 participants