Skip to content

Commit 29dfe48

Browse files
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

desktop/src/shared/styles/globals.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
@import "tailwindcss";
22
@import "tw-animate-css";
3+
34
@import "./globals/scrollbars.css";
45
@import "./globals/motion.css";
56
@import "./globals/animations.css";
@@ -17,3 +18,15 @@
1718
@import "./globals/progress.css";
1819

1920
@config "../../../tailwind.config.js";
21+
22+
/* Tailwind v4 gates `hover:` behind `@media (hover: hover)`. Some Windows
23+
hosts answer that query `false` even with a mouse attached — WebView2 here
24+
reports `hover: none`, `any-pointer: fine: false`, `maxTouchPoints: 10` —
25+
which leaves every hover-revealed control permanently `visibility: hidden`:
26+
member row action menus, sidebar row actions, attachment controls. A bare
27+
`&:hover` trusts the actual hover event instead of the capability query;
28+
Chromium only fires :hover when a real pointer is present.
29+
30+
Must stay below every `@import`: CSS requires `@import` to precede other
31+
at-rules, so placing this above them silently drops the rest of the sheet. */
32+
@custom-variant hover (&:hover);

0 commit comments

Comments
 (0)