[csswg-drafts] [selectors-4] Remove the :scope dependency from the relative selectors definition (#6399)

byung-woo has just created a new issue for https://github.com/w3c/csswg-drafts:

== [selectors-4] Remove the :scope dependency from the relative selectors definition ==
Hello,

I'm currently prototyping the ':has' spec for chromium, and I landed the [second patch](https://chromium-review.googlesource.com/c/chromium/src/+/2914967) of supporting the child/next-sibling/subsequent-sibling relations some days ago.

While implementing the patch, I had to consider the various cases involving `:scope` in the `:has` argument, and came to the conclusion that the `:scope` dependency of the relative selector causes a lot of problems.

So, I'd like to ask opinions about removing the `:scope` dependency from the [relative selectors definition](https://www.w3.org/TR/2018/WD-selectors-4-20181121/#relative).

As the grammar shows, the relative selector is the selector to begin syntactically with a combinator.

```
<relative-selector> = <combinator>? <complex-selector>
```

Similar with the `complex selector`, the relative selector represents a set of simultaneous conditions on a set of elements in the particular relationship described by its combinators. The only difference is that the relative selector can specify a relation to the reference element with the leftmost combinator.

To specify the reference element of the relative selector, the spec decided to use `:scope`.

> Certain contexts may accept relative selectors, which are a shorthand for selectors that represent elements relative to a :scope element.

And it also specifies how to handle the :scope in the relative selector. ([Absolutizing a Relative Selector](https://www.w3.org/TR/2018/WD-selectors-4-20181121/#absolutizing))

The decision of using `:scope` for the reference element creates too much confusion especially with `:has` (`:has` seems to be the only selector that currently uses relative selector spec)

Below, I listed issues about `:scope` in `:has`. All the issues came from the `:scope` dependency in the relative selector. I think the `:scope` has its own purpose, and what relative selector need to have is a definition of its reference element. Using the `:scope' for the reference element of the relative selector looks problematic.

How about removing `:scope` dependency from the relative selector?
I think we can use the relative selector without absolutizing. Or we can use it after absolutized with a dummy pseudo(other than :scope) which represents reference element of the relative selector (And the dummy pseudo should be restricted to appear at the very beginning if it is allowed to use explicitly).

These are the issues from the `:scope` dependency.

**1. Different behavior of `:scope` in `:has`**

The [`:has` spec](https://www.w3.org/TR/2018/WD-selectors-4-20181121/#relational) tells that, `:has` evaluates the `:scope` in its argument selector as an element that the `:has` is representing.

> It represents an element if any of the relative selectors, when absolutized and evaluated with the element as the :scope elements, would match at least one element.

This means that, when we have `.a:has(> .b)`, it will be absolutized to `.a:has(:scope > .b)` internally before matching, and the `:scope` will be evaluated as `.a` element. Based on the :has definition, the two selectors `.a:has(> .b)` and `.a:has(:scope > .b)` are equivalent.

This behavior is changing the usual `:scope` usage.
- The `:scope` in the style rule `:scope > .b { color=blue; }` will be `:root`. But the `:scope` in the js call `main.querySelectorAll(':scope > .b')` will be the `main` element.
- The `:scope` in the style rule `.a:has(:scope > .b) { color=blue; }` will be the `.a` element, and the `:scope` in the js call `main.querySelectorAll('.a:has(:scope > .b)')` will be also `.a` element.

And there can be some confusing cases, like...
- `main.querySelectorAll('.a:has(~ .b:is(:scope .c)')` -> `:scope` should be `.a`? or `main`?
- `.a:has(~ .b:is(:scope .c)) {...}` -> `:scope` should be `.a`? or root?
- `main.querySelectorAll(':scope .a:has(:scope ~ .c)')` -> The first :scope will be `main` but the second `:scope` will be `.a`
- ...

**2. Explicit `:scope` in a `:has` argument can create complex cases.**

Matching `:has()` on an element is basically heavy operation because it need to match its argument selector on its descendants.

For example, for `.a:has(.b)`, all the descendants of `.a` need to match the argument selector `.b` to determine whether the `.a` element matches the `:has(.b)` selector or not. And when we have any `.b` element from descendants of `.a`, we can mark ancestors of the `.b` as a possible scope element of `:has(.b)'. With this, we can prevent repetitive argument matching operations for some cases.

But when a `:scope` is not leftmost (`.a:has(.b :scope .c)`) or it is compounded with other simple selectors (`.a:has(.b:scope .c)`), it is impossible or difficult to apply the optimization. The scope element of `:has(.b:scope .c)` must satisfy both `.b` and `:has(c)`, and the scope element of `:has(.b :scope .c)` must satisfy both `.b *` and `:has(.c)`.

The worst thing about ':has(.b :scope .c)' is that, it is same with `:has(:is(.b *):scope .c)`. So the left side of the `:scope` in `:has` creates similar problems of `:is` in `:has`(actually worse)  and the problems are really complex.

I think this issue is related with the responsibility of `:has` selector. What `:has` selector need to provide is selecting elements with ancestors or previous-sibling relations. But in those case, `:has` need to check the selected element, and need to check descendant/next-sibling relations from its upward. Instead of `.a:has(.b .c:scope .d)`, using `.b .a.c:has(.d)` is more clear, intuitive and match the `:has` responsibility.

(Actually, the argument selector `.b .c:scope .d` is a concatenation of `.b .c:scope` and `:scope .d`(which means `:has(.d)`) and the first part doesn't need to be a part of the relative selector. This looks another issue)

**3. Ambiguity of absolutizing a relative selector with `:scope`**

The leftmost combinator of the relative selector differentiates itself from complex selector. And the leftmost combinator represents the relation to the reference element of the relative selector. To explicitly representing this, the `~ .a .b` will be absolutized to `:scope ~ .a .b` before matching. But when we have `~ .a :scope .b`, it will be absolutized to `:scope ~ .a :scope .b` which doesn't make sense and will never match.

Please view or discuss this issue at https://github.com/w3c/csswg-drafts/issues/6399 using your GitHub account


-- 
Sent via github-notify-ml as configured in https://github.com/w3c/github-notify-ml-config

Received on Monday, 21 June 2021 15:33:25 UTC