Skip to content

dogfood: fix hover-card, dropdown submenu, sonner on iOS/touch #745

Description

@vivek7405

Problem

Dogfood QA on a real iPhone (iOS 18.7 / Safari 26.5), AFTER the #730 marker-crash fix landed, found three Tier-2 components still broken on touch. All the rest now work. Fix all three in a SINGLE PR.

  1. hover-card: opens only on mouseenter / focusin. Touch has no hover, so a tap falls through to the inner <a href> and the client router navigates (looks like a page "refresh"; it is NOT prefetch). Tapping the trigger should open the card and not navigate the link.
  2. dropdown-menu submenu: the sub-trigger opens the submenu on pointer-enter and closes on pointer-leave, so on touch the submenu is only open while the finger is held down. A tap should toggle the submenu open and keep it open.
  3. sonner: the demo "Show toast" button calls toast.success() via a dynamic import. The import path (/components/ui/sonner.ts, HTTP 200) and the toast.success export both verified, but a Chromium + desktop-WebKit triage shows the toast does NOT render even on desktop (clicked=true, <ui-sonner> mounted, but no toast node appears). So sonner is broader than iOS, likely the <ui-sonner> viewport signal re-render or where the toast node is placed.

Design / approach

  • hover-card + dropdown share a root cause: hover/pointer-only interaction with no touch path. Add a pointer-type-aware tap path (e.g. @click / pointerdown with pointerType === 'touch', or a coarse-pointer check) that opens (hover-card) / toggles (submenu) without breaking mouse hover or keyboard nav. For hover-card the tap handler must preventDefault() the inner link navigation on the first tap (open), letting a second tap follow the link, or open without consuming the link per the agreed UX.
  • sonner is a separate, desktop-reproducible bug: root-cause why toast() does not produce a visible toast (the items signal re-render in <ui-sonner>, the toaster singleton routing in connectedCallback, or the toast node placement), then fix.

Implementation notes (for the implementing agent)

  • Where to edit (registry is the source; scripts/copy-registry.js mirrors it into the ui-website):
    • packages/ui/packages/registry/components/hover-card.ts (UiHoverCardTrigger; open handlers _showTimer/_hideTimer, the control lookup a[href], button, ... ~L90).
    • packages/ui/packages/registry/components/dropdown-menu.ts (UiDropdownMenuSub / UiDropdownMenuSubTrigger; submenu open/close + dropdownMenuSubTriggerClass).
    • packages/ui/packages/registry/components/sonner.ts (toaster singleton ~L64, toast() export ~L?, <ui-sonner> connectedCallback sets toaster.add = this._add ~L152, items = signal<ToastItem[]>([]) ~L140, _add/addToast ~L165, render() with repeat).
    • Demos: packages/ui/packages/website/app/docs/components/[name]/examples.ts (sonner demo at L529 onclick="import('/components/ui/sonner.ts').then(m => m.toast.success(...))").
  • Landmines:
  • Invariants: light-DOM slot components; signals are the state primitive (read via signal.get() in render()); no class-field reactive props; tag-prefixed CSS in light DOM. Build on dogfood: all tier-2 ui components dead on iOS (slot hydration); tap does nothing #730 (MARKER='wjm-').
  • Tests + docs: browser tests (packages/core or a ui-package test layer) for the interaction + the sonner render (Chromium-runnable for sonner); update component docs/examples if the interaction contract changes.

Acceptance criteria

  • hover-card opens on tap (touch) without navigating the inner link; mouse hover still opens it; JS-off the link still navigates.
  • dropdown submenu opens and STAYS open on tap (touch); mouse hover still works; keyboard arrow/Escape nav unaffected.
  • sonner toast renders when triggered (fix the desktop-reproducible failure first; then confirm on iOS).
  • Tests cover each change at the layer it touches; the sonner render has a Chromium browser test; a counterfactual proves it fires.
  • All three fixed in a SINGLE PR; any on-device diagnostic scaffolding removed before merge.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type
No fields configured for issues without a type.

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions