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-cascade-6] Scoped selectors shouldn't match the scope root unless explicitly requested with :scope? #8377

Closed
mirisuzanne opened this issue Jan 30, 2023 · 26 comments

Comments

@mirisuzanne
Copy link
Contributor

This was suggested by @bramus in an offline conversation.

In the current cascade-6 specification, the @scope rule defines a 'scope' as a tree fragment that includes the scope root (:scope) element, but does not include any lower-boundary elements, or their decedents. Since the scope-root is 'in scope', it can be matched by scoped selectors:

@scope (div.scope) {
  /* This selector will match the :scope element */
  div { ... }

  /* :scope can be explicitly excluded */
  div:not(:scope) { ... }
}

While it's possible to exclude the scope-root from selectors by appending :not(:scope) - we could consider doing it the other way around. For example, a 'shadow host' is not matched by selectors in the shadow DOM unless :host is explicitly specified. The scope-root would still need to be 'available' to scoped selectors, but it would only match when specifically mentioned:

@scope (div.scope) {
  /* This selector WOULD NOT match the :scope element */
  div { ... }

  /* :scope can be explicitly included */
  div, :scope { ... }
}

On the one hand, it may be strange to have an element that is available 'in scope' but not matched by default. On the other hand, authors are likely to expect that matched elements will be 'nested' inside the scope-root selector. In either case, we would have a reasonable workaround for the counter-examples.

@tabatkins
Copy link
Member

The other way to exclude the scope is just * > ..., right? And when nesting you can just do > ..., which might be simple enough to be worth leaving it as the way to handle things.

Note that the host element is unmatchable except by the :host pseudo; it's not contextual in the selector itself. That is, you can't write :host.foo - by definition the .foo won't match the host element. I suspect that it'll be relatively common to want to match additional selectors against the scope, tho. For host elements we allow that with a functional form - :host(.foo), but we don't (yet?) have a :scope() functional form.

I lean weakly towards "no change", then.

@mirisuzanne
Copy link
Contributor Author

The other way to exclude the scope is just * > ..., right? And when nesting you can just do > ..., which might be simple enough to be worth leaving it as the way to handle things.

Not quite - though your point may still stand. Only the matched element needs to be in scope, the rest of the scoped selector matches unrestricted – so you would need to explicitly list the scope root itself as the ancestor: :scope * would match everything in-scope except for the scope root.

@mirisuzanne
Copy link
Contributor Author

I suppose > * might imply a starting &, which would be treated as :scope in this case? But * > * could still match the scope root itself.

@tabatkins
Copy link
Member

Oh duh, right. Well yeah, at least the Nesting version would work by default, yeah.

@romainmenke
Copy link
Member

romainmenke commented Jan 30, 2023

I suppose > * might imply a starting &, which would be treated as :scope in this case?

How?
Do you have a full example?

/* relative selector is not allowed here */
> * {}

@scope (div.scope) {
  /* relative selector is not allowed here */
  > * {}
}

.bar
  @scope (div.scope) {
    /* relative selector IS allowed here, but & is `.bar` */
    > * {}
  }
}

In the last example it would be .bar > * where anything that matches must also be within scope div.scope right?

@bramus
Copy link
Contributor

bramus commented Jan 30, 2023

Thanks @mirisuzanne for posting this issue. It was still on my todo, but glad to see you posted it (in a more articulate way than I ever could).

The idea came up after playing with the @scope prototype in Chrome and somewhat at the same time thinking about sibling scopes. For sibling scopes it would seem very weird that one of the boundaries is included by default and the other not. To make sense, both boundaries should either be included, or both excluded.

With regular @scope excluding the end boundary by default, excluding the start boundary seemed like the logical thing to do in order to reach a balanced type of selection. From thereon out :scope came to mind to target the start boundary element itself.

One thing that might seem a bit weird in this scenario is that there’s only one pseudo available to select the start boundary, not one to select the lower boundary. Miriam had arguments that this wasn’t too big of a problem.

@mirisuzanne
Copy link
Contributor Author

@romainmenke my understanding was that we had decided to make & valid everywhere. According to the Editor's Draft:

When used in the selector of a nested style rule, the nesting selector represents the elements matched by the parent rule. When used in any other context, it represents the same elements as :scope in that context (unless otherwise defined).

I suppose it's not clear to me if you're allowed to leave the & off those selectors, when not in a nested style rule? Maybe that needs to be clarified? At the very least, all your example selectors seem to be allowed if you start them with &, which is normally implied by a relative selector.

One thing that might seem a bit weird in this scenario is that there’s only one pseudo available to select the start boundary, not one to select the lower boundary. Miriam had arguments that this wasn’t too big of a problem.

@bramus Yeah, that might be a little odd, and may point to a :boundary selector being useful (name TBD of course).

@romainmenke
Copy link
Member

romainmenke commented Jan 30, 2023

I suppose it's not clear to me if you're allowed to leave the & off those selectors, when not in a nested style rule? Maybe that needs to be clarified? At the very least, all your example selectors seem to be allowed if you start them with &, which is normally implied by a relative selector.

I suggested to allow relative selectors in more contexts here : #8010
But this is a bit of a tangent :) So maybe worth revisiting that issue in case it could be helpful for @scope.

@mirisuzanne
Copy link
Contributor Author

Digging into this, I'm convinced that authors will expect 'nested by default' behavior - which would better match how nesting works as well. To get there, we don't actually want to remove the scope root from the scope. We only want to imply that the scoped element is prepended as an ancestor, unless otherwise explicitly placed in a selector. This would bring :scope and & more fully in line. Both already have the same specificity, and the same is-like parent-element matching logic already.

This is a bit different from the 'virtual' scoping root behavior defined for dom fragments, the primary use of :scope currently. According to the spec, document fragment roots can be mentioned in a scoped selector, but can't be the subject of the selector. If we have to match that behavior, I think it would be too restrictive.

If we go that rout, it's not an exact parallel with :boundary or :scope-end. On the root side, we're defining an implied ancestor/descendant relationship between selectors. On the lower boundary, we would have an element that is fully out of scope for some selectors, and fully in scope for other selectors. That might also be useful, but it would need a different mechanism for describing the behavior.

@tabatkins
Copy link
Member

According to the spec, document fragment roots can be mentioned in a scoped selector, but can't be the subject of the selector. If we have to match that behavior, I think it would be too restrictive.

Yeah, that restriction is only because doc fragments aren't elements, and selectors are defined to operate over elements. It shouldn't be taken as a more general restriction on :scope.

@mirisuzanne
Copy link
Contributor Author

In that case, my proposed resolution is that scoped selectors are implicitly descendants of the scope root element, unless :scope appears explicitly in the selector - matching the behavior of nested selectors with or without the explicit &.

I think something like :scope-end might be useful, but could have it's own issue.

@mirisuzanne
Copy link
Contributor Author

Agenda+ to resolve on that change.

@andruud
Copy link
Member

andruud commented Feb 16, 2023

+1, this is great. I think it opens up some new ways of optimizing.

unless :scope appears explicitly in the selector

This means we'll add something like the "nest-containing" concept but for scoped selectors, right?

@tabatkins
Copy link
Member

I suppose, yeah. A little more annoying to do the "if you see something that even resembles a :scope, mark the selector and preserve it in forgiving lists", but not untenable.

Hm, this will require a small edit to Selectors, tho - the restriction against matching the scoping root itself is in fact baked into the matching algorithm, so I'll need to account for this there.

@mirisuzanne
Copy link
Contributor Author

@tabatkins can you clarify that 'anything that even resembles a :scope'? Is that meant to account for using & in a scoped context?

The main difference I see here is that :scope is intended to match only the root element of a given scope - not to represent all possible matched elements of the scoping selector. I don't think we want :scope :scope to match anything (there can't be a second scoping element), while that is possible with & &. But that maybe causes a problem for treating & like :scope in a scoped context.

@tabatkins
Copy link
Member

"contains the nesting selector" has that special text where we look for an & token in the original tokens, before potentially throwing anything out for invalidity, so :is(:unknown(&), div) still counts as containing the nesting selector regardless of whether the browser understands the :unknown() pseudo or not. (We can't tell that :unknown()'s argument is meant to be a selector, after all!) We'd need to apply the same logic to :scope. This is unrelated to anything about match semantics.


But that maybe causes a problem for treating & like :scope in a scoped context.

As currently specced, & does indeed match all the elements matched by the scope-start selector, rather than something more specific that only matches the single element that happens to be the scoping root for a given element.

@mirisuzanne
Copy link
Contributor Author

That makes sense, yes.

For matching semantics with & in a scoping context, I see a couple options:

  1. Scoped & is identical to :scope, and only matches the root element. It makes the selector scope-containing, and & & wouldn't match anything.
  2. Scoped & refers to the scoping root selector, rather than the scoped element itself. That allows & & to be resolved as per usual, but then either:
    a. It continues to make the selector scope-containing, so there is no implied ancestor :scope
    b. It is not scope-containing, so a bare & would be treated as :scope &
  3. Revert the pervious decision, and don't allow & directly in @scope

Laying out those options, I think 2a is the one that would match expectations most accurately.

@tabatkins
Copy link
Member

Agreed, I think 2a is the clearest - maintains & semantics from general usage, but also interfaces with @scope in a reasonable and intuitive way.

@andruud
Copy link
Member

andruud commented Feb 16, 2023

