fix(desktop): show Gateway statusbar tooltip via composed trigger Slots

The Gateway item is the only statusbar entry with variant === 'menu'.
Since da73223f4 wrapped every render branch in `Tip`, the menu branch
nested `<DropdownMenu>` (a Radix Root that renders no DOM node) inside
`Tip`'s `<TooltipTrigger asChild>`. With no element to attach to, Radix
could never wire hover listeners, so the tooltip silently never showed.

`Tip` also can't be moved inside `DropdownMenuTrigger asChild` (the shape
proposed in #54859): it's a plain component, not a Slot-forwarding one, so
the trigger's injected ref/handlers would land on `TooltipContent` instead
of the button and break the menu's click + popper anchoring.

Fix by composing both trigger Slots directly onto a single <button>
(`TooltipTrigger asChild` over `DropdownMenuTrigger asChild`), the pattern
already used in profile-switcher.tsx, and skip the tooltip wrapper entirely
when the item has no title.

Supersedes #54859.

Co-authored-by: wnuuee1 <wnuuee1@users.noreply.github.com>
This commit is contained in:
Brooklyn Nicholson 2026-06-29 13:44:32 -05:00
parent 929dd9c0d7
commit 7a6b3cb923

View file

@ -2,7 +2,7 @@ import { type ComponentProps, type ReactNode, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
import { Tip } from '@/components/ui/tooltip'
import { Tip, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
// Shared chrome styling for interactive statusbar items (button / link / menu
@ -100,60 +100,76 @@ function StatusbarItemView({ item, navigate }: { item: StatusbarItem; navigate:
)
if (item.variant === 'menu' && (item.menuContent || (item.menuItems && item.menuItems.length > 0))) {
return (
<Tip label={item.title}>
<DropdownMenu onOpenChange={setMenuOpen} open={menuOpen}>
<DropdownMenuTrigger asChild>
<button className={cn(STATUSBAR_ACTION_CLASS, item.className)} disabled={item.disabled} type="button">
{content}
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
align={item.menuAlign ?? 'start'}
className={cn('w-56', item.menuContent && 'p-0', item.menuClassName)}
side="top"
sideOffset={8}
>
{item.menuContent
? typeof item.menuContent === 'function'
? item.menuContent(() => setMenuOpen(false))
: item.menuContent
: (item.menuItems ?? [])
.filter(menuItem => !menuItem.hidden)
.map(menuItem => (
<DropdownMenuItem
className={cn('gap-2 text-foreground focus:bg-accent [&_svg]:size-4', menuItem.className)}
disabled={menuItem.disabled}
key={menuItem.id}
onSelect={() => {
if (menuItem.to) {
navigate(menuItem.to)
}
// The `Tip` helper can't wrap a menu: its TooltipTrigger needs a DOM child,
// but DropdownMenu's Root renders no element, so the hover listeners never
// land on the button and the tooltip silently never shows. Compose the two
// trigger Slots directly onto the same <button> instead (both asChild), the
// way profile-switcher.tsx stacks Popover/ContextMenu/Tooltip triggers.
const trigger = (
<DropdownMenuTrigger asChild>
<button className={cn(STATUSBAR_ACTION_CLASS, item.className)} disabled={item.disabled} type="button">
{content}
</button>
</DropdownMenuTrigger>
)
menuItem.onSelect?.()
}}
>
{menuItem.href ? (
<a
className="inline-flex w-full items-center gap-2"
href={menuItem.href}
rel="noreferrer"
target="_blank"
>
{menuItem.icon}
<span className="truncate">{menuItem.label}</span>
</a>
) : (
<>
{menuItem.icon}
<span className="truncate">{menuItem.label}</span>
</>
)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</Tip>
return (
<DropdownMenu onOpenChange={setMenuOpen} open={menuOpen}>
{item.title ? (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>{trigger}</TooltipTrigger>
<TooltipContent>{item.title}</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
trigger
)}
<DropdownMenuContent
align={item.menuAlign ?? 'start'}
className={cn('w-56', item.menuContent && 'p-0', item.menuClassName)}
side="top"
sideOffset={8}
>
{item.menuContent
? typeof item.menuContent === 'function'
? item.menuContent(() => setMenuOpen(false))
: item.menuContent
: (item.menuItems ?? [])
.filter(menuItem => !menuItem.hidden)
.map(menuItem => (
<DropdownMenuItem
className={cn('gap-2 text-foreground focus:bg-accent [&_svg]:size-4', menuItem.className)}
disabled={menuItem.disabled}
key={menuItem.id}
onSelect={() => {
if (menuItem.to) {
navigate(menuItem.to)
}
menuItem.onSelect?.()
}}
>
{menuItem.href ? (
<a
className="inline-flex w-full items-center gap-2"
href={menuItem.href}
rel="noreferrer"
target="_blank"
>
{menuItem.icon}
<span className="truncate">{menuItem.label}</span>
</a>
) : (
<>
{menuItem.icon}
<span className="truncate">{menuItem.label}</span>
</>
)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
)
}