mirror of
https://github.com/suitenumerique/meet.git
synced 2026-07-26 11:58:53 +00:00
💄(fix) align overlays in pip
route menus/tooltips to the pip document
This commit is contained in:
@@ -1,10 +1,16 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { ReactNode, useMemo } from 'react'
|
||||
import { MenuTrigger } from 'react-aria-components'
|
||||
import { StyledPopover } from './Popover'
|
||||
import { Box } from './Box'
|
||||
import {
|
||||
useOverlayBoundaryElement,
|
||||
useOverlayPortalContainer,
|
||||
} 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
|
||||
*
|
||||
* Uses UNSAFE_PortalProvider context automatically for portal container (no need for UNSTABLE_portalContainer).
|
||||
*/
|
||||
export const Menu = ({
|
||||
children,
|
||||
@@ -16,10 +22,30 @@ export const Menu = ({
|
||||
placement?: 'bottom' | 'top' | 'left' | 'right'
|
||||
}) => {
|
||||
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 &&
|
||||
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}>
|
||||
<StyledPopover
|
||||
placement={placement ?? defaultPlacement}
|
||||
shouldFlip={shouldFlip}
|
||||
boundaryElement={boundaryElement}
|
||||
>
|
||||
<Box size="sm" type="popover" variant={variant}>
|
||||
{menu}
|
||||
</Box>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from 'react-aria-components'
|
||||
import { styled } from '@/styled-system/jsx'
|
||||
import { Box } from './Box'
|
||||
import { useOverlayBoundaryElement } from './useOverlayPortalContainer'
|
||||
|
||||
export const StyledPopover = styled(RACPopover, {
|
||||
base: {
|
||||
@@ -65,6 +66,8 @@ const StyledOverlayArrow = styled(OverlayArrow, {
|
||||
*
|
||||
* Note: to show a list of actionable items, like a dropdown menu, prefer using a <Menu> or <Select>.
|
||||
* This is here when needing to show unrestricted content in a box.
|
||||
*
|
||||
* Uses UNSAFE_PortalProvider context automatically for portal container (no need for UNSTABLE_portalContainer).
|
||||
*/
|
||||
export const Popover = ({
|
||||
children,
|
||||
@@ -82,10 +85,11 @@ export const Popover = ({
|
||||
withArrow?: boolean
|
||||
} & Omit<DialogProps, 'children'>) => {
|
||||
const [trigger, popoverContent] = children
|
||||
const boundaryElement = useOverlayBoundaryElement()
|
||||
return (
|
||||
<DialogTrigger>
|
||||
{trigger}
|
||||
<StyledPopover>
|
||||
<StyledPopover boundaryElement={boundaryElement}>
|
||||
{withArrow && (
|
||||
<StyledOverlayArrow variant={variant}>
|
||||
<svg width={12} height={12} viewBox="0 0 12 12">
|
||||
|
||||
@@ -6,12 +6,17 @@ import {
|
||||
type TooltipProps,
|
||||
} from 'react-aria-components'
|
||||
import { styled } from '@/styled-system/jsx'
|
||||
import { useOverlayPortalContainer } from './useOverlayPortalContainer'
|
||||
import { VisualOnlyTooltip } from './VisualOnlyTooltip'
|
||||
|
||||
export type TooltipWrapperProps = {
|
||||
tooltip?: string
|
||||
tooltipType?: 'instant' | 'delayed'
|
||||
}
|
||||
|
||||
const INSTANT_TOOLTIP_DELAY_MS = 150
|
||||
const DELAYED_TOOLTIP_DELAY_MS = 1000
|
||||
|
||||
/**
|
||||
* Wrap a component you want to apply a tooltip on (for example a Button)
|
||||
*
|
||||
@@ -24,11 +29,25 @@ export const TooltipWrapper = ({
|
||||
}: {
|
||||
children: ReactNode
|
||||
} & TooltipWrapperProps) => {
|
||||
const portalContainer = useOverlayPortalContainer()
|
||||
const isExternalDocument =
|
||||
portalContainer && portalContainer.ownerDocument !== document
|
||||
|
||||
return tooltip ? (
|
||||
<TooltipTrigger delay={tooltipType === 'instant' ? 150 : 1000}>
|
||||
{children}
|
||||
<Tooltip>{tooltip}</Tooltip>
|
||||
</TooltipTrigger>
|
||||
isExternalDocument ? (
|
||||
<VisualOnlyTooltip tooltip={tooltip}>{children}</VisualOnlyTooltip>
|
||||
) : (
|
||||
<TooltipTrigger
|
||||
delay={
|
||||
tooltipType === 'instant'
|
||||
? INSTANT_TOOLTIP_DELAY_MS
|
||||
: DELAYED_TOOLTIP_DELAY_MS
|
||||
}
|
||||
>
|
||||
{children}
|
||||
<Tooltip>{tooltip}</Tooltip>
|
||||
</TooltipTrigger>
|
||||
)
|
||||
) : (
|
||||
children
|
||||
)
|
||||
@@ -53,11 +72,11 @@ const StyledTooltip = styled(RACTooltip, {
|
||||
fontSize: 14,
|
||||
transform: 'translate3d(0, 0, 0)',
|
||||
'&[data-placement=top]': {
|
||||
marginBottom: '8px',
|
||||
marginBottom: '2px',
|
||||
'--origin': 'translateY(4px)',
|
||||
},
|
||||
'&[data-placement=bottom]': {
|
||||
marginTop: '8px',
|
||||
marginTop: '2px',
|
||||
'--origin': 'translateY(-4px)',
|
||||
},
|
||||
'&[data-placement=right]': {
|
||||
|
||||
@@ -2,11 +2,12 @@ import {
|
||||
type ReactElement,
|
||||
cloneElement,
|
||||
isValidElement,
|
||||
useRef,
|
||||
useMemo, useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { useUNSAFE_PortalContext } from '@react-aria/overlays'
|
||||
|
||||
export type VisualOnlyTooltipProps = {
|
||||
children: ReactElement
|
||||
@@ -32,6 +33,7 @@ export const VisualOnlyTooltip = ({
|
||||
tooltipPosition = 'top',
|
||||
}: VisualOnlyTooltipProps) => {
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
const { getContainer } = useUNSAFE_PortalContext()
|
||||
const wrapperRef = useRef<HTMLDivElement>(null)
|
||||
const [position, setPosition] = useState<{
|
||||
top: number
|
||||
@@ -56,6 +58,11 @@ export const VisualOnlyTooltip = ({
|
||||
}
|
||||
|
||||
const tooltipData = isVisible && position ? { isVisible, position } : null
|
||||
|
||||
const portalContainer = useMemo(() => {
|
||||
if (getContainer) return getContainer()
|
||||
return wrapperRef.current?.ownerDocument?.body ?? document.body
|
||||
}, [getContainer])
|
||||
const wrappedChild = isValidElement(children)
|
||||
? cloneElement(children, {
|
||||
...(ariaLabel ? { 'aria-label': ariaLabel } : {}),
|
||||
@@ -73,7 +80,7 @@ export const VisualOnlyTooltip = ({
|
||||
>
|
||||
{wrappedChild}
|
||||
</div>
|
||||
{tooltipData &&
|
||||
{tooltipData && portalContainer &&
|
||||
createPortal(
|
||||
<div
|
||||
aria-hidden="true"
|
||||
@@ -116,7 +123,7 @@ export const VisualOnlyTooltip = ({
|
||||
>
|
||||
{tooltip}
|
||||
</div>,
|
||||
document.body
|
||||
portalContainer
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useUNSAFE_PortalContext } from '@react-aria/overlays'
|
||||
|
||||
export const useOverlayPortalContainer = () => {
|
||||
const { getContainer } = useUNSAFE_PortalContext()
|
||||
|
||||
// Read the portal container provided by UNSAFE_PortalProvider.
|
||||
// This is how overlays know which document/window they should render into.
|
||||
// "UNSAFE" means we're overriding the library default container on purpose.
|
||||
return useMemo(() => getContainer?.() ?? undefined, [getContainer])
|
||||
}
|
||||
|
||||
export const useOverlayBoundaryElement = () => {
|
||||
const portalContainer = useOverlayPortalContainer()
|
||||
return useMemo(() => {
|
||||
// Use the portal container as the positioning boundary.
|
||||
// In PiP this keeps overlays positioned relative to the PiP window.
|
||||
if (portalContainer) return portalContainer
|
||||
return undefined
|
||||
}, [portalContainer])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user