[Why is it that the second css-nesting comes into the picture there's a list with options and suboptions? :P]

(2a) I'm not sure it's very consistent to disable the implied :scope when nothing in the selector actually represents :scope. 2b seems more consistent.

@mirisuzanne
Copy link
Contributor Author

Yeah, 2b is semantically 'cleaner' in some ways (both selectors maintain their behavior), but I think this would be very surprising to authors, and not a meaningful way of supporting & in a scoped context. Authors will expect these to reference the same elements:

.media {
  & > img { ... }
}

@scope (.media) {
  & > img { ... }
}

In the current spec, that works because we don't imply an initial :scope. But if we go with 2a, we've broken that expectation.

@bramus
Copy link
Contributor

bramus commented Feb 17, 2023

Authors will expect these to reference the same elements:

.media {
  & > img { ... }
}

@scope (.media) {
  & > img { ... }
}

That’s also the case with option 1, no? The way I read that option, is that inside @scope (…) { … } the contents are implicitly wrapped in a :scope { } block?

So that this … :

@scope (.media) {
  & > img { ... }
}

… equals that:

@scope (.media) {
  :scope {
    & > img { ... }
  }
}

That would essentially make :scope useless inside @scope, as one would need to use & instead … but maybe that’s OK?


[Why is it that the second css-nesting comes into the picture there's a list with options and suboptions? :P]

🙈

@bramus
Copy link
Contributor

bramus commented Feb 17, 2023

That would essentially make :scope useless inside @scope, as one would need to use & instead … but maybe that’s OK?

Come to think of it: for @sibling-scope that might be weird, as one might want to target the start and end boundaries there. Using & for the start boundary and something like :scope-end for the end boundary seems inconsistent.

@mirisuzanne
Copy link
Contributor Author

I think in both css nesting and scope we want to think of it as an implicit descendant combinator, not an implicit extra level of nesting.

@mirisuzanne
Copy link
Contributor Author

The proposed resolution here is:

  • Scoped selectors are implicitly descendants of the scope root (this matches css-nesting)…
  • Unless they are scope-containing, with either :scope or & explicitly placed in the selector (the matches and expands on css-nesting)
  • In a scoped selector, :scope refers to the scope root element (cannot match additional nested elements), while & refers to the scoping selector (can match additional nested elements).

This builds on (and clarifies) the resolution in #7854 that & should be allowed in a scoping context. It also ensures that scope and nesting behave in similar ways where they overlap.

@bramus
Copy link
Contributor

bramus commented Feb 28, 2023

Proposed resolution SGTM.

Maybe we can also consider :scope-end to target the end boundary along with this as it’s very closely related? Happy to branch off a new issue (as suggested here) if we can’t tackle it all in one go.

@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed [css-cascade-6] Scoped selectors shouldn't match the scope root unless explicitly requested with :scope?, and agreed to the following:

  • RESOLVED: clarifications for scoping selectors proposed by miriam
The full IRC log of that discussion <fremy> miriam: the scope roots elements are currently part of the scope, with no implied relationship
<fremy> miriam: so, if you use the same selector for both ends, the end matches the root
<fremy> miriam: bramus and others found that confusing
<fremy> miriam: so the proposal would be to imply that nesting scope selectors include :scope
<fremy> miriam: except if they explicitly refererence :scope or use ampersand
<fremy> miriam: there are a couple of differences between :scope and ampersand
<TabAtkins> & references all the elements matched by the scope-start selector (as normal for Nesting), :scope matches the specific element that is functioning as the scope in a given element's context.
<fantasai> +1 to the proposal
<fremy> miriam: mainly you can use & multiple times
<TabAtkins> +1 to the proposal
<fremy> miriam: so the proposal clarifies the difference
<astearns> ack fantasai
<fremy> fantasai: what is the specificity of :scope and ampersand?
<fremy> miriam: there is another issue on that
<fremy> miriam: but my guess is that ampersand should work just as in nesting
<fremy> miriam: (wrapping the starting scope selector in :is)
<bramus> q+
<fremy> TabAtkins: (missed short comment)
<fremy> astearns: does that work for you fantasai ?
<astearns> ack bramus
<fremy> fantasai: yes
<TabAtkins> TabAtkins: That's also my preference
<fremy> bramus: should we also introduce :end for the end boundary?
<fremy> bramus: for sibling scopes, that might be useful
<fantasai> proposal was to have & use the specificity of the selector it's referencing, and for :scope to have its normal specificity (1 pseudo-class)
<fremy> miriam: this sounds like a separate resolution
<fremy> miriam: could you provide some clear use cases for that?
<TabAtkins> have we discussed the meaning of relative selectors like `> .foo`? Do those get captured by Nesting, implying an &, or do they imply a :scope? I think the latter should be the case.
<fremy> astearns: the proposed resolution is in the last comment of the thread
<fremy> astearns: looking at tab's question on irc
<fremy> astearns: tab, do you want to voice the question?
<fremy> miriam: I think I agree with TabAtkins suggestion
<fremy> miriam: relative selectors (...missed)
<fremy> miriam: question was what happens if the selector starts with a descendant combinator
<fremy> miriam: so just adding a descendant combinator should be the same as the default
<astearns> ack fantasai
<fremy> fantasai: I think that it implies that every selector inside a scope that doesn't start with an ampersand has an additional pseudo-class specificity
<fremy> TabAtkins: no, no difference
<fremy> TabAtkins: the implied-ness is just for the meaning, but not the specificity
<fremy> TabAtkins: the implied one does not add specificity
<fremy> fantasai: p and :scope p are the same, but with different specificities
<fremy> TabAtkins: yes
<fremy> fantasai: and if I add (...) I could match more
<fremy> TabAtkins: yes, not in that use case, but in others yes
<fremy> fantasai: if you use descendants, you can match much more
<fremy> fantasai: and both :scope and & can match the scoping element, but no other selector can
<fremy> miriam: no other selector make it remove the nested-within combinator
<fremy> TabAtkins: by the magic of relative selectors, it will never happen
<fremy> TabAtkins: you have to make it absolute to override
<fremy> fantasai: ok, this is probably fine, but the spec should be extra clear about it
<fremy> astearns: so, the proposed resolution is to accept miriam's prososal + add new clarifications
<fremy> astearns: any comment?
<fremy> fantasai: lgtm
<fremy> astearns: any objection?
<fantasai> s/lgtm/I think this is a good change and I support it/
<fremy> RESOLVED: clarifications for scoping selectors proposed by miriam

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

8 participants