♻️ decouple PiP behavior from shared primitives

Remove PiP-specific branching from Menu/Tooltip primitives via explicit props and context.
This commit is contained in:
Cyril
2026-05-07 11:25:58 +02:00
parent f9ca489b2f
commit 5a508aeb31
4 changed files with 38 additions and 43 deletions
@@ -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(
<UNSAFE_PortalProvider getContainer={() => container}>
{children}
<VisualOnlyTooltipsContext.Provider value={true}>
{children}
</VisualOnlyTooltipsContext.Provider>
</UNSAFE_PortalProvider>,
container
)
+6 -20
View File
@@ -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 (
<MenuTrigger>
{trigger}
<StyledPopover
placement={placement ?? defaultPlacement}
placement={placement}
shouldFlip={shouldFlip}
boundaryElement={boundaryElement}
>
+17 -22
View File
@@ -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 ? (
<VisualOnlyTooltip tooltip={tooltip}>{children}</VisualOnlyTooltip>
) : (
<TooltipTrigger
delay={
tooltipType === 'instant'
? INSTANT_TOOLTIP_DELAY_MS
: DELAYED_TOOLTIP_DELAY_MS
}
>
{children}
<Tooltip>{tooltip}</Tooltip>
</TooltipTrigger>
)
) : (
children
if (!tooltip) return children
if (visualOnly) {
return <VisualOnlyTooltip tooltip={tooltip}>{children}</VisualOnlyTooltip>
}
return (
<TooltipTrigger delay={tooltipDelay}>
{children}
<Tooltip>{tooltip}</Tooltip>
</TooltipTrigger>
)
}
@@ -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)