Commit 29dfe48
authored
fix(desktop): don't gate hover affordances on the hover media query (#3657)
## What problem this solves
Tailwind v4 compiles every `hover:` variant inside `@media (hover:
hover)`. Some
Windows hosts answer that query `false` **even with a mouse attached**,
and then
every hover-revealed control in the app is permanently `visibility:
hidden`.
Measured in the app's own WebView2 devtools console, on a mouse-driven
Windows 11
desktop:
```js
matchMedia('(hover: hover)').matches // false
matchMedia('(any-hover: hover)').matches // false
matchMedia('(pointer: fine)').matches // false
matchMedia('(any-pointer: fine)').matches // false
navigator.maxTouchPoints // 10
```
Windows itself, on the same machine at the same moment, reports a mouse
present
and an integrated digitizer:
```
GetSystemMetrics(SM_DIGITIZER) = 197 // INTEGRATED_TOUCH | INTEGRATED_PEN
// | MULTI_INPUT | READY
GetSystemMetrics(SM_MAXIMUMTOUCHES) = 10
SystemInformation.MousePresent = True
```
So this is not "the user has no mouse". Windows knows a mouse is
attached, and
Chromium still reports `any-pointer: fine: false` and `any-hover: false`
— the
`any-*` queries exist precisely to describe *any* available input
device, and
they are wrong here. The presence of an integrated touch digitizer
collapses the
reported capability to touch-only.
The compiled rule that never applies:
```css
.group-hover\/member\:visible {
&:is(:where(.group\/member):hover *) {
@media (hover: hover) { visibility: visible; }
}
}
```
The row genuinely matches `:hover` (verified: `row.matches(':hover') ===
true`),
the button is in the DOM, the utility class is generated — and the
declaration
still never lands.
## Why this is more than one control
Not a single menu. Confirmed newly-ungated in the production bundle
after the
change:
| utility | media-gated before | after |
|---|---|---|
| `group-hover/member:visible` | yes | no |
| `group-hover/inbox-item:opacity-100` | yes | no |
| `group-hover/channel-row:opacity-100` | yes | no |
| `group-hover/attachment:opacity-100` | yes | no |
| `hover:bg-muted` | yes | no |
On an affected host the channel-member action menu (remove member,
change role,
start/stop agent) has **no reachable affordance at all**: `visibility:
hidden`
also removes the button from tab order, so there is no keyboard path
either.
## The fix
One line, at the root, next to the existing variant override:
```css
@custom-variant hover (&:hover);
```
This trusts the actual hover event rather than the capability query.
Chromium
only fires `:hover` when a real pointer is present, so behaviour on
hosts that
report the capability correctly is unchanged.
Verified against a production `vite build`, not just the dev server —
the
override cascades to the *named* group variants (`group-hover/member`,
etc.),
which is the part that matters here.
## Prior art in this repo
#2849 overrides Tailwind v4's `dark:` variant default at the *exact same
insertion point* in this file, for the same class of reason (a v4
default that
does not match how this app actually works). This change follows that
precedent.
**Note for whoever merges second: #2849 and this PR will conflict
textually** —
both append a `@custom-variant` immediately after `@config`. The
resolution is
to keep both lines; they are independent.
## Scope
Desktop only. `web/src/shared/styles/globals.css` has the same Tailwind
v4
default, but `web/src` contains **zero** `group-hover` usages, so there
are no
hover-revealed affordances to strand there. Adding the override to web
would be
speculative.
One `hover` capability query is deliberately left in place —
`.buzz-wave-hover-trigger` in `animations.css` gates a decorative
wave-hand
animation on `(hover: hover) and (pointer: fine)`. That is a cosmetic
flourish
rather than an affordance, so it stays inert on affected hosts instead
of
widening this diff.
## Reproducing
The trigger is **an integrated touch digitizer anywhere on the
machine**, not the
display you are actually working on. This was found on a touch-capable
laptop
docked to an ordinary non-touch external monitor, driven entirely by a
mouse — so
"I'm on a desktop monitor" does not rule you out. Check with:
```js
matchMedia('(hover: hover)').matches // false ⇒ affected
```
Not reproducible on macOS, or on a Windows machine with no digitizer at
all —
`hover: hover` is true there and every affordance works normally. If you
are on
such a host, emulate it in devtools by forcing `hover: none` / `pointer:
coarse`,
then open a channel's member list and hover a row: no action menu
appears.
## Tradeoff worth naming
On a genuine touch-only device, a bare `&:hover` can latch after a tap
and stay
applied until the next interaction, where the media-query default would
have
suppressed it. That is the real cost of this change.
The judgement here is that a stuck hover style is a cosmetic annoyance,
while an
unreachable "remove member" button is a functional dead end — and that
the
affected hosts are overwhelmingly mouse-driven machines that merely
*happen* to
ship a digitizer, as the `MousePresent = True` reading above shows. If
you would
rather scope this to `@media not (hover: hover)` as an additive fallback
instead
of overriding the variant, I am happy to rework it.
Signed-off-by: sumit-m <33051892+sumit-m@users.noreply.github.com>1 parent 114d40d commit 29dfe48
1 file changed
Lines changed: 13 additions & 0 deletions
File tree
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
| 3 | + | |
3 | 4 | | |
4 | 5 | | |
5 | 6 | | |
| |||
17 | 18 | | |
18 | 19 | | |
19 | 20 | | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
0 commit comments