w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.46k stars 657 forks source link

[css-view-transitions-2] `:hover` doesn't work during a transition. #9586

Open khushalsagar opened 11 months ago

khushalsagar commented 11 months ago

Try this test case: https://codepen.io/emattias/pen/OJrGVEO.

<button id="button">foobar</button>
<style>
    button {
      cursor: pointer;
      background: green;
      transition: all 0.5s ease-out;
    }

    button:hover {
      background: red;
      transition: all 0.5s ease-out;
    }

    ::view-transition {
      pointer-events: none;
    }
</style>
<script>
let state = true
const button = document.querySelector('button')

button.addEventListener('click', () => {
  document.startViewTransition(() => {
    const newText = state ? 'baz' : 'foobar'
    button.innerText = newText;
    state = !state
  })
})
</script>

When we change the DOM for the transition (by adding pseudos), a new hit-test is triggered. If you add pointer-events: none, it ignores the pseudos and runs on the underlying DOM. But if some element is participating in a transition, that will be ignored in hit-testing (by design). So right now :hover can't be active on any DOM element participating in the transition. Relevant spec text : https://drafts.csswg.org/css-view-transitions-1/#:~:text=do%20not%20respond%20to%20hit%2Dtesting.

The goal with not hit-testing DOM elements was to avoid incorrect clicks. The DOM element is not painting where its box is. But we didn't realize that a side-effect of this is that activating hover is delayed until transition end. So if the author is using :hover to style an element, those styles will abruptly apply when the transition is over.

The principled fix for this (credits to @vmpstr) is to route hit-testing from ::view-transition-new to its corresponding DOM element. Its the reverse of what we're conceptually doing with the element's painting (routing it from the DOM element to the VT pseudo).

nt1m commented 10 months ago

This is a weird one. I think :hover should not match if you can't activate the button during the transition, because otherwise it gives the expectation that the user can click it. OTOH, the old capture does include the :hover state so not sure there's a great solution here.

khushalsagar commented 9 months ago

I think :hover should not match if you can't activate the button during the transition, because otherwise it gives the expectation that the user can click it

That's an interesting way to look at it. If you're using hover to style an element to indicate user interaction (like a button which changes color) then yeah you don't want hover to be activated while its being rendered by the pseudo. If we did add a browser capability to route events that hit the pseudo back to the DOM element then it would be ok?

In the short term, authors will need to rely on non-VT ways to animate visual changes from hover (like CSS transitions). Otherwise you will see an abrupt change when the transition ends and hover is activated. But you would have seen that anyway if hover state changes for that element outside of a transition.

bokand commented 9 months ago

I guess the suggestion is that :hover isn't special - so if the hover state is activated, a click will also be captured?

The principled fix for this (credits to @vmpstr) is to route hit-testing from ::view-transition-new to its corresponding DOM element.

I'm not sure simply routing it would work. Consider something like this:

  <div id="container" style="view-transition-name:button">
    <div class="background">
       <button onclick="dosomething()">Foo</button>
    </div>
  </div>

If we route to the v-t-pseudo associated DOM element, we'll dispatch the event at #container so clicks won't hit the button, hover might not be applied to background, etc.

Perhaps we could run a new hit test starting from the associated DOM element so that we hit the correct inner-most element and perform the usual event bubbling/routing?

khushalsagar commented 9 months ago

I guess the suggestion is that :hover isn't special - so if the hover state is activated, a click will also be captured?

+1 to that.

Perhaps we could run a new hit test starting from the associated DOM element so that we hit the correct inner-most element and perform the usual event bubbling/routing?

But what if the inner-most element that the hit test resolves to is itself a named element being rendered somewhere else by its pseudo. VT can bring up interesting cases where an element which is occluded in the actual DOM is not occluded in the pseudo-DOM. So a full hit-test on a DOM subtree will require some thinking to get the stacking right.

bokand commented 9 months ago

That's a good point...definitely complexities to think through. Off hand, I guess we'd want to avoid descending during the hit test through a view-transition-name'd element. That should be ok since, had it actually been hit, we'd have used its view-transition-pseudo to route to the correct DOM subtree.

nt1m commented 9 months ago

I would prefer not pursuing any kind of special hit testing especially given view transitions can end up with different layering than the actual elements underneath, see #9672. Not to mention all the complexities mentioned here.

khushalsagar commented 9 months ago

@nt1m maybe not for :hover but interactivity during transitions is worth thinking about for the use-case highlighted in https://github.com/WICG/view-transitions/issues/157. Being able to interact before the transition animations are over, a userland solution to this problem is able to support that. My hope right now is scoped transitions will solve such use-cases and we should revisit once a firm design for that is in place.