Skip to content

feat: Support :hover on touch devices#9720

Open
MatiPl01 wants to merge 8 commits into
mainfrom
@matipl01/css-touch-hover
Open

feat: Support :hover on touch devices#9720
MatiPl01 wants to merge 8 commits into
mainfrom
@matipl01/css-touch-hover

Conversation

@MatiPl01

@MatiPl01 MatiPl01 commented Jun 20, 2026

Copy link
Copy Markdown
Member

Stacked on #9729 (shared press touch listener). The diff here is just the :hover additions on top of it; review/merge #9729 first.

Summary

:hover was a no-op on touchscreens (the pointer recognizers never fire for a finger). This makes it respond to touch with the sticky model the major browser engines use: a tapped view gains :hover and keeps it after the finger lifts, clearing only when a later touch lands elsewhere or on scroll. :active stays press-only and distinct; real-pointer hover (trackpad/mouse/stylus) is unchanged.

Native-only change (the pseudo-state flow is selector-agnostic). On each touch-down a :hover view is hovered when its on-screen bounds contain the point. iOS uses a shared coordinator watching the key window through a passive, non-recognizing gesture recognizer (so it never competes with the :active recognizers); Android recomputes per view plus a Window.Callback observer to catch taps on blank space.

A :hover style needs a non-zero transitionDuration to apply (zero-duration is a pre-existing no-op).

Demo

Tap the box: it turns green and stays green after you lift; tap elsewhere to clear.

@MatiPl01 MatiPl01 self-assigned this Jun 20, 2026
@MatiPl01 MatiPl01 force-pushed the @matipl01/css-touch-hover branch 9 times, most recently from 634132b to 7b95326 Compare June 21, 2026 23:18
@MatiPl01 MatiPl01 changed the base branch from main to @matipl01/share-press-touch-listener June 21, 2026 23:18
MatiPl01 added 2 commits June 22, 2026 03:28
A View exposes a single OnTouchListener slot, so attaching both :active and
:active-deepest to one view made whichever selector registered second overwrite
the first. Funnel both through one shared listener keyed by activeCallbacks and
deepestCallbacks so they coexist.
On a touchscreen the pointer-only recognizers never fire for a finger, so
:hover did nothing. Apply :hover while a finger is pressing a view and clear it
on lift, matching Chromium-based browsers (non-sticky) - the same lifecycle as
:active. Both platforms drive it from the per-view press path that :active
already uses: iOS adds a min-duration UILongPressGestureRecognizer, Android adds
a per-view OnTouchListener shared with :active. Real-pointer hover (trackpad,
mouse, stylus) is unchanged.
@MatiPl01 MatiPl01 force-pushed the @matipl01/share-press-touch-listener branch from e72ae7e to e9e76cd Compare June 22, 2026 01:31
@MatiPl01 MatiPl01 force-pushed the @matipl01/css-touch-hover branch from 7b95326 to 1e20121 Compare June 22, 2026 01:31
Base automatically changed from @matipl01/share-press-touch-listener to main June 22, 2026 11:08
@wisniewskij wisniewskij self-assigned this Jun 23, 2026
@wisniewskij wisniewskij force-pushed the @matipl01/css-touch-hover branch from 2d0f0b5 to ddc6205 Compare June 23, 2026 10:32
…over

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@wisniewskij wisniewskij force-pushed the @matipl01/css-touch-hover branch from ddc6205 to e0690e9 Compare June 23, 2026 11:17
@wisniewskij wisniewskij marked this pull request as ready for review June 23, 2026 11:38

@wisniewskij wisniewskij left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

wisniewskij and others added 4 commits June 23, 2026 18:40
The observer set its state to .failed on touch end/cancel. Because it never sets
.began it never claims a touch, so failing was pointless and made UIKit stop
delivering the rest of a multi-touch sequence to it (losing the scroll-clear slop
and the new-finger recompute). Drop the .failed writes so it stays .possible.
Two parity gaps in the sticky :hover clear paths, each present on one platform
only:
- iOS: removeWindowObserver did not clear hover, so when a modal/alert became the
  key window and the observer rebound, the old window's hover stayed stuck. Clear
  on teardown, mirroring Android.
- Android: the window observer had no ACTION_MOVE handling, so a drag past touch
  slop that the per-view ACTION_CANCEL missed left sticky hover on. Clear on slop,
  mirroring iOS.

Also dedupe the iOS purge-and-teardown into purgeEntries.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants