fix(desktop): stop tip-wrapping kebab menu triggers

Remove the ActionsMenu tooltip prop and Tip wrappers on session, project,
workspace, panel, and credential ⋯ menus. aria-label stays for a11y; drop
the unused credentialActions string.
This commit is contained in:
Brooklyn Nicholson 2026-07-27 21:43:52 -05:00
parent 58714e0e2a
commit 26413bbf57
13 changed files with 19 additions and 55 deletions

View file

@ -19,7 +19,6 @@ import {
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { Popover, PopoverAnchor, PopoverContent } from '@/components/ui/popover'
import { Tip } from '@/components/ui/tooltip'
import { useI18n } from '@/i18n'
import { cn } from '@/lib/utils'
import { $panesFlipped, dismissAutoProject } from '@/store/layout'
@ -192,13 +191,9 @@ export function ProjectMenu({
</DropdownMenuItem>
)
// The bare trigger button (no Tip, no anchor) — composed with whichever of
// Tip / PopoverAnchor apply below, always OUTSIDE the asChild chain that
// ends at this button, never wrapping it directly. asChild clones only its
// immediate child, so any of these wrappers placed inside another
// asChild-consuming component (instead of around it) would have its
// injected props silently swallowed by that inner component instead of
// reaching the real DOM button (see #67500).
// When anchorRef is absent, PopoverAnchor wraps the trigger so the
// appearance popover positions against this button. Keep asChild chains free
// of non-forwarding wrappers (#67500).
const triggerButton = (
<DropdownMenuTrigger asChild>
<button
@ -217,16 +212,7 @@ export function ProjectMenu({
</DropdownMenuTrigger>
)
// Tip always wraps the outermost element of whatever we render — either the
// trigger directly (anchorRef present: the popover anchors to the whole row
// via a separate virtualRef, so PopoverAnchor isn't involved here), or the
// PopoverAnchor-wrapped trigger (anchorRef absent: the popover anchors to
// this button itself). Either way, Tip > PopoverAnchor > DropdownMenuTrigger
// > button, so asChild composes props/ref all the way down to the real DOM
// node instead of stopping at an intermediate wrapper.
const trigger = (
<Tip label={p.menu}>{anchorRef ? triggerButton : <PopoverAnchor asChild>{triggerButton}</PopoverAnchor>}</Tip>
)
const trigger = anchorRef ? triggerButton : <PopoverAnchor asChild>{triggerButton}</PopoverAnchor>
return (
<Popover onOpenChange={setAppearanceOpen} open={appearanceOpen}>

View file

@ -116,7 +116,7 @@ export function WorkspaceMenu({ path, onRemove }: { path: null | string; onRemov
const items = useWorkspaceItems({ onRemove, path })
return (
<ActionsMenu ariaLabel={p.menu} contentClassName="w-48" items={items} tooltip={p.menu}>
<ActionsMenu ariaLabel={p.menu} contentClassName="w-48" items={items}>
<button
aria-label={p.menu}
className="grid size-4 shrink-0 place-items-center rounded-sm bg-transparent text-(--ui-text-quaternary) opacity-0 transition-opacity hover:bg-(--ui-control-hover-background) hover:text-foreground group-hover/workspace:opacity-100 data-[state=open]:opacity-100"

View file

@ -388,13 +388,10 @@ function useSessionActions({
interface SessionActionsMenuProps
extends SessionActions, Pick<React.ComponentProps<typeof ActionsMenu>, 'align' | 'sideOffset'> {
children: React.ReactNode
/** Tooltip label for the trigger. */
tooltip?: React.ReactNode
}
export function SessionActionsMenu({
children,
tooltip,
align = 'end',
sideOffset = 6,
...actions
@ -410,7 +407,6 @@ export function SessionActionsMenu({
contentClassName="w-40"
items={renderItems}
sideOffset={sideOffset}
tooltip={tooltip}
>
{children}
</ActionsMenu>

View file

@ -116,7 +116,6 @@ export function SidebarSessionRow({
profile={session.profile}
sessionId={session.id}
title={title}
tooltip={r.actionsFor(title)}
>
<Button
aria-label={r.actionsFor(title)}

View file

@ -256,7 +256,7 @@ export function PanelRowMenu({ items, label = 'Actions' }: { items: PanelMenuIte
}
return (
<ActionsMenu ariaLabel={label} contentClassName="w-40" items={renderPanelMenuItems(items)} tooltip={label}>
<ActionsMenu ariaLabel={label} contentClassName="w-40" items={renderPanelMenuItems(items)}>
<Button
aria-label={label}
className="size-5 rounded-[4px] bg-transparent text-(--ui-text-tertiary) opacity-0 transition-colors duration-100 hover:bg-(--ui-control-active-background) hover:text-foreground focus-visible:opacity-100 focus-visible:ring-0 group-hover/row:opacity-100 data-[state=open]:bg-(--ui-control-active-background) data-[state=open]:text-foreground data-[state=open]:opacity-100 [&_svg]:size-3.5!"

View file

@ -9,7 +9,6 @@ import {
} from '@/components/ui/actions-menu'
import { Button } from '@/components/ui/button'
import { Codicon } from '@/components/ui/codicon'
import { Tip } from '@/components/ui/tooltip'
import { useI18n } from '@/i18n'
import { triggerHaptic } from '@/lib/haptics'
import { ExternalLink, Eye, EyeOff, KeyRound, Trash2 } from '@/lib/icons'
@ -174,16 +173,14 @@ export function EnvVarActionsTrigger({ className, label, ...props }: EnvVarActio
const copy = t.settings.envActions
return (
<Tip label={copy.credentialActions}>
<Button
aria-label={copy.actionsFor(label)}
className={cn('text-muted-foreground hover:text-foreground', className)}
size="icon-sm"
variant="ghost"
{...props}
>
<Codicon name="ellipsis" size="0.875rem" />
</Button>
</Tip>
<Button
aria-label={copy.actionsFor(label)}
className={cn('text-muted-foreground hover:text-foreground', className)}
size="icon-sm"
variant="ghost"
{...props}
>
<Codicon name="ellipsis" size="0.875rem" />
</Button>
)
}

View file

@ -23,7 +23,6 @@ import {
DropdownMenuSubTrigger,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { Tip } from '@/components/ui/tooltip'
// One place to define a set of actions and get BOTH a kebab dropdown and a
// matching right-click context menu — so a row's ⋯ menu and its right-click menu
@ -108,15 +107,14 @@ interface ActionsMenuProps extends Pick<
items: (kit: MenuKit) => React.ReactNode
ariaLabel?: string
contentClassName?: string
/** Optional tooltip on the trigger (composed INSIDE the asChild chain). */
tooltip?: React.ReactNode
open?: boolean
onOpenChange?: (open: boolean) => void
}
/**
* A kebab dropdown menu. Pair it with `ActionsContextMenu` using the same
* `items` render function so the two menus stay identical.
* `items` render function so the two menus stay identical. No tip on the
* trigger `aria-label` on the button is enough (see DESIGN.md).
*/
export function ActionsMenu({
align = 'end',
@ -127,17 +125,11 @@ export function ActionsMenu({
onOpenChange,
open,
side,
sideOffset = 6,
tooltip
sideOffset = 6
}: ActionsMenuProps) {
// Tip wraps the trigger, not the reverse: Tip doesn't forward the ref/props an
// `asChild` clone injects, so a Tip placed as the trigger's child silently
// drops onClick/aria-haspopup and the menu stops opening (#67500).
const trigger = <DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
return (
<DropdownMenu onOpenChange={onOpenChange} open={open}>
{tooltip ? <Tip label={tooltip}>{trigger}</Tip> : trigger}
<DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>
<DropdownMenuContent
align={align}
aria-label={ariaLabel}

View file

@ -640,7 +640,6 @@ export const ar = defineLocale({
},
envActions: {
actionsFor: label => `إجراءات ${label}`,
credentialActions: 'إجراءات بيانات الاعتماد',
docs: 'الوثائق',
hideValue: 'إخفاء القيمة',
revealValue: 'إظهار القيمة',

View file

@ -574,7 +574,6 @@ export const en: Translations = {
},
envActions: {
actionsFor: label => `Actions for ${label}`,
credentialActions: 'Credential actions',
manageInKeys: 'Manage in API Keys',
docs: 'Docs',
hideValue: 'Hide value',

View file

@ -672,7 +672,6 @@ export const ja = defineLocale({
},
envActions: {
actionsFor: label => `${label} のアクション`,
credentialActions: '認証情報のアクション',
manageInKeys: 'API キーで管理',
docs: 'ドキュメント',
hideValue: '値を非表示',

View file

@ -477,7 +477,6 @@ export interface Translations {
}
envActions: {
actionsFor: (label: string) => string
credentialActions: string
manageInKeys: string
docs: string
hideValue: string

View file

@ -659,7 +659,6 @@ export const zhHant = defineLocale({
},
envActions: {
actionsFor: label => `${label} 的動作`,
credentialActions: '憑證動作',
manageInKeys: '在 API 金鑰中管理',
docs: '文件',
hideValue: '隱藏值',

View file

@ -780,7 +780,6 @@ export const zh: Translations = {
},
envActions: {
actionsFor: label => `${label} 的操作`,
credentialActions: '凭据操作',
manageInKeys: '在 API 密钥中管理',
docs: '文档',
hideValue: '隐藏值',