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-ui] select:hover and select:active styles #11185

Open
josepharhar opened this issue Nov 12, 2024 · 45 comments
Open

[css-ui] select:hover and select:active styles #11185

josepharhar opened this issue Nov 12, 2024 · 45 comments
Labels
Agenda+ Async Resolution: Proposed Candidate for auto-resolve with stated time limit css-forms-1 selectors-4 Current Work

Comments

@josepharhar
Copy link
Contributor

In this issue for customizable select colors, there are proposed UA style rules for select:hover and select:active. However, these rules are also applying when clicking and hovering inside the select's popover.

I think that we should make select:hover and select:active not match when the picker is being hovered or activated.

select_hover

@nt1m @fantasai

@josepharhar
Copy link
Contributor Author

@dbaron

@mfreed7
Copy link
Contributor

mfreed7 commented Dec 3, 2024

This is a more general issue, it seems to me, with top layer elements. E.g.

  <button popovertarget=foo>Open Popover
    <div id=foo popover>Popover</div>
  </button>

In this case, hovering/activating the popover will trigger :hover and :active styles on the button itself. That feels weird to me for any top layer element, including dialogs. Perhaps we should just add a bit to the spec that hovering or activating top layer elements don't trigger those styles on containing elements? That would feel like a good fix - I don't know of any good use cases for this. And if there are any, they could still be achieved like

button:has([popover]:hover) { /* hover styles for button */ }

@mfreed7
Copy link
Contributor

mfreed7 commented Dec 3, 2024

Side note: this is peripherally related to whatwg/html#10770, which is also about nesting popovers inside buttons.

@mfreed7
Copy link
Contributor

mfreed7 commented Dec 12, 2024

Agenda+ to discuss exempting top layer elements from :hover's and :active's "descendants in the flat tree" clause.

The current spec for :hover says:

An element also matches :hover if one of its descendants in the flat tree (including non-element nodes, such as text nodes) matches the above conditions.

The spec for :active says:

An element also matches :active if one of its descendants in the flat tree (including non-element nodes, such as text nodes) matches the above conditions.

I propose, for both, to change to:

...if one of its descendants in the flat tree (including non-element nodes, such as text nodes) matches the above conditions, as long as the element and its descendant have the same ancestor element within the top layer, or neither the element nor its descendant are descendants of any element in the top layer.

...or similar.

@dbaron
Copy link
Member

dbaron commented Dec 12, 2024

I was tempted to suggest a rewording like:

An element E also matches :hover if one of its descendants D in the flat tree (including non-element nodes, such as text nodes) matches the above conditions, and both D and E are in the same top layer.

to fix both the case where the descendant itself is an element in the top layer, and the case where the element is in the top layer but the descendant is nested within another element in the top layer... but then I realized that it's still not right because the "top layer root" of an element is an ancestor of that element, not an ancestor-or-self. (Though maybe that's a mistake?) Also, the definitions (unnecessarily) apply only to elements and not to nodes.

so instead, how about a rewording as:

An element E also matches :hover if one of its descendants D in the flat tree (including non-element nodes, such as text nodes) matches the above conditions, and neither D, nor any of D's flat tree ancestors that are flat tree descendants of E, is in the top layer.

@dbaron
Copy link
Member

dbaron commented Dec 12, 2024

One other note: I don't think this makes :hover any more cyclic than :hover already is. We already have a mechanism for breaking those cycles (for example, when :hover styles change layout in a way that changes whether the element is under the mouse pointer). That mechanism may not be clearly defined, but it exists.

@mfreed7
Copy link
Contributor

mfreed7 commented Dec 12, 2024

After a decent amount of private back and forth with @dbaron, I think I agree with the proposed wording. Perhaps it was only me that missed a few things, but just in case, here are some notes:

An element E also matches :hover if one of its descendants D in the flat tree (including non-element nodes, such as text nodes) matches the above conditions,

Same wording as existing spec, but with D and E defined.

and neither D, nor any of D's flat tree ancestors that are flat tree descendants of E, is in the top layer.

  • An element is "in the top layer" if it is one of the elements in the top layer set. Note that a descendant of a top layer element is therefore not automatically "in the top layer", unless it is separately placed there (e.g. nested popovers).
  • "top layer root" is defined, perhaps unfortunately, as a non-inclusive ancestor that is in the top layer. So each of the elements in the top layer doesn't have itself as its top layer root. (Note that "ancestor" isn't linked in CSS to a definition, so I'm using DOM's definition of the term.)
  • "in the same top layer" means two elements share the same top layer root.
  • The above two mean that if E is an element in the top layer, then "E isn't in the same top layer as E".
  • @dbaron's proposed wording gets around this by explicitly checking ancestors and descendants to make sure D is contained within the nearest inclusive top layer root.

