Closed tabatkins closed 2 weeks ago
Thanks for bringing the conversation here @tabatkins! I think the :host:has(…)
combination will also be a boon to the in progress slot:has-slotted(…)
API that is pending in https://github.com/w3c/csswg-drafts/pull/10586, and will be quite powerful as an appropriate customization of:host:has(slot:has-slotted)
. 🥹
To clarify, are you saying that both :host:has(h1)
and :has(h1)
should be able to match the host, or only the former?
In https://github.com/w3c/csswg-drafts/issues/10179#issuecomment-2093869789 the idea is
Compound selectors are allowed to match the host only if all the contained simple selectors are allowed to match the host.
I think it should match in both cases, just like the logical combination pseudos. The reason the host is featureless is because you can't control the markup of the host element, and so it's easy to write a selector you expect to only match the markup you control (in the shadow) and have it accidentally match the host element due to the outer page doing things you can't predict.
This doesn't apply to :has()
- it's matching based on markup you do control (in the shadow), and thus is completely predictable. Even if you use a complex selector in the :has()
argument, because it's matching inside the shadow and uses that matching context, it can't match the host element unexpectedly. (That is, :has(.foo .bar)
wouldn't be able to match just from the host element having a foo
class; you'd need to write :has(:host(.foo) .bar)
to make that happen, explicitly indicating that you're looking at the host element's markup.)
The only way to get an unexpected match is if you just forget the host element exists at all (because you generally don't pay attention to it), but that's a brain fart, not an unpredictable situation. I think that's okay. In particular, writing an unqualified :has()
seems like a weird thing to do in the first place, which is the only way it would match the host without actually mentioning :host
.
This would allow https://github.com/w3c/csswg-drafts/issues/10492 to be solved using :host:has(:popover-open)
, correct? We just had an internal developer run into that issue recently.
Yes, that would work.
(We should discuss this at the same time as #10179 fwiw; they're covering the same space.)
Shouldn't this be about :host(:has(...))
? I think :host:has()
should generally not work to be consistent with other selectors like :host:nth-child(...)
(which IIRC doesn't match)
:host(:has(…))
matches against light DOM children already, at least in Firefox and WebKit, and should continue to go so and expand to Baseline status as soon as possible. This aligns with :host(.class)
et al also referencing things outside of the shadow root.
:host:has(…)
should introspect the shadow root the same way :host > .child
does. It would seem that:host:nth-child(…)
should work in this way, as well.
I think there are a few cases here that it would be great to untangle and specify.
The spec defines Tree-Structural pseudo-classes, but it doesn't specifically distinguish the scope or relationship: ancestor or descendent.
Similarly, :host() matches the argument in the host's "normal context," the scope in which the host itself exists, not its Shadow DOM.
Additionally, there's a special carve out for the flat tree against which match :hover
, :active
, :target-within
, and :focus-within
. And the syntax uses the function version::host(:focus-within)
.
Currently, :host(:empty)
works and matches against the host's light tree. As noted, :host(:has(...))
seems like it should work, but it's currently not well supported.
When Shadow DOM is involved, for the cases defined in the spec, only a descendent relationship is relevant to consider since this could be either via the light or shadow tree or both, the composed tree.
The example noted above :host:nth-child(…)
needs no consideration since it's based on an ancestor relationship (host within parent). It should never be able to match.
I believe only :has(...)
and :empty
are potentially ambiguous since they could match against the light, shadow, or flat tree.
Using the functional :host(...)
syntax, it does seem like the spec is suggesting these should match against the light tree, and they do (minus bugs).
Matching these against the shadow tree does seem useful as is suggested here, and using the non-functional syntax also seems to make sense :host:has(...)
or :host:empty
(seems like this should also work).
Adding the capability makes sense, but I see 2 downsides to using this syntax for it:
1. It's unclear if there's a mental model for when to use the functional syntax
:host()
and doing so changes the meaning. For example, why is it :host:after
and not :host(:after)
? At least only one of those works.2. Using light v. shadow tree as a key for when to use the functional v. non syntax would break down for flat tree matching selectors like :host(:focus-within)
.
Shouldn't this be about :host(:has(...))?
No. As I said in the initial comment, :host(:has(...))
is a separate thing that's already well-defined. This is about :host:has(...)
.
I think :host:has() should generally not work to be consistent with other selectors like :host:nth-child(...) (which IIRC doesn't match)
It's not clear if you've read my later comment giving a more thorough explanation for why I think :host:has()
is fine to match. If you have read that, could you elaborate on how you disagree?
(And correct, :host:nth-child()
does not match. While it would not be problematic for it to do so, since it only has access to the information in the shadow tree and thus always sees the host element as a lone sibling, it wouldn't be useful for it to do so either, for the exact same reason.)
@sorvell You seem to be confusing yourself about what it means for :host(...)
to take a selector argument, versus having selectors following :host
. (And possibly have let emilio confuse you into thinking this issue is about :host(:has(...))
, which it is not.)
:host(sel)
matches the selector argument against the host element in the context of the outer (light) tree. If sel
would match the host element in that context, then :host(sel)
matches the host element within the shadow.sel:host
attempts to match the sel
selector against the host element in the context of the inner (shadow) tree. Within the shadow, the host element is featureless, and only matches the exact selectors that are explicitly specified as allowed to match.:host(::after)
does not work because the host element does not match the selector ::after
- it's not an ::after pseudo-element! This happens to be true in either context, but is certainly true in the light context. :host::after
works because :host
is allowed to match the host element, and then ::after
isn't trying to match the host element, it's just a pseudo-combinator shifting you into matching the host's pseudo-elements; it's identical to :host > div
in action.
Yeah, sorry, it wasn't clear to me that the intention was to have :host:has() match the contents of the shadow tree.
I think it's a bit confusing that :host:has()
and :host(:has())
would do such different things, fwiw, but it's not like I have a counter-proposal if this is something people need... It just feels a bit weird conceptually that we'd match the fragment's subtree but then forward the selector to the real element...
:host(::after) does not work because the host element does not match the selector ::after
Thanks for the clarification, that makes sense.
Overall, this would be a valuable feature to add.
@tabatkins Do you think it makes sense to support :host:empty
as well?
@emilio
I'm not sure I understand the confusion. You have two entirely separate subtrees, both rooted in the same element; the two syntaxes match against one or the other. It seems unremarkable to me that they would do different things, because that's what happens for every other possible selector as well: normal selectors match against the shadow tree (which is where the stylesheet in question lives, so that's exactly what you'd expect), and :host(...)
arguments match against the light tree.
@sorvell
Do you think it makes sense to support :host:empty as well?
Hm, yeah, that's workable under the same argument (it's only matching based on the non-host contents of the shadow, which are under the stylesheet author's control). It's only different from :host:has(*)
in the case that the shadow contains text nodes but no elements, tho.
Not sure it's particularly useful - it seems a lot more likely that that component knows whether it's put anything inside the shadow tree, and in particular, a <style>
in the shadow would prevent it from matching, but there are ways to attach stylesheets without inserting nodes, so I guess it's reasonable...
The CSS Working Group just discussed [selectors][css-scoping] Should `:host:has()` match?
, and agreed to the following:
RESOLVED: :host:has() can match, :has() can't
In https://github.com/web-platform-tests/wpt/pull/47441 a few tentative tests are suggested that test whether
:host:has(...)
works. The test author correctly notes that this isn't currently specified to work - only the logical combination pseudo-classes are allowed to match featureless elements and:has()
, while defined in the "logical combination" section, isn't on the list of logical combo pseudo-classes.I agree with the tester, tho, that this should work.
:host h1
works just fine, so:host:has(h1)
should work the same. I suggest that we add:has()
to the list of pseudo-classes allowed to work on featureless elements.(Note the first few tests in the WPT PR are testing the different scenario of
:host(:has(...))
, which is already well-defined to match, in a different way to what's discussed here. I'm talking about the latter three which are marked "tentative" in their filenames.)