diff --git a/src/frontend/src/features/pip/components/DocumentPiPPortal.tsx b/src/frontend/src/features/pip/components/DocumentPiPPortal.tsx
index fd0e21ac..246ba2a2 100644
--- a/src/frontend/src/features/pip/components/DocumentPiPPortal.tsx
+++ b/src/frontend/src/features/pip/components/DocumentPiPPortal.tsx
@@ -5,6 +5,7 @@ import { useDocumentPiP } from '../hooks/useDocumentPiP'
import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce'
import { useRestoreFocus } from '@/hooks/useRestoreFocus'
import { UNSAFE_PortalProvider } from '@react-aria/overlays'
+import { VisualOnlyTooltipsContext } from '@/primitives/VisualOnlyTooltipsContext'
// Minimal base styles so the PiP window renders correctly on first paint.
const ensureBaseStyles = (target: Document) => {
@@ -224,7 +225,9 @@ export const DocumentPiPPortal = ({
if (!container) return null
return createPortal(
container}>
- {children}
+
+ {children}
+
,
container
)
diff --git a/src/frontend/src/primitives/Menu.tsx b/src/frontend/src/primitives/Menu.tsx
index f0fe5f59..873782ee 100644
--- a/src/frontend/src/primitives/Menu.tsx
+++ b/src/frontend/src/primitives/Menu.tsx
@@ -1,11 +1,8 @@
-import { ReactNode, useMemo } from 'react'
+import { ReactNode } from 'react'
import { MenuTrigger } from 'react-aria-components'
import { StyledPopover } from './Popover'
import { Box } from './Box'
-import {
- useOverlayBoundaryElement,
- useOverlayPortalContainer,
-} from './useOverlayPortalContainer'
+import { useOverlayBoundaryElement } from './useOverlayPortalContainer'
/**
* a Menu is a tuple of a trigger component (most usually a Button) that toggles menu items in a tooltip around the trigger
@@ -15,33 +12,22 @@ import {
export const Menu = ({
children,
variant = 'light',
- placement,
+ placement = 'top',
+ shouldFlip,
}: {
children: [trigger: ReactNode, menu: ReactNode]
variant?: 'dark' | 'light'
placement?: 'bottom' | 'top' | 'left' | 'right'
+ shouldFlip?: boolean
}) => {
const [trigger, menu] = children
const boundaryElement = useOverlayBoundaryElement()
- const portalContainer = useOverlayPortalContainer()
-
- // Detect if we're in PiP: portal container is in a different document than the main window
- const isInPiP = useMemo(
- () =>
- !!portalContainer?.ownerDocument &&
- portalContainer.ownerDocument !== document,
- [portalContainer]
- )
-
- // Default placement: 'bottom' in PiP, 'top' elsewhere (to match existing behavior)
- const defaultPlacement = isInPiP ? 'bottom' : 'top'
- const shouldFlip = isInPiP ? false : undefined
return (
{trigger}
diff --git a/src/frontend/src/primitives/TooltipWrapper.tsx b/src/frontend/src/primitives/TooltipWrapper.tsx
index 0e4f252b..a8143070 100644
--- a/src/frontend/src/primitives/TooltipWrapper.tsx
+++ b/src/frontend/src/primitives/TooltipWrapper.tsx
@@ -6,8 +6,8 @@ import {
type TooltipProps,
} from 'react-aria-components'
import { styled } from '@/styled-system/jsx'
-import { useOverlayPortalContainer } from './useOverlayPortalContainer'
import { VisualOnlyTooltip } from './VisualOnlyTooltip'
+import { useVisualOnlyTooltips } from './VisualOnlyTooltipsContext'
export type TooltipWrapperProps = {
tooltip?: string
@@ -29,28 +29,23 @@ export const TooltipWrapper = ({
}: {
children: ReactElement
} & TooltipWrapperProps) => {
- const portalContainer = useOverlayPortalContainer()
- const isExternalDocument =
- !!portalContainer?.ownerDocument &&
- portalContainer.ownerDocument !== document
+ const visualOnly = useVisualOnlyTooltips()
+ const tooltipDelay =
+ tooltipType === 'instant'
+ ? INSTANT_TOOLTIP_DELAY_MS
+ : DELAYED_TOOLTIP_DELAY_MS
- return tooltip ? (
- isExternalDocument ? (
- {children}
- ) : (
-
- {children}
- {tooltip}
-
- )
- ) : (
- children
+ if (!tooltip) return children
+
+ if (visualOnly) {
+ return {children}
+ }
+
+ return (
+
+ {children}
+ {tooltip}
+
)
}
diff --git a/src/frontend/src/primitives/VisualOnlyTooltipsContext.tsx b/src/frontend/src/primitives/VisualOnlyTooltipsContext.tsx
new file mode 100644
index 00000000..579c7638
--- /dev/null
+++ b/src/frontend/src/primitives/VisualOnlyTooltipsContext.tsx
@@ -0,0 +1,11 @@
+import { createContext, useContext } from 'react'
+
+/**
+ * When true, tooltips render as visual-only (no aria-describedby).
+ * Provided by surfaces where React Aria TooltipTrigger doesn't work
+ * correctly (e.g. cross-document portals).
+ */
+export const VisualOnlyTooltipsContext = createContext(false)
+
+export const useVisualOnlyTooltips = () =>
+ useContext(VisualOnlyTooltipsContext)