I think perhaps it might be a better idea to try to fix up at least the non-inclusiveness of "top layer root"?

@css-meeting-bot
Copy link
Member

The CSS Working Group just discussed [css-ui] select:hover and select:active styles.

The full IRC log of that discussion <noamr> dbaron: the issue came up from customizable select
<noamr> dbaron: look at the screen capture in the issue
<noamr> dbaron: I believe the issue is showing with the default UA styles for customizable select
<noamr> dbaron: whether or not it should be part of the UA styles is separate
<noamr> dbaron: regardless of the default UA styles, these would be custom styles people would want to write for customizable select and others
<noamr> dbaron: the problem is that :hover and :active are hierarchical
<noamr> dbaron: where this shows with customizable select, is that if you hover an option in the popup of the select, it makes the customizable select "hover"
<noamr> dbaron: CSS can't distinguish between "the select is being hovered" and "something in the select is being hovered, e.g. a popup"
<noamr> dbaron: masonf suggested that we break the hierarchical nature of :hover/:active for the top layer
<noamr> dbaron: putting something in the top layer is a strong indication that you probably don't want the hierarchical hover/select behavior
<noamr> dbaron: welcome to chime in on how to word it, but less important for the call
<noamr> dbaron: I want to get consensus that this is a reasonable direction
<JakeA> Seems reasonable
<ydaniv> +1
<noamr> astearns: just hover and active? Or other hierarchical pseudos?
<joshtumath> +1 to making an exception for top-layer
<JakeA> focus?
<noamr> dbaron: I think it's just :hover and :active? Not sure about :focus-within
<noamr> dbaron: Haven't thought deeply about :focus-within, maybe not.
<noamr> masonf: makes more sense to keep current behavior for :focus-within
<JakeA> q+
<dholbert> q+
<noamr> fantasai: :focus-within is sometimes used specifically for this, e.g. that the focus is within the popup, so would not change it
<noamr> astearns: if we make this change, can we somehow enable the current hierarchical behavior?
<miriam> :hover:not(:has(:hover))
<noamr> dbaron: you could do it with :has
<noamr> dbaron: doable, but the vast majority case here is what we propose
<noamr> masonf: +1, it's the most common case
<astearns> ack JakeA
<noamr> JakeA: would the same happen for JS events related to hover/
<ydaniv> q+
<noamr> dbaron: I don't think we will currently be proposing this
<noamr> dbaron: not proposing DOM event changes
<vmpstr> q+
<JakeA> q+
<noamr> masonf: +1, in CSS this is confusing, but in JS changing bubbling in this way would be confusing
<astearns> ack dholbert
<noamr> dholbert: one use of :hover is to show which a element would be activated
<noamr> dholbert: would that change that behavior?
<noamr> dbaron: probably true. It's probably a bad idea to put interactive content inside an A element.
<astearns> ack ydaniv
<noamr> noamr: recursive interactive elements are against ARIA guideliens
<noamr> ydaniv: this is the default behavior for menus, working as we expected. So this would be breaking menus
<noamr> dbaron: there is a q of whether menus are in the top layer?
<noamr> masonf: It depends on how you construct the DOM tree to build the menu
<noamr> masonf: the prev example does do exactly that - you can currently activate a link from within the top layer
<noamr> ydaniv: I think people rely on the current hover behavior
<noamr> masonf: It's still possible to do that
<noamr> masonf: are you saying there might be a compat issue?
<noamr> ydaniv: yes
<noamr> masonf: need to explore compat
<astearns> ack vmpstr
<noamr> vmpstr: in carousel scroll-marker/group have the same problem, as when items are hovered the element is hovered. there is no top layer there. perhaps the solution is not about top-layer
<kizu> q+
<astearns> ack JakeA
<noamr> JakeA: perhaps a CSS property that creates a boundary for active/hover etc?
<noamr> JakeA: that can be in the UA stylesheet
<noamr> q+
<masonf> q+
<noamr> vmpstr: that would work for my use case
<astearns> ack kizu
<noamr> kizu: I think a CSS property might be dangerous, we try not to create loops
<noamr> kizu: maybe an HTML attribute?
<noamr> kizu: like enabling it by default for select and not other elements?
<JakeA> good point about the loop. It's always the loop
<bramus> scribe+
<astearns> ack noamr
<bramus> noamr: perhaps we can use overflow for this?
<bramus> … if an el is hovered and has an area outside of its normal overflow and that is hovered, then the element itself is probably not hovered
<bramus> … not going to help people relying on it today, but better than relyigng on top layer
<bramus> … not sure
<bramus> q+
<noamr> dbaron: that might get too many other cases where we want the hierarchical behavior
<astearns> ack masonf
<noamr> masonf: I really like the idea of a CSS property
<noamr> masonf: an attribute can be a lot cleaner
<astearns> q+
<noamr> vmpstr: should be CSS, because it's pseudo-elements
<noamr> dbaron: I think we already have solutions for loops for hover/active
<noamr> dbaron: we already break loops for hover/active
<noamr> dbaron: as long as we don't also touch other things like focus within
<noamr> masonf: how does it break the loop?
<noamr> dbaron: we don't have spec definitions/interop, but we break loops. I think we update it only once for refresh cycles
<noamr> kizu: in Safari/firefox it doesn't exactly work
<noamr> dbaron: hover/active already fully have this problem
<astearns> ack bramus
<noamr> bramus: would this also apply to regular select, or only customizable select?
<bramus> https://codepen.io/bramus/pen/GgKWmVg/6a7fa40ecea75e5f07e423d32cc07a7f
<noamr> masonf: the old style select doesn't set hover
<noamr> bramus: it does, see demo ^^^
<noamr> bramus: they apply in chrome/safari, not firefox
<noamr> dbaron: I wouldn't be surprised if it's OS specific as well
<noamr> q+
<ydaniv> q+
<noamr> masonf: one key difference is that you can do interesting things with the options, but not here
<noamr> astearns: a bit concerned making special case for top-layer when it catches thing that we might not want to catch, and might not work for non-top-layer things
<noamr> astearns: maybe go back to the issue?
<astearns> ack noamr
<astearns> ack astearns
<bramus> noamr: maybe can be another contain? As in “your hover is contained”. perhaps can do something like that. Need to think about it further.
<astearns> ack ydaniv
<noamr> ydaniv: contain might put us in a loop? Perhaps a new hover-*/active-* sort of things that don't bubble?
<kizu> https://codepen.io/kizu/pen/GgKWEZp — CSS hover loop example, behaves differently in Chrome, Safari, and Firefox (but, well, works)
<noamr> astearns: taking back to the issue
<noamr> 17:04 <astearns> github-bot, take up https://github.com//issues/9141

