Closed Bug 1509418 Opened 6 years ago Closed 4 years ago

Implement `:is()` and `:where()` selectors

Categories

(Core :: CSS Parsing and Computation, enhancement, P3)

enhancement

Tracking

()

RESOLVED FIXED
mozilla77
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)

`: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
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.)
Flags: needinfo?(emilio)
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)
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.
Flags: needinfo?(emilio)
Priority: -- → P3
Depends on: 1629735

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.

Assignee: nobody → emilio
Status: NEW → ASSIGNED

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

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

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

Blocks: 906353
Summary: Implement the specificity adjustment selector `:where()` → Implement `:is()` and `:where()` selectors
Blocks: 933562

(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?

Blocks: 1631202

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

We can only collect hashes from single-length selectors, for obvious
reasons.

Depends on D71457

Whiteboard: [layout:backlog:77]
Attachment #9141494 - Attachment description: Bug 1509418 - Collect ancestor hashes from single-length :is and :where selectors. r=heycam,#style → Bug 1509418 - Collect ancestor hashes from single-length :is and :where selector list. r=heycam,#style
Attachment #9141494 - Attachment description: Bug 1509418 - Collect ancestor hashes from single-length :is and :where selector list. r=heycam,#style → Bug 1509418 - Collect ancestor hashes from single-length :is and :where selector lists. r=heycam,#style
Blocks: is-where
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
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

Relanded as the failures are not from here.

Flags: needinfo?(emilio)

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!

Regressions: 1829540
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: