mirror of
https://github.com/stablyai/orca.git
synced 2026-10-13 00:02:54 +00:00
fix(tabs): stop tab and close tooltips firing while grazing the strip (#25808)
* fix(tabs): delay tab and close tooltips past grazing
Tab title and close-icon tooltips fired near-instantly while dragging
across the strip: Radix's provider-wide skipDelayDuration reopens the
next tooltip with no delay for 300ms after any tooltip closes.
Scope a nested TooltipProvider to the tab strip with a 700ms delay and
skipDelayDuration=0, so each tab tooltip waits the full delay.
* fix(tabs): drop close-button tooltip and lengthen tab tooltip delay
Remove the hover tooltip from the tab close button entirely (aria-label
keeps the accessible name), and raise the tab-title tooltip delay from
700ms to 1500ms so grazing or scrolling the strip no longer pops a label.
Add an interaction test proving the delay is enforced and Radix's
skip-delay is disabled.
* fix(tabs): delay the agent-state glyph tooltip to match the strip
The tab's leading agent-state dot renders its own feature tooltip with a
hardcoded 200ms delay, so tabs with a live agent state (working/done/
needs-attention) popped almost instantly while plain tabs waited the full
strip delay — the 'every other tab' inconsistency. Thread an optional
delay through StateIndicatorTooltip/AgentStateDot and pass the tab-strip
1500ms from TerminalTabLeadingIcon; other surfaces keep 200ms.
* fix(tabs): remove the agent-state glyph tooltip on tabs
Hovering a tab's status glyph (spinner/check/question) popped its own
tooltip after 200ms, so tabs with a live agent state popped while plain
tabs stayed quiet. Remove that tooltip entirely via title={null}; the
glyph stays self-describing with its aria-label, and the tab title keeps
its tooltip. Reverts the now-unused delay plumbing.
* reduce the time down from 1500 ms to 500 ms
* fix(tabs): restore the close-tab shortcut hint with a slower delay
Close buttons share one TabCloseTooltip that waits 1s, longer than the 500ms tab-title tooltip. Also merges the tooltip provider tests.
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import { X } from 'lucide-react'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { useOptionalShortcutLabel } from '@/hooks/useShortcutLabel'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { TabCloseTooltip } from './TabCloseTooltip'
|
||||
|
||||
export function EditorFileTabCloseButton({
|
||||
fileIsDirty,
|
||||
@@ -12,43 +11,32 @@ export function EditorFileTabCloseButton({
|
||||
showsSelectionChrome: boolean
|
||||
onClose: () => void
|
||||
}): React.JSX.Element {
|
||||
const closeShortcut = useOptionalShortcutLabel('tab.close')
|
||||
const closeLabel = translate(
|
||||
'auto.components.tab.bar.EditorFileTabCloseButton.a768f428f1',
|
||||
'Close tab'
|
||||
)
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
className={`flex items-center justify-center w-4 h-4 rounded-sm ${
|
||||
fileIsDirty
|
||||
? 'hidden group-hover:flex text-muted-foreground hover:text-foreground hover:bg-muted'
|
||||
: showsSelectionChrome
|
||||
? 'text-muted-foreground hover:text-foreground hover:bg-muted'
|
||||
: 'text-transparent group-hover:text-muted-foreground hover:!text-foreground hover:!bg-muted'
|
||||
}`}
|
||||
type="button"
|
||||
// Why: simulator unified tabs reuse this tab chrome, so E2E needs
|
||||
// the same stable close affordance on the real button users click.
|
||||
data-tab-close-button="true"
|
||||
aria-label={translate(
|
||||
'auto.components.tab.bar.EditorFileTabCloseButton.4655cf570e',
|
||||
'Close tab'
|
||||
)}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" sideOffset={6}>
|
||||
{closeShortcut ? `${closeLabel} (${closeShortcut})` : closeLabel}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<TabCloseTooltip>
|
||||
<button
|
||||
className={`flex items-center justify-center w-4 h-4 rounded-sm ${
|
||||
fileIsDirty
|
||||
? 'hidden group-hover:flex text-muted-foreground hover:text-foreground hover:bg-muted'
|
||||
: showsSelectionChrome
|
||||
? 'text-muted-foreground hover:text-foreground hover:bg-muted'
|
||||
: 'text-transparent group-hover:text-muted-foreground hover:!text-foreground hover:!bg-muted'
|
||||
}`}
|
||||
type="button"
|
||||
// Why: simulator unified tabs reuse this tab chrome, so E2E needs
|
||||
// the same stable close affordance on the real button users click.
|
||||
data-tab-close-button="true"
|
||||
aria-label={translate(
|
||||
'auto.components.tab.bar.EditorFileTabCloseButton.4655cf570e',
|
||||
'Close tab'
|
||||
)}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</TabCloseTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ import { SortableTabContextMenu } from './SortableTabContextMenu'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { TAB_LABEL_WIDTH_CLASSES } from './tab-width-rules'
|
||||
import { useTabStripSlotProps } from './use-tab-strip-slot-props'
|
||||
import { useOptionalShortcutLabel } from '@/hooks/useShortcutLabel'
|
||||
import { useTabStripPointerActivation } from './tab-strip-pointer-activation'
|
||||
import { TabCloseTooltip } from './TabCloseTooltip'
|
||||
import { TerminalTabLeadingIcon } from './TerminalTabLeadingIcon'
|
||||
import {
|
||||
isTerminalTabActivityLive,
|
||||
@@ -174,8 +174,6 @@ export default function SortableTab({
|
||||
disabled: isEditing
|
||||
})
|
||||
const slotProps = useTabStripSlotProps(tab.id, isActive)
|
||||
const closeShortcut = useOptionalShortcutLabel('tab.close')
|
||||
const closeLabel = translate('auto.components.tab.bar.SortableTab.95db5f2f7d', 'Close tab')
|
||||
const tabTitle = tab.customTitle ?? tab.title
|
||||
const tabRoot = (
|
||||
<div
|
||||
@@ -322,45 +320,40 @@ export default function SortableTab({
|
||||
</button>
|
||||
)}
|
||||
{!isEditing && !isPinned && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
className={`relative z-10 flex items-center justify-center w-4 h-4 rounded-sm shrink-0 ${
|
||||
isActive
|
||||
? 'text-muted-foreground hover:text-foreground hover:bg-muted focus-visible:text-foreground focus-visible:bg-muted'
|
||||
: 'text-transparent group-hover:text-muted-foreground hover:!text-foreground hover:!bg-muted focus-visible:!text-foreground focus-visible:!bg-muted'
|
||||
}`}
|
||||
// Why: stable accessible name lets E2E drive the real close path (hover, then X) instead of calling the store.
|
||||
aria-label={translate(
|
||||
'auto.components.tab.bar.SortableTab.6df69d9388',
|
||||
'Close tab {{value0}}',
|
||||
{ value0: tabTitle }
|
||||
)}
|
||||
type="button"
|
||||
data-tab-close-button="true"
|
||||
onPointerDown={(e) => {
|
||||
if (e.button === 0) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
if (e.button === 0) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
<TabCloseTooltip>
|
||||
<button
|
||||
className={`relative z-10 flex items-center justify-center w-4 h-4 rounded-sm shrink-0 ${
|
||||
isActive
|
||||
? 'text-muted-foreground hover:text-foreground hover:bg-muted focus-visible:text-foreground focus-visible:bg-muted'
|
||||
: 'text-transparent group-hover:text-muted-foreground hover:!text-foreground hover:!bg-muted focus-visible:!text-foreground focus-visible:!bg-muted'
|
||||
}`}
|
||||
// Why: stable accessible name lets E2E drive the real close path instead of calling the store.
|
||||
aria-label={translate(
|
||||
'auto.components.tab.bar.SortableTab.6df69d9388',
|
||||
'Close tab {{value0}}',
|
||||
{ value0: tabTitle }
|
||||
)}
|
||||
type="button"
|
||||
data-tab-close-button="true"
|
||||
onPointerDown={(e) => {
|
||||
if (e.button === 0) {
|
||||
e.stopPropagation()
|
||||
onClose(tab.id)
|
||||
}}
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" sideOffset={6}>
|
||||
{closeShortcut ? `${closeLabel} (${closeShortcut})` : closeLabel}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
}
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
if (e.button === 0) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onClose(tab.id)
|
||||
}}
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</TabCloseTooltip>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { expandNode } from './tab-bar-dropdown-menu-item-probe'
|
||||
import { stubHeadlessReact, stubShallowSelector } from './tab-bar-windows-shell-launch-render-stubs'
|
||||
import {
|
||||
stubHeadlessReact,
|
||||
stubShallowSelector,
|
||||
stubTooltip
|
||||
} from './tab-bar-windows-shell-launch-render-stubs'
|
||||
|
||||
const appStoreSnapshot: {
|
||||
activeTabId: string | null
|
||||
@@ -181,11 +185,7 @@ vi.mock('@/components/ui/dropdown-menu', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/components/ui/tooltip', () => ({
|
||||
Tooltip: 'Tooltip',
|
||||
TooltipContent: 'TooltipContent',
|
||||
TooltipTrigger: 'TooltipTrigger'
|
||||
}))
|
||||
vi.mock('@/components/ui/tooltip', () => stubTooltip())
|
||||
|
||||
type ReactElementLike = {
|
||||
type: unknown
|
||||
|
||||
@@ -14,7 +14,8 @@ import {
|
||||
stubSortableTab,
|
||||
stubStatusDisplay,
|
||||
stubTabInsertion,
|
||||
stubTabStripDragScroll
|
||||
stubTabStripDragScroll,
|
||||
stubTooltip
|
||||
} from './tab-bar-windows-shell-launch-render-stubs'
|
||||
import {
|
||||
appStoreSnapshot,
|
||||
@@ -38,6 +39,7 @@ vi.mock('./QuickLaunchButton', () => stubQuickLaunchButton())
|
||||
vi.mock('./shell-icons', () => stubShellIcons())
|
||||
vi.mock('@/lib/focus-terminal-tab-surface', () => stubFocusTerminalTabSurface())
|
||||
vi.mock('@/components/ui/dropdown-menu', () => stubDropdownMenu())
|
||||
vi.mock('@/components/ui/tooltip', () => stubTooltip())
|
||||
|
||||
describe('TabBar PowerShell launch wiring', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -14,7 +14,8 @@ import {
|
||||
stubSortableTab,
|
||||
stubStatusDisplay,
|
||||
stubTabInsertion,
|
||||
stubTabStripDragScroll
|
||||
stubTabStripDragScroll,
|
||||
stubTooltip
|
||||
} from './tab-bar-windows-shell-launch-render-stubs'
|
||||
import {
|
||||
appStoreSnapshot,
|
||||
@@ -38,6 +39,7 @@ vi.mock('./QuickLaunchButton', () => stubQuickLaunchButton())
|
||||
vi.mock('./shell-icons', () => stubShellIcons())
|
||||
vi.mock('@/lib/focus-terminal-tab-surface', () => stubFocusTerminalTabSurface())
|
||||
vi.mock('@/components/ui/dropdown-menu', () => stubDropdownMenu())
|
||||
vi.mock('@/components/ui/tooltip', () => stubTooltip())
|
||||
|
||||
describe('TabBar PowerShell launch wiring', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -14,7 +14,8 @@ import {
|
||||
stubSortableTab,
|
||||
stubStatusDisplay,
|
||||
stubTabInsertion,
|
||||
stubTabStripDragScroll
|
||||
stubTabStripDragScroll,
|
||||
stubTooltip
|
||||
} from './tab-bar-windows-shell-launch-render-stubs'
|
||||
import {
|
||||
appStoreSnapshot,
|
||||
@@ -38,6 +39,7 @@ vi.mock('./QuickLaunchButton', () => stubQuickLaunchButton())
|
||||
vi.mock('./shell-icons', () => stubShellIcons())
|
||||
vi.mock('@/lib/focus-terminal-tab-surface', () => stubFocusTerminalTabSurface())
|
||||
vi.mock('@/components/ui/dropdown-menu', () => stubDropdownMenu())
|
||||
vi.mock('@/components/ui/tooltip', () => stubTooltip())
|
||||
|
||||
describe('TabBar PowerShell launch wiring', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { useOptionalShortcutLabel } from '@/hooks/useShortcutLabel'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { TAB_TOOLTIP_DELAY_MS } from './TabStripTooltipProvider'
|
||||
|
||||
// Why: the close hint is secondary to the tab title, so it waits longer.
|
||||
export const TAB_CLOSE_TOOLTIP_DELAY_MS = TAB_TOOLTIP_DELAY_MS + 500
|
||||
|
||||
export function TabCloseTooltip({ children }: { children: ReactNode }): React.JSX.Element {
|
||||
const closeShortcut = useOptionalShortcutLabel('tab.close')
|
||||
const closeLabel = translate(
|
||||
'auto.components.tab.bar.EditorFileTabCloseButton.a768f428f1',
|
||||
'Close tab'
|
||||
)
|
||||
|
||||
return (
|
||||
<Tooltip delayDuration={TAB_CLOSE_TOOLTIP_DELAY_MS}>
|
||||
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
||||
<TooltipContent side="bottom" sideOffset={6}>
|
||||
{closeShortcut ? `${closeLabel} (${closeShortcut})` : closeLabel}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { TAB_TOOLTIP_DELAY_MS, TabStripTooltipProvider } from './TabStripTooltipProvider'
|
||||
|
||||
function renderTooltip(label: string, tip: string): void {
|
||||
render(
|
||||
<TabStripTooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button type="button">{label}</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{tip}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TabStripTooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
describe('TabStripTooltipProvider', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('holds a tab tooltip until the full delay elapses', () => {
|
||||
renderTooltip('tab label', 'tab tooltip')
|
||||
fireEvent.pointerMove(screen.getByRole('button', { name: 'tab label' }), {
|
||||
pointerType: 'mouse'
|
||||
})
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(TAB_TOOLTIP_DELAY_MS - 1)
|
||||
})
|
||||
expect(screen.queryByText('tab tooltip')).toBeNull()
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(1)
|
||||
})
|
||||
expect(screen.getByText('tab tooltip')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('does not let a recently-closed tooltip skip the delay on the next tab', () => {
|
||||
render(
|
||||
<TabStripTooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button type="button">first tab</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>first tip</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button type="button">second tab</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>second tip</TooltipContent>
|
||||
</Tooltip>
|
||||
</TabStripTooltipProvider>
|
||||
)
|
||||
|
||||
const first = screen.getByRole('button', { name: 'first tab' })
|
||||
const second = screen.getByRole('button', { name: 'second tab' })
|
||||
|
||||
fireEvent.pointerMove(first, { pointerType: 'mouse' })
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(TAB_TOOLTIP_DELAY_MS)
|
||||
})
|
||||
expect(screen.getByText('first tip')).toBeTruthy()
|
||||
|
||||
fireEvent.pointerLeave(first)
|
||||
fireEvent.pointerMove(second, { pointerType: 'mouse' })
|
||||
|
||||
// Radix's default would open this one instantly for 300ms after a close.
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(TAB_TOOLTIP_DELAY_MS - 1)
|
||||
})
|
||||
expect(screen.queryByText('second tip')).toBeNull()
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(1)
|
||||
})
|
||||
expect(screen.getByText('second tip')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
|
||||
// Why: longer than the app-wide 400ms so a pointer merely passing over the strip never pops a title.
|
||||
export const TAB_TOOLTIP_DELAY_MS = 500
|
||||
|
||||
export function TabStripTooltipProvider({ children }: { children: ReactNode }): React.JSX.Element {
|
||||
// Why: skipDelayDuration=0 keeps the full delay after a tooltip closes. Radix
|
||||
// otherwise opens the next tab's label instantly for 300ms after any close,
|
||||
// which is what makes dragging across the strip fire tooltips back to back.
|
||||
return (
|
||||
<TooltipProvider delayDuration={TAB_TOOLTIP_DELAY_MS} skipDelayDuration={0}>
|
||||
{children}
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const agentDotMock = vi.hoisted(() => vi.fn((_props: Record<string, unknown>) => null))
|
||||
|
||||
vi.mock('@/components/AgentStateDot', () => ({
|
||||
AgentStateDot: (props: Record<string, unknown>) => {
|
||||
agentDotMock(props)
|
||||
return null
|
||||
}
|
||||
}))
|
||||
|
||||
import { TerminalTabLeadingIcon } from './TerminalTabLeadingIcon'
|
||||
|
||||
describe('TerminalTabLeadingIcon agent-state glyph', () => {
|
||||
it('renders the state glyph without a hover tooltip', () => {
|
||||
renderToStaticMarkup(
|
||||
<TerminalTabLeadingIcon
|
||||
agent="claude"
|
||||
activityStatus="working"
|
||||
shell={undefined}
|
||||
showUnreadActivity={false}
|
||||
isActive
|
||||
/>
|
||||
)
|
||||
|
||||
expect(agentDotMock).toHaveBeenCalledWith(expect.objectContaining({ title: null }))
|
||||
})
|
||||
})
|
||||
@@ -76,7 +76,8 @@ export function TerminalTabLeadingIcon({
|
||||
data-agent-activity-status={activityStatus}
|
||||
className="mr-1 inline-flex shrink-0 items-center gap-1"
|
||||
>
|
||||
<AgentStateDot state={dotState} size="md" />
|
||||
{/* Why: the state glyph is self-describing and the tab title already has a tooltip; hover here stays quiet. */}
|
||||
<AgentStateDot state={dotState} size="md" title={null} />
|
||||
{/* Why: status and identity answer different questions. Keep the agent
|
||||
logo beside the state glyph so parallel tabs remain scannable. */}
|
||||
{agent ? <TerminalTabAgentIdentityIcon agent={agent} isActive={isActive} /> : null}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
|
||||
import { QuickLaunchAgentMenuItems } from './QuickLaunchButton'
|
||||
import TabBarCreateEntry from './TabBarCreateEntry'
|
||||
import { TabStripScrollIndicator } from './TabStripScrollIndicator'
|
||||
import { TabStripTooltipProvider } from './TabStripTooltipProvider'
|
||||
import { getTabStripScrollMaskClassName } from './tab-strip-scroll-metrics'
|
||||
import type { useTabStripOverflowNavigation } from './tab-strip-overflow-navigation'
|
||||
import type { useTabStripDragScrollHandlers } from './tab-strip-drag-scroll'
|
||||
@@ -159,16 +160,18 @@ export function renderTabBarSurface({
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
{renderedItems}
|
||||
{clientHostedBrowserRows.length > 0 ? (
|
||||
<ClientHostedBrowserTabRows
|
||||
rows={clientHostedBrowserRows}
|
||||
worktreeId={worktreeId}
|
||||
groupId={resolvedGroupId}
|
||||
groupActiveTabId={props.groupActiveTabId ?? null}
|
||||
includeTopTabBorder={includeTopTabBorder}
|
||||
/>
|
||||
) : null}
|
||||
<TabStripTooltipProvider>
|
||||
{renderedItems}
|
||||
{clientHostedBrowserRows.length > 0 ? (
|
||||
<ClientHostedBrowserTabRows
|
||||
rows={clientHostedBrowserRows}
|
||||
worktreeId={worktreeId}
|
||||
groupId={resolvedGroupId}
|
||||
groupActiveTabId={props.groupActiveTabId ?? null}
|
||||
includeTopTabBorder={includeTopTabBorder}
|
||||
/>
|
||||
) : null}
|
||||
</TabStripTooltipProvider>
|
||||
</div>
|
||||
<TabStripScrollIndicator
|
||||
hasOverflow={tabStripOverflowState.hasOverflow}
|
||||
|
||||
@@ -46,6 +46,21 @@ export function stubTabStripDragScroll(): Record<string, unknown> {
|
||||
}
|
||||
}
|
||||
|
||||
// Tooltip primitives render to inert descriptors so the probe never invokes a
|
||||
// hook-using provider/trigger under the headless React stub.
|
||||
export function stubTooltip(): Record<string, unknown> {
|
||||
const passthrough = (type: string) =>
|
||||
function TooltipPrimitiveStub(props: { children?: unknown }) {
|
||||
return { type, props }
|
||||
}
|
||||
return {
|
||||
TooltipProvider: passthrough('TooltipProvider'),
|
||||
Tooltip: passthrough('Tooltip'),
|
||||
TooltipTrigger: passthrough('TooltipTrigger'),
|
||||
TooltipContent: passthrough('TooltipContent')
|
||||
}
|
||||
}
|
||||
|
||||
export function stubStatusDisplay(): Record<string, unknown> {
|
||||
return { buildStatusMap: () => new Map() }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user