@ydaniv
Copy link
Contributor

ydaniv commented Dec 18, 2024

Adding what I suggested in the discussion:

Perhaps add a new :hover-*/:active-* that don't bubble? Maybe that would be an easy way out without risking compat issues?

@jakearchibald
Copy link
Contributor

@ydaniv I think the problem is you'd want them to bubble to a point. Otherwise an <img> in an <option> wouldn't trigger hover on the option.

@ydaniv
Copy link
Contributor

ydaniv commented Dec 18, 2024

@jakearchibald sounds like @scopeing, right? So maybe that should be the place to look for a solution?

@mfreed7
Copy link
Contributor

mfreed7 commented Dec 18, 2024

Thanks for the great ideas in the discussion. It sounds like there are roughly four options on the table:

  1. Top layer elements "break" the hover/active "bubbling" behavior (as described)
  2. Mint a new CSS property (or new value for e.g. contain) that breaks the hover/active bubbling
  3. Mint a new HTML attribute that breaks the hover/active bubbling
  4. Create new CSS properties, like :hover-on that don't bubble at all

Briefly listing pros/cons:

Option #1 (break at top layer):

  • Pros:
    • Solves for customizable-<select>
    • Likely works as expected in most common cases
  • Cons:
    • Might be web compat issues, particularly for nested menus constructed this way today
    • Does not work for non-top-layer use cases

Option #2 (new CSS property):

  • Pros:
    • Solves customizable-<select>
    • Works for pseudo elements
    • Does not have web compat issues
  • Cons:
    • Might need special care to avoid loops. (:hover/:active already have this issue anyway, so maybe ok)

Option #3 (new HTML attribute):

  • Pros:
    • Solves customizable-<select>
    • Does not have web compat issues
  • Cons:
    • Does not work for pseudo elements

Option #4 (new, non-bubbling :hover-*, etc):

  • Pros:
    • Solves customizable-<select> (with a decently more complicated UA stylesheet)
    • Does not have web compat issues
  • Cons:
    • Significantly more difficult to use. E.g. typical desire is for :hover to match all descendants.

@jakearchibald
Copy link
Contributor

Option #3 (new HTML attribute):

  • Pros:

    • Solves customizable-<select>

Does it? Aren't you wanting to set the boundary at a pseudo element?

@mfreed7
Copy link
Contributor

mfreed7 commented Dec 18, 2024

Option #3 (new HTML attribute):

  • Pros:

    • Solves customizable-<select>

Does it? Aren't you wanting to set the boundary at a pseudo element?

It does - we want to break the boundary at a shadow DOM element (the backing element for ::picker(select)), which we could add an attribute to. But it doesn't solve the carousel pseudo elements use case.

@ydaniv
Copy link
Contributor

ydaniv commented Dec 18, 2024

Cons:
Significantly more difficult to use. E.g. typical desire is for :hover to match all descendants.

The idea is that it doesn't bubble - as in upwards, so it won't affect ancestors. See demo

The :hover doesn't trickle down, so no issue there.

@dbaron
Copy link
Member

dbaron commented Dec 18, 2024

I think the problem with option 4 is that you get different div:hover-new behavior for <div>This is text.</div> and for <div>This <span>is text</span>.</div>... you'll fail to get the :hover-new when the pointer is inside the <span>.

And we don't want to solve this by using the div's rectangle, because we actually don't want the hover effect if the popup partially occludes the div and the pointer is in the popup but inside the div's rectangle.

@nt1m
Copy link
Member

nt1m commented Dec 18, 2024

A slight variation of 4 would be a selector for :hover(in-page) (name to bikeshed) or where descendants are still matched but top-layer descendants excluded.

My preference would be for 1. or what I just described. An opt-in attribute/CSS property would be quite difficult to understand for the majority of cases I think.

@astearns astearns removed the Agenda+ label Dec 18, 2024
@ydaniv
Copy link
Contributor

ydaniv commented Dec 18, 2024

Another option of scoping 4 could be something like :hover-closest(<selector>) bubbling until first match of <selector>.

@mfreed7
Copy link
Contributor

mfreed7 commented Dec 18, 2024

My preference would be for 1. or what I just described. An opt-in attribute/CSS property would be quite difficult to understand for the majority of cases I think.

select::picker(select) {
  hover-propagation: stop;
}

seems relatively easy to understand, doesn't it? It goes right on the "border" element where bubbling should stop. That's in contrast to the proposed variations for #4 where you have to apply a property to an entire sub-tree, minus a "donut" of that sub-tree.

I'm obviously "ok" with option #1 also, but it doesn't address some of the non-top-layer use cases that were raised in the meeting, like carousel pseudo elements.

@astearns
Copy link
Member

For completeness, is there another option to do nothing yet and rely on a more complex :has selector to accomplish the behavior we want in the UA style rules (with the con of forcing authors to use the more complex selector if they want to override things)?

@nt1m
Copy link
Member

nt1m commented Dec 18, 2024

For completeness, is there another option to do nothing yet and rely on a more complex :has selector to accomplish the behavior we want in the UA style rules (with the con of forcing authors to use the more complex selector if they want to override things)?

Authors will likely want to do this too without some complex :has()/:not() logic.

select::picker(select) {
hover-propagation: stop;
}
seems relatively easy to understand, doesn't it? It goes right on the "border" element where bubbling should stop. That's in contrast to the proposed variations for #4 where you have to apply a property to an entire sub-tree, minus a "donut" of that sub-tree.

I think it raises more questions than answers. A big one is: does it affect JS mouse events? It's not necessarily obvious from the name.

If we were to go this route, maybe something like pointer-events: contain makes more sense? It would end up affecting everything that relates to hit-testing including JS events (like other pointer-events values work)

@jakearchibald
Copy link
Contributor

select::picker(select) {
  hover-propagation: stop;
}

fwiw, I was imagining something like:

select::picker(select) {
  stop-propagation: hover active focus-within;
}

…so you could pick individual things.

does it affect JS mouse events?

Folks in the meeting didn't think it should, and I'm ok with that. However, if folks decide it should impact related JS events, it's important that it only prevents propagation in the bubbling phase. The capturing phase should be left as-is.

@mfreed7
Copy link
Contributor

mfreed7 commented Jan 10, 2025

fwiw, I was imagining something like:

select::picker(select) {
  stop-propagation: hover active focus-within;
}

…so you could pick individual things.

I think I like that better too - it is more descriptive of what happens, and for what actions. So +1 to this proposal.

does it affect JS mouse events?

Folks in the meeting didn't think it should, and I'm ok with that. However, if folks decide it should impact related JS events, it's important that it only prevents propagation in the bubbling phase. The capturing phase should be left as-is.

Agreed - it seems like JS is a separate, and more complicated thing, and changing event propagation via CSS sounds like a footgun. Having said that, does this impact the name somehow? I.e. instead of stop-propagation which almost implies event bubbling, perhaps something like blocks: hover or something that indicates it "blocks" the named thing(s) from going above this point? Naming is hard.

@nt1m
Copy link
Member

nt1m commented Jan 10, 2025

select::picker(select) {
  stop-propagation: hover active focus-within;
}

I'm not too sure about this. This words it very much in terms of pseudo-classes, and that delineation doesn't necessarily make sense for the use cases here. E.g. you might want :focus-within to keep behaving the same for querySelector() for the purposes of DOM queries, but prevent the propagation when using it for styling.

Hence my preference for re-using primitives like pointer-events, I know it's challenging that hit-testing is not well-defined, but I do think pointer-events: contain feels like a natural fit in the mental model of pointer-events: none/auto/etc..

(I'm not set in stone with this proposal, but that's the best one I have so far, I'd be open to other ideas, especially if they re-use pre-existing primitives)

@jakearchibald
Copy link
Contributor

E.g. you might want :focus-within to keep behaving the same for querySelector() for the purposes of DOM queries, but prevent the propagation when using it for styling.

That's what I was intending with my proposal. It only impacts CSS, not JS. I guess the naming I went with didn't make that clear. I'm sure there's better names out there.

Hence my preference for re-using primitives like pointer-events

The problem is :active and :focus-within are not limited to pointers, they can happen through keyboard interaction. pointer-events is about hit testing, whereas we're talking about propagation of particular pseudo states that aren't limited to pointers.

I'm not tied to my proposal either, but my aim was to provide a property that lets you create a CSS propagation boundary for particular pseudo classes. I don't think it should impact JS events.

Would your proposal impact JS events? I worry that would recreate the issue we see with iframes when it comes to things like drag & drop.

@nt1m
Copy link
Member

nt1m commented Jan 10, 2025

E.g. you might want :focus-within to keep behaving the same for querySelector() for the purposes of DOM queries, but prevent the propagation when using it for styling.

That's what I was intending with my proposal. It only impacts CSS, not JS. I guess the naming I went with didn't make that clear. I'm sure there's better names out there.

I think the issue isn't just about the name, as soon as you mention CSS selectors directly, people (reasonably) expect the result to be consistent across CSS/JS/DOM (e.g. .matches() / .querySelector() / hit-testing / etc.), or at least the question will cross their mind. I doubt any name will be clear enough without it being too verbose.

I also don't know how :has(...) would behave or how easy / sensical / predictable it would be to just change :has() for some APIs but not others.

I'd rather the control to be at "customer facing feature" level than be split at CSS-selector level, such as user-select: contain (this already exists fwiw) / pointer-events: contain / etc. I think it makes the result a lot more understandable.

Hence my preference for re-using primitives like pointer-events

The problem is :active and :focus-within are not limited to pointers, they can happen through keyboard interaction. pointer-events is about hit testing, whereas we're talking about propagation of particular pseudo states that aren't limited to pointers.

That's a problem to solve for pointer-events: none and friends too right? In the majority of cases, you would not want UI that has pointer-events: none to be tabbable/focusable through. If there's a problem with existing primitives, I'd rather have them addressed in some way rather than completely dodged.

This somewhat reminds me of the brand new interactivity: inert, there may be some mental model where a new value addresses this use case without needing to introduce yet another CSS property for a similar use-case. cc @flackr @emilio

I'm not tied to my proposal either, but my aim was to provide a property that lets you create a CSS propagation boundary for particular pseudo classes. I don't think it should impact JS events.

Would your proposal impact JS events? I worry that would recreate the issue we see with iframes when it comes to things like drag & drop.

I am personally the type of person who thinks in terms of customer features, so the division between CSS and JS feels somewhat arbitrary to me (to me these are just different tools, not an end result).

It's possible to design specific values like pointer-events: contain-style, but I'm really not convinced this is good API design.

@jakearchibald
Copy link
Contributor

The problem is :active and :focus-within are not limited to pointers, they can happen through keyboard interaction. pointer-events is about hit testing, whereas we're talking about propagation of particular pseudo states that aren't limited to pointers.

That's a problem to solve for pointer-events: none and friends too right? In the majority of cases, you would not want UI that has pointer-events: none to be tabbable/focusable through. If there's a problem with existing primitives, I'd rather have them addressed in some way rather than completely dodged.

I think you'd struggle to make pointer-events about something more than pointers in a backwards compatible way. Also, if you do that, the feature becomes a real mess. For staters, the naming really sucks - the word 'pointer' is right there.

the division between CSS and JS feels somewhat arbitrary to me

It is, absolutely. But as things currently stand, you can't impact :hover by preventing/stopping JS events. I think you'd be giving developers a lot of pain if you decide that this particular feature does impact JS, when similar features do not.

That's why my instinct was to create CSS containment of particular CSS features.

I also don't know how :has(...) would behave

I think that's pretty straight forward. el:has(:hover) would match if any descendant matches :hover. The containment would only prevent :hover applying directly to elements outside the containment.

@emilio
Copy link
Collaborator

emilio commented Jan 10, 2025

Don't we have precedents for this via modal dialogs / fullscreen? Having a modal dialog open or a fullscreen element turns the rest of the page inert. We might want the same to happen for a <select> popup? That'd contain focus and pointer events as well, right?

@emilio
Copy link
Collaborator

emilio commented Jan 10, 2025

I guess that leaves the issue of the hover / active / focus-within chain probably still propagating to the <select>...

But that I think is an issue that should be fixed for all top layer elements IMO? If we don't somehow make it the default (why not? are there use cases for this?), it still doesn't seem like css would be the right place to put that behavior...

Maybe it should be argument of whatever triggers the top layer element? Something like showPopover({ constrainHover: true, constrainFocus: true, constrainActive: true }) (or some more general / better naming)

@emilio
Copy link
Collaborator

emilio commented Jan 10, 2025

Or maybe inert elements should stop the :hover / :active / :focus-within chain. But I haven't thought too much about that...

@nt1m
Copy link
Member

nt1m commented Jan 10, 2025

I think you'd struggle to make pointer-events about something more than pointers in a backwards compatible way. Also, if you do that, the feature becomes a real mess. For staters, the naming really sucks - the word 'pointer' is right there.

That's not my suggestion fwiw. I was just saying we should approach the problem differently and think about existing primitives (top layer, inert, pointer-events, etc.). If a new primitive is added, I'd at least expect it to fit in the current patterns (e.g. contain/none/auto type values, rather than mentioning specific CSS selectors).

But that I think is an issue that should be fixed for all top layer elements IMO? If we don't somehow make it the default (why not? are there use cases for this?)

I wouldn't be opposed to this personally. Top layer sort of conceptually breaks the propagation by laying out everything as a sibling of the root.

@jakearchibald
Copy link
Contributor

Fwiw, I think you're getting hung up on it being the same name as a pseduo-class. Hover is a feature. The pseduo-class has that name because it's the name of the feature.

@mfreed7
Copy link
Contributor

mfreed7 commented Jan 10, 2025

There are kind of two different conversations happening here. One is about the best name(s) to choose for this feature, to make it clear what's happening. While that's an important question, the other one is about the actual behavior. Maybe we can decide the behavior question first, and then find the best name for it?

The questions I see:

  1. Should this new thing affect Javascript event propagation? My view (and it sound like the majority view?) is that this new thing should only affect selector matching and not event propagation, which is a different thing from how the selectors are defined, and just "happens" to also use the flat tree.
  2. How does this new feature affect things like querySelector(), matches(), and :has()? I think those seem clear - this new feature affects selector matching, and it should therefore "just work" in the same way via e.g. .matches(':hover') as it does in a stylesheet with :hover {}.
  3. What CSS properties are "in play" for this new feature? This should be contained to just those properties that are defined with "an element also matches :pseudoclass if one of its descendants in the flat tree matches". That sounds like only these three? :hover, :active, :focus-within. Are there others?
  4. Should the "containment" of these properties happen via a new CSS property or value, or should it happen at the top layer boundary? I'm personally happy with either solution (those are options 2 and 1, respectively, from this comment). But option 2 feels like a more general solution to me, which gives the developer more control so they can do the right thing.

Don't we have precedents for this via modal dialogs / fullscreen? Having a modal dialog open or a fullscreen element turns the rest of the page inert. We might want the same to happen for a <select> popup? That'd contain focus and pointer events as well, right?

I think this would be a big mistake. Popovers are not modal, and therefore don't inert the page. For good reason - they're not intended to behave modally. I.e. while you have a <select> picker open, you can happily go hover some other element to get a tooltip to show up. Or click on a button elsewhere on the page, which light-dismisses the picker.

@jakearchibald
Copy link
Contributor

jakearchibald commented Jan 13, 2025

  1. Should this new thing affect Javascript event propagation? My view (and it sound like the majority view?) is that this new thing should only affect selector matching and not event propagation, which is a different thing from how the selectors are defined, and just "happens" to also use the flat tree.

JS doesn't have the concept of 'hover' or 'active'. It has some related events, like mouseover, but preventing those, or preventing their propagation, doesn't impact the CSS concept of 'hover'.

JS does have the concept of 'focus', and preventing that does impact :focus-within. However, preventing focus event propagation has no impact on :focus-within.

I think the simplest way to describe this feature is: It's a barrier to the propagation of :hover, :active, :focus-within.

There's no current link between the propagation of these things and the propagation of JS events. I don't recall this ever being an issue for developers. I certainly haven't experienced it as an issue. I think it would be more of an issue to change that now.

  1. How does this new feature affect things like querySelector(), matches(), and :has()? I think those seem clear - this new feature affects selector matching, and it should therefore "just work" in the same way via e.g. .matches(':hover') as it does in a stylesheet with :hover {}.

select:hover and select:is(:hover) mean "match elements in the :hover state that are selects". So, if propagation of :hover is prevented by a child, and that element or one of its descendants is hovered, select:hover and select:is(:hover) would not match.

The above is similar to "did the select receive a mouseover event?", when a child element stops propagation.

select:has(:hover) means roughly "match select elements that contain an element that matches :hover", so this would bypass any propagation prevention.

The above is similar to "did an element within the select receive a mouseover event?", when a child element stops propagation. It doesn't matter, the inner element still received the event.

  1. What CSS properties are "in play" for this new feature? This should be contained to just those properties that are defined with "an element also matches :pseudoclass if one of its descendants in the flat tree matches". That sounds like only these three? :hover, :active, :focus-within. Are there others?

There's :target-within.

  1. Should the "containment" of these properties happen via a new CSS property or value, or should it happen at the top layer boundary? I'm personally happy with either solution (those are options 2 and 1, respectively, from this comment). But option 2 feels like a more general solution to me, which gives the developer more control so they can do the right thing.

Agreed. The top-layer solution might be a good fallback if consensus can't be reached on a general solution.

Don't we have precedents for this via modal dialogs / fullscreen? Having a modal dialog open or a fullscreen element turns the rest of the page inert. We might want the same to happen for a <select> popup? That'd contain focus and pointer events as well, right?

I think this would be a big mistake. Popovers are not modal, and therefore don't inert the page. For good reason - they're not intended to behave modally. I.e. while you have a <select> picker open, you can happily go hover some other element to get a tooltip to show up. Or click on a button elsewhere on the page, which light-dismisses the picker.

Agreed. I don't think this should be used as an opportunity to change a bunch of long-standing and expected behaviours on the platform. The changes in behaviour here should be opt-in.

@mfreed7
Copy link
Contributor

mfreed7 commented Jan 14, 2025

+1 to everything you said above. And thanks for pointing out :target-within, which looks like it should be added to the list. Though that doesn't appear to be shipped on any browser yet?

@mfreed7
Copy link
Contributor

mfreed7 commented Jan 21, 2025

I asked to have this on the agenda last week, but the CSSWG agenda is crowded. Perhaps we've reached something like a consensus here? At least on the behavior question, perhaps? Modulo the naming question (which will need to come next), I propose this async resolution:

Proposed resolution: Add a CSS property or value (TBD) that affects the behavior of :hover, :active,
:focus-within, and :target-within, such that those pseudo classes no longer match box tree
descendants of an element with the new property/value. This will not affect JS event propagation
behavior. 

Comments? Objections?

@mfreed7 mfreed7 added the Async Resolution: Proposed Candidate for auto-resolve with stated time limit label Jan 21, 2025
@nt1m
Copy link
Member

nt1m commented Jan 21, 2025

I'd like to object to that. It's just generally strange to have properties affect selector matching directly, and a lot of questions to resolve around it (querySelector/matches/etc.). Also, circularity is also directly an issue:

:hover > foo {
    stop-propagation: hover;
}

I'd be more in favor in what Emilio proposed of applying this behavior automatically for top layer elements, given they somewhat behave like detached elements from the DOM in some sense. It would also solve for other popover/modal/fullscreen, where preventing this propagation sounds desirable.

I also don't think it necessarily blocks from such extension from being added in the future if someone comes back with a more solid proposal.

@mfreed7
Copy link
Contributor

mfreed7 commented Jan 21, 2025

I'd like to object to that.

Ok, thanks for the comment!

It's just generally strange to have properties affect selector matching directly, and a lot of questions to resolve around it (querySelector/matches/etc.).

Can you clarify the questions? It seems to me like querySelector() and matches() just match in exactly the same way that a stylesheet selector would match.

Also, circularity is also directly an issue:

:hover > foo {
    stop-propagation: hover;
}

This was discussed last time we talked about this issue live in CSSWG. The same issues are present for :hover already. E.g.

:hover {
  top:1000px;
}

Though admittedly this is a bit different. But the point is that things like :hover and :active already have problems like this.

I'd be more in favor in what Emilio proposed of applying this behavior automatically for top layer elements, given they somewhat behave like detached elements from the DOM in some sense. It would also solve for other popover/modal/fullscreen, where preventing this propagation sounds desirable.

I also don't think it necessarily blocks from such extension from being added in the future if someone comes back with a more solid proposal.

I'm ok with this also, it just feels less flexible. But it does solve the OP use case. And it nicely doesn't have the circularity problem you raised.

@annevk
Copy link
Member

annevk commented Jan 22, 2025

It seems to me like querySelector() and matches() just match in exactly the same way that a stylesheet selector would match.

Wouldn't that make them depend on layout/style happening? That seems pretty bad and a major change in behavior. Maybe I'm misunderstanding what you mean?

@mfreed7
Copy link
Contributor

mfreed7 commented Jan 22, 2025

It seems to me like querySelector() and matches() just match in exactly the same way that a stylesheet selector would match.

Wouldn't that make them depend on layout/style happening? That seems pretty bad and a major change in behavior. Maybe I'm misunderstanding what you mean?

Oh, I see what you're saying. Yes, if this is defined as a new CSS property, then selectors (whether in a stylesheet or in a call to querySelector) will need to run style on descendent elements in order to determine whether :hover matches the ancestor. I think my confusion with the question is that this has nothing to do with JS in particular, does it? I'm also confused because we already have to run style/layout on descendants, since these pseudo classes are already defined to match "if one of its descendants in the flat tree matches". Right? I'm not sure how this proposal changes that.

Anyway, based on the feedback, here's a revised proposed resolution:

Proposed resolution: For an element E being matched by :hover, :active, :focus-within, or
:target-within, flat tree descendants D that match the pseudo-class, but that are in the top
layer (at a later position in the top layer stack, if E is also in the top layer) do not cause E
to match the selector. In this case, all flat tree descendants of D also do not cause E to
match the selector.

Sound better? The wording is tricky, but hopefully the above captures the important bits.

@astearns astearns moved this to FTF agenda items in CSSWG January 2025 meeting Jan 22, 2025
@astearns astearns moved this from FTF agenda items to Regular agenda items in CSSWG January 2025 meeting Jan 22, 2025
@annevk
Copy link
Member

annevk commented Jan 23, 2025

I'm also confused because we already have to run style/layout on descendants, since these pseudo classes are already defined to match "if one of its descendants in the flat tree matches".

Can the difference actually be tested? (Between flat tree descendants and shadow-including descendants.)

@emilio
Copy link
Collaborator

emilio commented Jan 23, 2025

Yes it can. A <slot> will match with flat tree descendants but not with shadow-including for example.

@emilio
Copy link
Collaborator

emilio commented Jan 23, 2025

For a more concrete example, you can put an empty <slot> with slotted contents in the top layer via popover, and the slot won't have any shadow-including descendants.

@annevk
Copy link
Member

annevk commented Jan 23, 2025

Fair. I guess looking at the flattened tree technically still doesn't require style/layout (though might in implementations), but it's certainly more expensive as you do have to unwrap slots and such. Do we have tests already for that in the non-top-layer case for :hover and friends? If so, repurposing those would be good.

@emilio
Copy link
Collaborator

emilio commented Jan 23, 2025

:hover and :active already use the flat tree, right? You kinda just need to treat top layer elements as their own :hover / :active "root"s

chromium-wpt-export-bot pushed a commit to web-platform-tests/wpt that referenced this issue Jan 23, 2025
This patch implements a fix while we wait for a resolution here:
w3c/csswg-drafts#11185

Bug: 389830175
Change-Id: Ie0fd5d655cc5ac83f68fb0da0cfd2c7e5de49214
aarongable pushed a commit to chromium/chromium that referenced this issue Jan 24, 2025
This patch implements a fix while we wait for a resolution here:
w3c/csswg-drafts#11185

Bug: 389830175
Change-Id: Ie0fd5d655cc5ac83f68fb0da0cfd2c7e5de49214
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6019064
Commit-Queue: Joey Arhar <[email protected]>
Reviewed-by: David Baron <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1410952}
chromium-wpt-export-bot pushed a commit to web-platform-tests/wpt that referenced this issue Jan 24, 2025
This patch implements a fix while we wait for a resolution here:
w3c/csswg-drafts#11185

Bug: 389830175
Change-Id: Ie0fd5d655cc5ac83f68fb0da0cfd2c7e5de49214
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6019064
Commit-Queue: Joey Arhar <[email protected]>
Reviewed-by: David Baron <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1410952}
chromium-wpt-export-bot pushed a commit to web-platform-tests/wpt that referenced this issue Jan 24, 2025
This patch implements a fix while we wait for a resolution here:
w3c/csswg-drafts#11185

Bug: 389830175
Change-Id: Ie0fd5d655cc5ac83f68fb0da0cfd2c7e5de49214
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6019064
Commit-Queue: Joey Arhar <[email protected]>
Reviewed-by: David Baron <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1410952}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Agenda+ Async Resolution: Proposed Candidate for auto-resolve with stated time limit css-forms-1 selectors-4 Current Work
Projects
Status: Regular agenda items
Development

No branches or pull requests