Open fergald opened 6 years ago
Thanks @fergald
@travisleithead @SmauG @rniwa Could you help @fergald to know other browser's position about shadow parts spec?
I think the details of the spec don't matter here. We can fix the spec if something in detail is wrong. What matters here is that we can have a rough consensus that shadow parts spec's approach is worth to implement as an experiment, given that we abandoned /deep/ combinator.
This explainer, https://meowni.ca/posts/part-theme-explainer/, would be helpful to understand the motivation.
cc-ing: @tabatkins just in case.
Apple supports the general idea of ::part
and ::theme
but not necessarily specifics of the current proposal.
I guess we notified the wrong smaug. We are sorry for that. @smaug---- should be correct.
@emilio @dbaron
I feel like the problem it tries to solve is valid, but the general mechanism seems somewhat hairy.
In particular (maybe I misread how it works), doesn't all the part-forwarding
stuff mean that a ::part(..)
selector matches differently depending on which
scope the selector is declared? Or should ::part(confirm-label)
match even
if such selector is in the <x-button>
component, as long as it's nested in a
<x-panel>
component (using the spec examples)?
Anyway, personally I think Mozilla should take a similar position to Apple here, of supporting the general idea but not any specifics, but I opened https://github.com/mozilla/standards-positions/issues/59 to get a proper official position here.
Yes, the part forwarding stuff does mean exactly that as far as I can tell. The Chromium design doc is here
and discusses this a bit.
We don't want any nested shadow boundaries to be automatically crossed. If a component chooses to expose its internal as a part, it should have the opportunity to make that decision.
Having said that, we can provide a mechanism for a component to expose its inter-component's part as its own part.
In particular (maybe I misread how it works), doesn't all the part-forwarding stuff mean that a ::part(..) selector matches differently depending on which scope the selector is declared?
No, there's nothing scope-specific about it at all. Could you point to what gave you that impression, so I can fix it in the spec? The parts that an element exposes are just a part of the element's API, like its class list or its ID. Each shadow host just has a parts map, which maps part names to a list of elements (either from its own shadow tree, or a descendant tree if they're forwarding).
Anyway, personally I think Mozilla should take a similar position to Apple here, of supporting the general idea but not any specifics, but I opened mozilla/standards-positions#59 to get a proper official position here.
Directing this towards both Moz and Apple: this isn't a useful position to take. :/ We're currently proceeding in implementation on the assumption of support, based on previous statements from both of y'all's companies. "Like the idea, don't know about any details" means you don't like the spec and don't want us to implement, right? Because if we do implement, things'll gradually freeze, so you better make sure all the details you're unsure about are indeed something you'd like to see.
Directing this towards both Moz and Apple: this isn't a useful position to take. :/ We're currently proceeding in implementation on the assumption of support, based on previous statements from both of y'all's companies. "Like the idea, don't know about any details" means you don't like the spec and don't want us to implement, right? Because if we do implement, things'll gradually freeze, so you better make sure all the details you're unsure about are indeed something you'd like to see.
I want to insist that that was a personal position, not nothing official, fwiw. I don't have the cycles right now to be able to review the spec as carefully as it deserves, and thus I don't want to say I support specifics that may have implications that I haven't actually grasped fully.
No, there's nothing scope-specific about it at all. Could you point to what gave you that impression, so I can fix it in the spec? The parts that an element exposes are just a part of the element's API, like its class list or its ID. Each shadow host just has a parts map, which maps part names to a list of elements (either from its own shadow tree, or a descendant tree if they're forwarding).
That means that selectors in an descendant shadow tree that specify a part that that is forwarded from an outer shadow tree should match, is that right? (See my example in that comment).
If so, that seems reasonable to me, but that is not in the specification (nor it seems the most obvious interpretation, given @fergald seems to have read the spec the same way I did). I can file an issue for that if you want.
Ah, I see what Fergal said. Looked at from the perspective of the element that ::part() refers to, sure, it can have different names. They're still not about scope, tho, they're about what shadow host you're drawing it from. The argument to ::part() is entirely a function of the originating element of the ::part() pseudo - the shadow host that has a parts map. The element that the pseudo points to has no choice in the matter.
That means that selectors in an descendant shadow tree that specify a part that that is forwarded from an outer shadow tree should match, is that right? (See my example in that comment).
Can you restate the example with some markup? It's not clear to me from your previous post what exact scenario you're asking about here.
I had some comments in both mozilla/standards-positions#59 and w3ctag/design-reviews#230. I think my biggest concern is that ::part()
and ::theme()
feel too related to each other to have completely unrelated names. (::theme()
almost feels like a combinator that could be attached to ::part()
.) I'm also a little concerned about the complexity of the part-forwarding microsyntax. I had one suggestion, although I don't know think it actually helps very much. I haven't had much time to dig in to the spec yet, though.
So, let's say I have nested components <x-foo>
, <x-bar>
, <x-baz>
and <x-quux>
, the last of which has a part="label"
element.
For simplicity, all forward with the relevant name, that is, in x-foo, we have: <x-bar part="* => bar-*">
(did I get the syntax right?), in x-bar we have <x-baz part="* => baz-*">
, and so on.
That means I can target the label from the outer scope like:
::part(bar-baz-quux-label) { /* stuff */ }
Right?
My question is, would that same selector match the same label, even if it was declared in one of the inner scopes? I guess the intention is that it does not, and that's the bit that makes a ::part(..)
selector depend on where the selector is declared / in which scope it does live. In particular, in the design doc linked by @fergald, this is the problematic condition (emphasis mine):
Having reached the scope containing the selector succeed if any of the names of the element in this scope match the name in ::part() component.
I'm not saying it's impossible to implement or anything like that, I just think that's a somewhat new requirement that deserves some thought. What should:
shadowRoot.querySelector("[part=label]").matches("::part(label)")
return?
What about:
shadowRoot.querySelector("[part=label]").matches("::part(bar-baz-quux-label)")
Does something like :host::part
work, thus allowing you to style parts from inside the component?
The argument to ::part() is entirely a function of the originating element of the ::part() pseudo - the shadow host that has a parts map. The element that the pseudo points to has no choice in the matter.
The issue is that ::part
has multiple originating elements, depending on which scope are you matching from, if you allow forwarding.
My question is, would that same selector match the same label, even if it was declared in one of the inner scopes? I guess the intention is that it does not, and that's the bit that makes a ::part(..) selector depend on where the selector is declared / in which scope it does live.
It doesn't match, but that nothing do with scopes like you're implying. It's because, like I said in my previous comment, the part names are associated with the shadow host, not the element that the ::part() pseudo is pointing to.
The x-foo
element has a bar-baz-quux-label
part. The x-bar
element has a baz-quux-label
part. The x-baz
element has a quux-label
part. And the x-quux
element has a label
part. The fact that all of these parts end up pointing to the same deeply-nested element is irrelevant; they're completely independent facts associated with different shadow hosts.
shadowRoot.querySelector("[part=label]").matches("::part(label)")
This will be false - it's the shadow root's host element that has a "label" part, and will match ::part(label)
. And thus, since this selector is being run inside of the shadow root, and the host element is featureless, there's nothing you can put on the LHS of that pseudo-element that'll match. (You could write :host::part(label)
; that would be true.
(This is assuming that .matches()
can ever return true when comparing elements and pseudo-element selectors; pseudo-elements that point to real elements is a pretty recent thing, and I don't know that this is well-defined. But it does make sense to me for this to work.)
shadowRoot.querySelector("[part=label]").matches("::part(bar-baz-quux-label)")
Since this is being run inside the x-quux
's shadow root, apparently, there is no element that can match that selector even if you use :host
(since the host element is x-quux
, which doesn't have a "bar-baz-quux-label" part in its map).
The issue is that ::part has multiple originating elements, depending on which scope are you matching from, if you allow forwarding.
This is the "pseudo-elements that point to real elements are new" confusion I just mentioned. The originating element of a pseudo-element is the thing matched by the compound selector to its left. In ::part()
, that's just the implicit *
, but of course it can be more. All four of the ::part()
examples you list are different pseudo-elements, each originating from a different host. They just all happen to point to the same element.
This is the "pseudo-elements that point to real elements are new" confusion I just mentioned. The originating element of a pseudo-element is the thing matched by the compound selector to its left. In ::part(), that's just the implicit *, but of course it can be more. All four of the ::part() examples you list are different pseudo-elements, each originating from a different host. They just all happen to point to the same element.
That's fair, though in practice no UA will / can implement it that way as far as I can tell.
So in practice, at least to the implementations I know (WebKit / Blink / Servo / Gecko), it imposes the requirement of knowing where the selector has been declared / where the rule comes from, so we can know which shadow root does it come from / which shadow host matches it.
Other question as I look more through this... Where do these rules fit in the cascade? After normal author rules? How do these interact with ::slotted
, in particular where nested slots and forwarding interact? All those need to be well defined for the spec to be implementable.
So in practice, at least to the implementations I know (WebKit / Blink / Servo / Gecko), it imposes the requirement of knowing where the selector has been declared / where the rule comes from, so we can know which shadow root does it come from / which shadow host matches it.
I'm still not seeing this. The context of the selector has absolutely nothing to do with this; it's purely about which element the ::part is descending from. As I explained in your examples earlier, a given part name can match an element from inside (:host::part(bar)
) or outside (x-foo::part(bar)
) a particular shadow tree, which shows that the tree context has no relevance here. (It just happens that, due to the way we've restricted selectors and shadow trees, a given element is only directly reachable from certain contexts.)
If you store the part-name information on the target element, rather than the shadow host, then yeah, you'll need to store it as a (shadow host, part name) tuple, but that's still got nothing to do with the selector's context. ^_^
Other question as I look more through this... Where do these rules fit in the cascade? After normal author rules? How do these interact with ::slotted, in particular where nested slots and forwarding interact? All those need to be well defined for the spec to be implementable.
How selectors reaching across shadow trees work in the cascade is already well-defined by the Shadow DOM spec. Unless we specifically want some special effect, there is no need to define anything further.
Here's one concrete feedback.
There should be no difference between closed and open shadow trees for ::theme. ::theme should always penetrate shadow boundaries. Otherwise, it defeats the primary use case of ::theme.
I'm still not seeing this. The context of the selector has absolutely nothing to do with this; it's purely about which element the ::part is descending from. As I explained in your examples earlier, a given part name can match an element from inside (:host::part(bar)) or outside (x-foo::part(bar)) a particular shadow tree, which shows that the tree context has no relevance here. (It just happens that, due to the way we've restricted selectors and shadow trees, a given element is only directly reachable from certain contexts.)
Ok, I think I see what you mean now, and I see how it can be implemented without caring about the context of the selector, thanks for bearing with me :).
I think CSSOM should probably not return true ever for something that contains a ::part
pseudo, though. That means that if you do something like element.matches("x-bar::part(foo)
, you need to actually check it against all possible originating elements, which sounds somewhat annoying and special-case-y compared to how all other selectors work. But that's probably worth another issue.
I think CSSOM should probably not return true ever for something that contains a ::part pseudo, though. That means that if you do something like element.matches("x-bar::part(foo), you need to actually check it against all possible originating elements, which sounds somewhat annoying and special-case-y compared to how all other selectors work. But that's probably worth another issue.
Maybe this isn't even actually that bad... Anyway, will think more about it. Thanks @tabatkins :)
There should be no difference between closed and open shadow trees for ::theme. ::theme should always penetrate shadow boundaries. Otherwise, it defeats the primary use case of ::theme.
Interesting. I'd think that letting ::theme penetrate defeats the primary purpose of closed shadow trees. ^_^
I'll open a separate issue for this, tho.
Pinging this issue again. Chrome has implemented part and partmap attributes and ::part selector behind a flag. The spec is in line with chrome's implementation draft spec.
At this point I would like to gather any more feedback on the spec and the issues listed there and here
I will also solicit feedback from custom-element-using devs too and then bring the end result to a CSSWG conference call and move towards shipping it in Chrome.
The theme section of the spec has not had much attention, feedback is welcome on that but I am hoping to move forward with part without blocking on theme.
@hober
@dbaron you had some comments in other issues on naming and overlap between part and theme. I don't know how to capture them. Do you feel they are blockers for moving forward with part?
The biggest blocker for us is https://github.com/w3c/csswg-drafts/issues/2411 We can't implement this without knowing the syntax for the content attribute and the semantics of IDL attribute.
Polling once more for vendor support. Spec has been updated quite a bit. I would like to take this to CSS-WG (perhaps next week).
Here's a list of things that I hope are resolved or can be resolve in WG:
part=
and exposedparts=
. Some debate over naming of exposedparts
(#2414).part
and .exposedParts
(#2414)exposedparts
syntax is "innerName outerName, sameName, ..."
::theme
is postponed pending feedback from users on need for it@rniwa @hober @annevk @tabatkins @domenic @FremyCompany @gregwhitworth @dbaron
I don't think "exposed parts" makes sense as its name. Given how much time we've spent discussing names & semantics for parts mapping, perhaps we should just postpone that and just focus on getting part
right. I really don't see a path forward to having part mapping in a short term at this point.
The semantics of the simple cases of declaring and forwarding a single part are clear (as far as I am aware), they haven't changed at all since I started on this and nobody has suggested that they should.
There has been a lot of the discussion on naming and syntax. Syntax appears settled (no complaints or alternative suggestions have been offered or requested beyond the CSS selectors version). Naming is not settled, that's fine, we can settle it. What else do you see as blocking the path?
What do you see as needing to be got right with part
?
Naming is not settled, that's fine, we can settle it. What else do you see as blocking the path?
The semantics of IDL attribute isn't clear at all. I've stated that renaming IDL attribute WOULD help but never conceded my point that it should really a map in https://github.com/w3c/csswg-drafts/issues/2414. To that end, there is no consensus about the semantics of IDL attribute at all as far as I'm concerned.
In general, I'm not at all confident the currently proposed forwarding mechanism provides a good developer ergonomics. It's unclear which name is getting mapped to which (is the first name one in the inner shadow or the second one?), and having to do string manipulation in JS just to update the mapping seems outright insane.
I'm a lot more interested in getting something simple spec'ed and shipped, which we can do immediately if we focused on part itself.
I don't think anyone disagrees that there should be a map-like API eventually. A string-based API to start with does not preclude a map one coming later and renaming was done for exactly that reason. I don't see that this blocks moving forward unless you think we will regret adding the string-API in the future. I would prefer to go with no IDL attributes than block on designing DOMTokenMap.
In my mind, exposedparts
is something that's set at element creation time and remains static. If you're updating it, you're probably doing something wrong. I haven't seen any examples that would require that except *
-forwarding but that's an argument for implementing *
-forwarding not for adding DOMTokenMap.
It's unclear which name is getting mapped to which (is the first name one in the inner shadow or the second one?) Right, you cannot tell just by looking at it, whether the inner or outer name part name comes first. However, we have documented which it is and people will soon remember. Are you suggesting it should be self-describing? The only way I can think of that makes it self-describing without making the value much more verbose and harder to parse is to rename the attribute. E.g.
innertooutertparts="foo bar, ..."
at which point it's probably clear that inner comes first. We could also try `exportpartas="foo as bar, ...". I'm not really a fan of either of them. Do you have a suggestion?
Part of the disconnect here, I believe, is that we (the Chrome team) have been doing a lot of developer and partner outreach around these features. We've heard a few times that the feature is much less useful without part forwarding. We've also heard from people who need string reflection (because they are using frameworks that abstract the DOM and assume every attribute has a corresponding property), and the proposal for map reflection was seen as unnecessary and would not be used.
But we haven't done a good job getting those partners to speak up to the CSSWG on this repository. We can work on that, which can help steer the discussion. If nothing else, several of them will be attending TPAC.
If necessary, we can also break off features which need more data into a separate incubation repository and spec. Then we can work on shipping them and gathering in-the-field data on their usefulness. Or not---if it turns out early feedback was wrong, and people don't use those features or need them changed, then we can revise the incubating features before anyone else invests energy in shipping.
I don't see that this blocks moving forward unless you think we will regret adding the string-API in the future. I would prefer to go with no IDL attributes than block on designing DOMTokenMap.
I'm sure you don't but we do.
But we haven't done a good job getting those partners to speak up to the CSSWG on this repository. We can work on that, which can help steer the discussion. If nothing else, several of them will be attending TPAC.
I don't think that kind of voicing would affect our overall objection to the current proposal.
If necessary, we can also break off features which need more data into a separate incubation repository and spec. Then we can work on shipping them and gathering in-the-field data on their usefulness.
I think we would be a lot more willing to show our support if we got rid of the part forwarding and focused on the rest because the rest is completely uncontroversial. Heck, we might be able to implement the rest in a couple of days by hooking it up to our existing pseudo element mechanism we have.
It didn't ever became part of https://www.w3.org/TR/html-design-principles/ unfortunately, but I think @rniwa's point, and what the "baby steps" principle was meant to convey, of doing things in small incremental steps, is a good one.
You all might remember there were similar claims of demand for multiple shadow roots per shadow host. That in part ended up delaying agreement around shadow trees for years and the need for it thus far hasn't resurfaced.
I think the more relevant design principle in this case is authors over implementers. It's easier for @rniwa to implement something smaller, but without the ability to style nested custom elements, ::part is not very useful for authors. As I said, we can make that more concrete via developer feedback and if necessary moving things to incubation.
It's easier for everyone to implement something smaller, no need for condescension. And I don't see how the principles are in conflict. Authors over implementers would only disagree with baby steps if the smaller and larger feature set were actually in conflict.
If Apple wants to implement a subset for now, that's fine, incremental building towards the full feature set is fine. But part forwarding is absolutely a necessary aspect of the eventual feature.
Without it, we have only two possible worlds:
You just plain can't style sub-components of your components. You're able to use ::part() on a component's parts, but have to continue to use custom properties to style sub-components. (AKA you either don't use sub-components at all, or all components continue to use custom properties as their primary styling mechanism, as maintaining two types of styling mechanisms is annoying.)
We let you chain ::part(), so you can access all of the sub-components of every component you're using. This is the same over-exposure of internal details that we're trying to prevent with Shadow DOM in the first place! It means that the outside world knows whether you've implemented some piece of your component yourself or via a sub-component (and thus you can't change this without risking breakage), and the outside world has full access to all of the parts of your sub-components (so your subcomponent can't change without risking breakage).
If we aim for world 2, with the idea that we'll add some way to restrict what sub-parts are exposed, congratulations, you've just reinvented part forwarding with a less convenient syntax (have to use ::part()::part()
now rather than just ::part()
). If we aim for world 1, that's an obviously bad world that we don't want authors to land in.
We can quibble over the details of part forwarding still; I'd be fine with removing any DOM API for it from this level, beyond basic getAttribute()
/setAttribute()
usage. But forwarding, itself, is an absolute necessity for a reasonable API for authors. If you disagree, please state which of the two above worlds you'd like for us to land in, or provide a plausible third possibility.
(There were plausible worlds without multiple shadow trees; trying to use that as an example doesn't work.)
Hey, I haven't read all the issues that have ever been discussed here, it would be nice if we could discuss this at the f2f if there is a list of contentious things on which feedback would be welcome, but I however gave the spec another look. And wow at a personal level I'm super excited about this spec!
I furthermore don't see any reason why Microsoft wouldn't support this spec moving forward as a prototype implementation in Blink. We can always learn from implementation experience and feedback, but this seems like a solid start.
That being said, I do have some minor feedback. The spec mentions part="confirm-button, * => confirm-*"
as something that exits, but the syntax of part
doesn't seem to allow this. Should this be added there?
Also, I would tend to try to keep the part
mapping syntax close to the desugaring syntax in javascript, so I'd voice a weak preference for part="keepAlike, oldName:newName"
and by extension oldName-*:newName-*
. I do see very valid use cases for exposedparts to perform prefixing operations for sub-components, I'd strongly suggest to keep this in v1.
Additionally, -- and I realize this doesn't have to be part of v1, -- something I would enjoy having along this would be something like :of-part(label)
which could be use within a tree to match any element that is in that tree and whose closest ancestor with the part
attribute defined has value label
(similar to how :lang
works today) maybe with :in-part(label)
which would select if any ancestor has part=label
(both of these would not cross shadow tree boundaries).
Finally, -- still a possible v2 addition, -- I wouldn't mind if we were able to add 'states' to parts. Something like button[secondary disabled]
whose secondary
and disabled
states would be able to be targeted as ::part(button[disabled])
as a single class, and would naturally follow the button
along with its eventual remapping like ::part(confim-button[disabled])
.
Oh, one more thing.
For example, x-panel::part(confirm-button)::part(label) never matches anything. This is because doing so would expose more structural information than is intended.
I do agree with this, but I have a weak opinion we should probably allow something like x-panel::part(confirm-button)::theme(label)
and I don't think the spec is very clear on that.
[EDIT] I say this because I'd probably use ::theme
to set thematic non-inherited custom properties, and I might want to do that per component part; I guess it is possible not to use themes and set inherited custom properties on the part, but being able to set a theme for a part seems useful to me as this is already possible using custom properties today.
Also, I would tend to try to keep the part mapping syntax close to the desugaring syntax in javascript, so I'd voice a weak preference for part="keepAlike, oldName:newName" and by extension oldName-:newName-. I do see very valid use cases for exposedparts to perform prefixing operations for sub-components, I'd strongly suggest to keep this in v1.
Ooh, that's a pretty reasonable critique. While I do often find myself having to think for a moment to remember the correct ordering for destructuring, having CSS use a related syntax at least means that I'd only have to learn this once. @fergald , @rniwa, what do y'all think?
Additionally, -- and I realize this doesn't have to be part of v1, -- something I would enjoy having along this would be something like :of-part(label) which could be use within a tree to match any element that is in that tree and whose closest ancestor with the part attribute defined has value label (similar to how :lang works today) maybe with :in-part(label) which would select if any ancestor has part=label (both of these would not cross shadow tree boundaries).
Already doable today by just giving the part multiple names. You can say part="button, button-disabled"
for instance, then both ::part(button)
and ::part(button-disabled)
will work.
Already doable today by just giving the part multiple names. You can say part="button, button-disabled" for instance, then both ::part(button) and ::part(button-disabled) will work.
Yeah, but that is verbose if you have a lot of them. Also you have to forward them all in exposedparts which then becomes verbose too. I agree []
is just syntactic sugar but it seems like a very convenient one to me. Maybe doesn't need to be in ::part()
and just in the attribute syntax though, didn't think this through.
Yeah, happy to discuss that in v2; the fact that it could come along "for free" when you forward the part, without you having to manually forward all the sub-states, does indeed seem useful.
Also, I would tend to try to keep the part mapping syntax close to the desugaring syntax in javascript, so I'd voice a weak preference for part="keepAlike, oldName:newName" and by extension oldName-:newName-. I do see very valid use cases for exposedparts to perform prefixing operations for sub-components, I'd strongly suggest to keep this in v1.
Ooh, that's a pretty reasonable critique. While I do often find myself having to think for a moment to remember the correct ordering for destructuring, having CSS use a related syntax at least means that I'd only have to learn this once. @fergald , @rniwa, what do y'all think?
Yeah, @grorg and I had some discussion today, and came up with almost identical syntax with exportparts
content attribute. So it would read like <sub-component exportparts="a:b c">
would mean that exporting the part "a" defined in the subcomponent as part "b" of this component, and forward-declare "c". I think that's probably the best syntax we've come up with so far.
Comma-separated, hopefully?
Right, comma separated.
So that's "outer1 : inner1, outer2 : inner2, same1, same2"
?
I think the javascript-compatible way is inner1: outer1
if I understand inner
and outer
as you do
Yeah. oldName:newName
is how JS works:
const {foo: bar} = {foo: 1};
// same as:
const bar = ({foo: 1}).foo;
Yeah, since it's called exportparts
, inner: outer
since we're exporting inner
as outer
.
OK, spec and explainer are updated to exportparts="inner : outer"
.
Here's a list of things that I'd like to confirm:
exportparts="innerName : outerName, sameName, ..."
(#2411)::theme
is postponed pending feedback from users on need for it
I am planning to implement the ::part() half of the shadow parts spec in Blink. In order to send an Intert To Implement for this (as an experimental feature), I need to know what support there is from other browsers. My understanding is that is that there is broad support for the basics of ::part from supported by Edge, Safari and Firefox. It would be very helpful if you could confirm this here. Thanks.
@travisleithead @smaug @rniwa