Implement `:is()` and `:where()` selectors
Categories
(Core :: CSS Parsing and Computation, enhancement, P3)
Tracking
()
Tracking | Status | |
---|---|---|
firefox77 | --- | fixed |
People
(Reporter: e7358d9c, Assigned: emilio)
References
(Blocks 1 open bug, )
Details
(Keywords: dev-doc-complete, Whiteboard: [layout:backlog:77])
Attachments
(6 files)
47 bytes,
text/x-phabricator-request
|
Details | Review | |
47 bytes,
text/x-phabricator-request
|
Details | Review | |
47 bytes,
text/x-phabricator-request
|
Details | Review | |
47 bytes,
text/x-phabricator-request
|
Details | Review | |
47 bytes,
text/x-phabricator-request
|
Details | Review | |
47 bytes,
text/x-phabricator-request
|
Details | Review |
`:where(…)` is the 0 specificity counterpart to `:is(…)`, which is being implemented in bug 906353. ## See also: - https://developer.mozilla.org/docs/Web/CSS/:where
Comment 1•5 years ago
|
||
This would prove really useful to simplify some of our browser CSS, even though we already managed to achieve a good result during our recent conversions of XBL stylesheets to document stylesheets by adjusting rules to use "!important". Emilio, while we don't have an immediate need, would this be difficult to implement, and would it have a similar performance to normal selectors? (The trick with CSS variables at <https://stackoverflow.com/a/51306751> is also neat, but as far as I know CSS variables may result in slower CSS.)
Comment 2•5 years ago
|
||
To clarify, my question is mainly about having it enabled just in privileged pages, to work around any potential web compatibility concerns.
Well, we could probably for the most part duplicate the code for `:‑moz‑any()` (which already has a fixed specifity, see bug 561154), but we’d have to implement support for complex selector lists and partial selector list invalidation (https://github.com/w3c/csswg-drafts/issues/3264#issuecomment-440752606)
Assignee | ||
Comment 4•5 years ago
|
||
Doing it without complex selector support is easy (it's mostly sharing code with :-moz-any). Doing it with complex selector support, supporting combinators and such (:where(.foo + .bar), :where(.bar + .baz)) is more annoying, since we need to figure out an efficient way to invalidate style when stuff in the DOM changes. That's something we need to figure out regardless for :is() / :matches() / etc, so it'd be valuable to think a bit hard about it. It'd be easy-ish (I should have notes somewhere) to do something that isn't perfect but works[1], though if I were to implement it I'd prefer to do it right. [1]: It'd over-invalidate in some cases, like when you toggle the class "foo" and have a selector like `.bar :where(.foo + .baz)`. We'd invalidate the style of all the `.baz` elements that are siblings of `.foo`, regardless of whether there's an ancestor that matches `.bar`. It'd also over-invalidate if you had `.bar :where(.foo)` and you toggled the "foo" class, even if there was no ancestor with .bar. Maybe we can live with that, though I'd prefer doing it right.
Updated•5 years ago
|
Assignee | ||
Comment 5•4 years ago
|
||
The tricky part of :is() and :where() is that they can have combinators inside,
so something like this is valid:
foo:is(#bar > .baz) ~ taz
The current invalidation logic is based on the assumption that you can
represent a combinator as a (selector, offset) tuple, which are stored in the
Dependency struct. This assumption breaks with :is() and :where(), so we need
to make them be able to represent a combinator in an "inner" selector.
For this purpose, we add a parent
dependency. With it, when invalidating
inside the :is()
we can represent combinators inside as a stack.
The basic idea is that, for the example above, when an id of "bar" is added or
removed, we'd find a dependency like:
Dependency {
selector: #bar > .baz,
offset: 1, // pointing to the `>` combinator
parent: Some(Dependency {
selector: foo:is(#bar > .baz) > taz,
offset: 1, // Pointing to the `~` combinator.
parent: None,
})
}
That way, we'd start matching at the element that changed, towards the right,
and if we find an element that matches .baz, instead of invalidating that
element, we'd look at the parent dependency, then double-check that the whole
left-hand-side of the selector (foo:is(#bar > .baz)) actually changed, and then
keep invalidating to the right using the parent dependency as usual.
This patch only builds the data structure and keeps the code compiling, the
actual invalidation work will come in a following patch.
Updated•4 years ago
|
Assignee | ||
Comment 6•4 years ago
|
||
That way we can look at the parent dependency as described in the previous
patch. An alternative would be to add a:
parent_dependency: Option<&'a Dependency>
on construction to Invalidation
, but this way seems slightly better to avoid
growing the struct. It's not even one more indirection because the selector is
contained directly in the Dependency struct.
Depends on D71421
Assignee | ||
Comment 7•4 years ago
|
||
See the comment about why this is valuable. For a selector like:
.foo:is(.bar) > .baz
Before this patch we'd generate an Dependency for .bar like this:
Dependency {
selector: .bar,
offset: 0,
parent: Some(Dependency {
selector: .foo:is(.bar) > .baz,
offset: 1, // Pointing to the `>` combinator.
parent: None,
}),
}
After this patch we'd generate just:
Dependency {
selector: .foo:is(.bar) > .baz,
offset: 1, // Pointing to the `>` combinator.
parent: None,
}
This is not only less memory but also less work. The reason for that is that,
before this patch, when .bar changes, we'd look the dependency, and see there's
a parent, and then scan that, so we'd match .bar
two times, one for the
initial dependency, and one for .foo:is(.bar).
Instead, with this we'd only check .foo:is(.bar)
once.
Depends on D71422
Assignee | ||
Comment 8•4 years ago
|
||
There are a bunch of missing tests, and there are some tests that don't
match the current spec text, that I need to write or fix before I enable the
feature everywhere.
But there are fairly complex invalidation tests, that we pass flawlessly :)
Depends on D71423
Updated•4 years ago
|
https://developer.mozilla.org/en-US/docs/Web/CSS/:is#Avoiding_selector_list_invalidation
This feature in the documentation does not seem to be implemented
Assignee | ||
Comment 10•4 years ago
|
||
(In reply to 709922234 from comment #9)
That is not in https://drafts.csswg.org/selectors/, other than in an issue, as far as I can tell. Not clear how should we serialize :is
with an empty list. Should writing :is()
be valid?
Assignee | ||
Comment 12•4 years ago
|
||
This way, something like:
*:where(.foo, .bar)
Will end up twice on the selector map, just as if you would've written
.foo, .bar.
But we're a bit careful to not be wasteful, so:
.foo:where(div, span)
Will still end up using the .foo bucket.
It needs a bit of borrow-checker gymnastics to avoid cloning the entry
in the common path. It's a bit gross but not too terrible I think.
Depends on D71424
Assignee | ||
Comment 13•4 years ago
|
||
We can only collect hashes from single-length selectors, for obvious
reasons.
Depends on D71457
Updated•4 years ago
|
Updated•4 years ago
|
Updated•4 years ago
|
Comment 14•4 years ago
|
||
Pushed by ealvarez@mozilla.com: https://hg.mozilla.org/integration/autoland/rev/ebef9346b5b1 Keep track of nested dependencies for :where() and :is(). r=heycam,boris https://hg.mozilla.org/integration/autoland/rev/6398c8f1b4d4 Make Invalidation work in terms of a dependency, not a selector. r=heycam,boris https://hg.mozilla.org/integration/autoland/rev/f572e241c626 Optimize invalidation by scanning the rightmost compound inside :where() and :is() with the outer visitor. r=heycam,boris https://hg.mozilla.org/integration/autoland/rev/0abf5d38ab61 Enable the feature in Nightly. r=heycam,boris https://hg.mozilla.org/integration/autoland/rev/859910d9fee2 Handle disjoint selectors in the selector map. r=heycam,boris https://hg.mozilla.org/integration/autoland/rev/0de514478e3c Collect ancestor hashes from single-length :is and :where selector lists. r=heycam,boris
Comment 15•4 years ago
|
||
Failure logs: https://treeherder.mozilla.org/logviewer.html#?job_id=299168652&repo=autoland
https://treeherder.mozilla.org/logviewer.html#?job_id=299170392&repo=autoland
https://treeherder.mozilla.org/logviewer.html#?job_id=299170400&repo=autoland
Backout link: https://hg.mozilla.org/integration/autoland/rev/9429eb59e262fa6e32c42f9a637474377c56041d
Comment 16•4 years ago
|
||
Pushed by csabou@mozilla.com: https://hg.mozilla.org/integration/autoland/rev/979dcbf7da31 Keep track of nested dependencies for :where() and :is(). r=heycam,boris https://hg.mozilla.org/integration/autoland/rev/95e39fd51ace Make Invalidation work in terms of a dependency, not a selector. r=heycam,boris https://hg.mozilla.org/integration/autoland/rev/18e9d0eb2beb Optimize invalidation by scanning the rightmost compound inside :where() and :is() with the outer visitor. r=heycam,boris https://hg.mozilla.org/integration/autoland/rev/58b044f3fda8 Enable the feature in Nightly. r=heycam,boris https://hg.mozilla.org/integration/autoland/rev/e6cf4f05fff1 Handle disjoint selectors in the selector map. r=heycam,boris https://hg.mozilla.org/integration/autoland/rev/f3686ddab414 Collect ancestor hashes from single-length :is and :where selector lists. r=heycam,boris
Comment 18•4 years ago
|
||
bugherder |
https://hg.mozilla.org/mozilla-central/rev/979dcbf7da31
https://hg.mozilla.org/mozilla-central/rev/95e39fd51ace
https://hg.mozilla.org/mozilla-central/rev/18e9d0eb2beb
https://hg.mozilla.org/mozilla-central/rev/58b044f3fda8
https://hg.mozilla.org/mozilla-central/rev/e6cf4f05fff1
https://hg.mozilla.org/mozilla-central/rev/f3686ddab414
Comment 19•4 years ago
|
||
MDN documentation updated accordingly; see https://github.com/mdn/sprints/issues/3190#issuecomment-630280034 for the specifics.
Let me know if you need anything else here; a review would be nice. Thanks!
Description
•