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-scoping-1] Clarify exactly what :host(<sel>) can use as argument #7953

Closed
tabatkins opened this issue Oct 25, 2022 · 13 comments
Closed

Comments

@tabatkins
Copy link
Member

The :host() definition says:

When evaluated in the context of a shadow tree, it matches the shadow tree’s shadow host if the shadow host, in its normal context, matches the selector argument. In any other context, it matches nothing.

This allows you to do things like :host(.foo) to test if the host element has the foo class, which you can't tell from within the shadow because the host is featureless.

The selector argument is specified to only be a compound selector, with the intention that it only needs to look at the host element itself to match. (This was intentional and is the difference between it and :host-context().) But technically selectors like :is(.foo .bar) and :has(> .foo) are also compound selectors, tho they contain complex selector arguments. Per the grammar they're allowed, but should they be? If they should be, should we just drop the <compound-selector> restriction and go with a plain <selector>? (This is a pretty general question for <compound-selector> arguments in other locations, too.)

Further, the intention of :host() when I wrote it was just to "de-cloak" the host element, but to do so I phrased the selector as matching "in [the host element's] normal context". This can be read as implying that :host(:has(> .foo)) tests if the host element has a light dom child with the foo class, and in fact Safari does exactly this, while Chrome doesn't. (Compare with :host:has(> .foo), which definitely tests if a shadow dom child has a foo class.) Should this be considered correct?

(Context: Reddit is apparently depending on the Safari behavior to style a slot element differently based on whether something is slotted into it or not, using a selector like :host(:has(> [slot="foo"]) slot[name="foo"]. Without this behavior they'd have to listen for slotchange events and manually toggle a class on the slot element.)


So in short:

  1. Should a selector with a <compound-selector> argument allow complex selectors to be "smuggled in" via :is() and :has()? If so, should we just drop the restriction entirely?
  2. If the answer to 1 is "yes, allow them", should :host() literally evaluate its selector argument in the host tree, so its light-dom ancestors/descendants are visible to :host(:is(...)) and :host(:has(...))?
@tabatkins
Copy link
Member Author

My naive preference is to say the answer to 1 is "yes, and yes" (allow smuggling in complex selectors, and just loosen the grammar to allow them generally), and to 2 is also "yes" (allow the selectors to see the host's light dom surroundings).

But the restrictions were originally put in place for perf reasons, and I'm not involved with the implementation, so I can't say how problematic this would be.

@Loirooriol
Copy link
Contributor

Same as #6737?

@tabatkins
Copy link
Member Author

Ah yes, question 1 is a dupe of #6737, but question 2 is still distinct. Thanks for the ref!

@lilles
Copy link
Member

lilles commented Oct 25, 2022

For consistency, if restrictions are lifted on :host(), restrictions should also be lifted for ::slotted()?

There is some bug with :has() in ::slotted() in Safari (lime bg on the span without an <i> as well):

<!doctype html>
<main>
  <div id="host"><span>Green<span> <span>Green on lime<i></i></span></div>
</main>
<script>
  let root = host.attachShadow({mode:"open"});
  root.innerHTML = `
    <slot></slot>
    <style>
      :host(:is(main > #host)) { background-color: blue; }
      :host(:has(i)) { border: 10px solid orange; }
      ::slotted(span:has(i)) { background-color: lime; }
      ::slotted(:is(#host > span)) { color: green; }
    </style>
  `;
</script>

@emilio
Copy link
Collaborator

emilio commented Nov 16, 2022

I think I'd be opposed to lift the :host() restrictions for the same reasons described in #1914. Instead the restrictions should apply to the compound selector arguments like other restrictions do already.

@tabatkins
Copy link
Member Author

Yeah, that's a consistent position that I'm fine with, and suspected we'd land on. Just requires some extra work on the spec side to make it work.

(Also, the original scenario that brought this up - that Reddit wants to know if there's anything assigned to a slot - would be solved both simpler and more correctly by #6867, so we can discount it as a motivating case.)

@tabatkins
Copy link
Member Author

Gonna close this as a dupe of #5093, where we have proposed edits answering this exact question - the logical combo pseudos pass their own context's restrictions on to their arguments. This means that :host(:has(...)) is invalid, since its arguments are inherently complex selectors (because they're relative, so there's a combinator), so imposing a compound-selector restriction on them makes all possible arguments invalid.

@justinfagnani
Copy link

@tabatkins I would like to see this issue reopened separately from #5093

There are some cases that are difficult to do without :host(:has()). Right now that works in Safari and I'd love to see it work in all browsers.

@tabatkins
Copy link
Member Author

Is the case you're discussing "check if a <slot> has an element slotted into it"? If so, we already have a proposal to address that directly with :has-slotted

If it's something else, could you elaborate?

@justinfagnani
Copy link

They're related, but not exactly the same.

Consider a case like:

:host(:not(:has(input:checked))) {
  border: solid 4px red;
}

meant to be used with markup like:

<my-element>
  <section>
    <label>One: <input type="checkbox" name="one"></label>
    <label>Two: <input type="checkbox" name="two"></label>
    <label>Three: <input type="checkbox" name="three"></label>
  </section
</my-element>

:has-slotted() (which is great and needed! I've been asking for it here) can match a slot, but we'd still need two things: 1) a way to match a host that has a slot with assigned nodes, and 2) for :has() to work inside :has-slotted(), like :not(:has-slotted(:has(input:checked))).

@cdoublev
Copy link
Collaborator

cdoublev commented Dec 29, 2022

I am sorry to disrupt your current discussion.

If I correctly understand the reason for restricting :host() argument to <compound-selector>, I think these cases should also be invalid:

  • :host(:host-context(.foo))
  • :host(:nth-*-child(1 of .foo .bar))
  • :host(:current(.foo .bar))

Basically, any functional pseudo-class/element taking a selector as its argument should be restricted to <compound-selector> (or any sub-production matching its argument syntax).

I guess these cases cannot match anything? Should they be invalid?

  • :host(::part(part-name))
  • :host(::slotted(slot-name))

And :host(:host(.foo)) is valid but nesting :host() is useless, right?

@Westbrook
Copy link

I'm agree with @justinfagnani here that :has-slotted (if it applied directly to a <slot> element) can't really replace the current implementation of :host(:has(...)) that is found in Safari. In the case that it would be the required path, there would really need to be a path to internal :has() on :host(), or "host has something in its shadow DOM so style accordingly". I've thought that:host(:has()) and :host:has() should be different to cover this, but it seems like a much longer path than admitting that CSS and browsers are better, faster, and smarter than they were in 2011 and allowing for complex selectors (or at least pseudos like :has(), :is(), .etc) in :host(...).

@tabatkins
Copy link
Member Author

If I correctly understand the reason for restricting :host() argument to , I think these cases should also be invalid:

The second and third are indeed invalid, but the first is valid. (It won't match anything, since the host element only matches :host/:host-context() from within the shadow; outside of the shadow (which is the context in which the selector argument is evaluated) it won't match the pseudo.)

I guess these cases cannot match anything? Should they be invalid?

As both argument contain pseudo-elements, neither of them are compound selectors, so they're both invalid.

And :host(:host(.foo)) is valid but nesting :host() is useless, right?

Yes.

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

7 participants