fix: prevent resource manager right-click menu leak (#2127)
This commit is contained in:
parent
8aae2939f4
commit
c771e02cee
|
|
@ -51,6 +51,7 @@ import {
|
|||
type UnifiedWorktreeRow
|
||||
} from './mergeSnapshotAndSessions'
|
||||
import { WorkspaceSpaceCompactPanel } from './WorkspaceSpaceCompactPanel'
|
||||
import { STATUS_BAR_CONTEXT_MENU_EXEMPT_PROPS } from './status-bar-context-menu-policy'
|
||||
|
||||
const POLL_MS = 2_000
|
||||
const SESSIONS_POLL_MS = 10_000
|
||||
|
|
@ -1085,6 +1086,7 @@ export function ResourceUsageStatusSegment({
|
|||
<PopoverTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
{...STATUS_BAR_CONTEXT_MENU_EXEMPT_PROPS}
|
||||
className="relative inline-flex items-center gap-1.5 cursor-pointer rounded px-1 py-0.5 hover:bg-accent/70"
|
||||
aria-label={
|
||||
spaceScanReady && !runtimeEnvironmentActive
|
||||
|
|
@ -1142,6 +1144,7 @@ export function ResourceUsageStatusSegment({
|
|||
side="top"
|
||||
align="end"
|
||||
sideOffset={8}
|
||||
{...STATUS_BAR_CONTEXT_MENU_EXEMPT_PROPS}
|
||||
className="w-[26rem] max-w-[calc(100vw-2rem)] p-0"
|
||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||
// Why: clicking a terminal row activates a tab, which causes xterm
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import { SshStatusSegment } from './SshStatusSegment'
|
|||
import { UpdateStatusSegment } from './UpdateStatusSegment'
|
||||
import { ResourceUsageStatusSegment } from './ResourceUsageStatusSegment'
|
||||
import { isStatusBarItemAvailable } from './status-bar-agent-gating'
|
||||
import { shouldOpenStatusBarContextMenu } from './status-bar-context-menu-policy'
|
||||
import { PetStatusSegment } from './PetStatusSegment'
|
||||
import { TOGGLE_FLOATING_TERMINAL_EVENT } from '@/lib/floating-terminal'
|
||||
import { FloatingTerminalIconContextMenu } from '@/components/floating-terminal/FloatingTerminalIconContextMenu'
|
||||
|
|
@ -837,10 +838,7 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele
|
|||
ref={containerRefCallback}
|
||||
className="flex items-center h-6 min-h-[24px] px-3 gap-4 border-t border-border bg-[var(--bg-titlebar,var(--card))] text-xs select-none shrink-0 relative"
|
||||
onContextMenuCapture={(event) => {
|
||||
if (
|
||||
event.target instanceof Element &&
|
||||
event.target.closest('[data-floating-terminal-toggle]')
|
||||
) {
|
||||
if (!shouldOpenStatusBarContextMenu(event.target)) {
|
||||
return
|
||||
}
|
||||
// Why: mirror the right-click pattern used across the app
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
STATUS_BAR_CONTEXT_MENU_EXEMPT_SELECTOR,
|
||||
shouldOpenStatusBarContextMenu
|
||||
} from './status-bar-context-menu-policy'
|
||||
|
||||
function targetMatching(selector: string | null): EventTarget & {
|
||||
closest: (value: string) => Element | null
|
||||
} {
|
||||
return {
|
||||
closest: (value: string) => (value === selector ? ({} as Element) : null)
|
||||
} as EventTarget & { closest: (value: string) => Element | null }
|
||||
}
|
||||
|
||||
describe('shouldOpenStatusBarContextMenu', () => {
|
||||
it('opens for plain status-bar right-clicks', () => {
|
||||
expect(shouldOpenStatusBarContextMenu(targetMatching(null))).toBe(true)
|
||||
})
|
||||
|
||||
it('ignores right-clicks inside exempt status-bar popovers', () => {
|
||||
expect(
|
||||
shouldOpenStatusBarContextMenu(targetMatching(STATUS_BAR_CONTEXT_MENU_EXEMPT_SELECTOR))
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps the floating terminal context menu independent', () => {
|
||||
expect(shouldOpenStatusBarContextMenu(targetMatching('[data-floating-terminal-toggle]'))).toBe(
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
it('opens when the browser gives a non-element target', () => {
|
||||
expect(shouldOpenStatusBarContextMenu(null)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
export const STATUS_BAR_CONTEXT_MENU_EXEMPT_ATTR = 'data-status-bar-context-menu-exempt'
|
||||
export const STATUS_BAR_CONTEXT_MENU_EXEMPT_SELECTOR = `[${STATUS_BAR_CONTEXT_MENU_EXEMPT_ATTR}]`
|
||||
export const STATUS_BAR_CONTEXT_MENU_EXEMPT_PROPS = {
|
||||
[STATUS_BAR_CONTEXT_MENU_EXEMPT_ATTR]: ''
|
||||
} as const
|
||||
|
||||
const FLOATING_TERMINAL_TOGGLE_SELECTOR = '[data-floating-terminal-toggle]'
|
||||
|
||||
function hasClosest(target: EventTarget | null): target is EventTarget & {
|
||||
closest: (selector: string) => Element | null
|
||||
} {
|
||||
return typeof (target as { closest?: unknown } | null)?.closest === 'function'
|
||||
}
|
||||
|
||||
export function shouldOpenStatusBarContextMenu(target: EventTarget | null): boolean {
|
||||
if (!hasClosest(target)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Why: Radix portal events can still bubble through the StatusBar React tree;
|
||||
// nested status-bar surfaces opt out so their right-clicks stay local.
|
||||
return (
|
||||
target.closest(FLOATING_TERMINAL_TOGGLE_SELECTOR) === null &&
|
||||
target.closest(STATUS_BAR_CONTEXT_MENU_EXEMPT_SELECTOR) === null
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue