mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-27 18:56:58 +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 { MenuTrigger } from 'react-aria-components'
|
||||||
import { StyledPopover } from './Popover'
|
import { StyledPopover } from './Popover'
|
||||||
import { Box } from './Box'
|
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
|
* 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 = ({
|
export const Menu = ({
|
||||||
children,
|
children,
|
||||||
@@ -16,10 +22,30 @@ export const Menu = ({
|
|||||||
placement?: 'bottom' | 'top' | 'left' | 'right'
|
placement?: 'bottom' | 'top' | 'left' | 'right'
|
||||||
}) => {
|
}) => {
|
||||||
const [trigger, menu] = children
|
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 (
|
return (
|
||||||
<MenuTrigger>
|
<MenuTrigger>
|
||||||
{trigger}
|
{trigger}
|
||||||
<StyledPopover placement={placement}>
|
<StyledPopover
|
||||||
|
placement={placement ?? defaultPlacement}
|
||||||
|
shouldFlip={shouldFlip}
|
||||||
|
boundaryElement={boundaryElement}
|
||||||
|
>
|
||||||
<Box size="sm" type="popover" variant={variant}>
|
<Box size="sm" type="popover" variant={variant}>
|
||||||
{menu}
|
{menu}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
} from 'react-aria-components'
|
} from 'react-aria-components'
|
||||||
import { styled } from '@/styled-system/jsx'
|
import { styled } from '@/styled-system/jsx'
|
||||||
import { Box } from './Box'
|
import { Box } from './Box'
|
||||||
|
import { useOverlayBoundaryElement } from './useOverlayPortalContainer'
|
||||||
|
|
||||||
export const StyledPopover = styled(RACPopover, {
|
export const StyledPopover = styled(RACPopover, {
|
||||||
base: {
|
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>.
|
* 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.
|
* 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 = ({
|
export const Popover = ({
|
||||||
children,
|
children,
|
||||||
@@ -82,10 +85,11 @@ export const Popover = ({
|
|||||||
withArrow?: boolean
|
withArrow?: boolean
|
||||||
} & Omit<DialogProps, 'children'>) => {
|
} & Omit<DialogProps, 'children'>) => {
|
||||||
const [trigger, popoverContent] = children
|
const [trigger, popoverContent] = children
|
||||||
|
const boundaryElement = useOverlayBoundaryElement()
|
||||||
return (
|
return (
|
||||||
<DialogTrigger>
|
<DialogTrigger>
|
||||||
{trigger}
|
{trigger}
|
||||||
<StyledPopover>
|
<StyledPopover boundaryElement={boundaryElement}>
|
||||||
{withArrow && (
|
{withArrow && (
|
||||||
<StyledOverlayArrow variant={variant}>
|
<StyledOverlayArrow variant={variant}>
|
||||||
<svg width={12} height={12} viewBox="0 0 12 12">
|
<svg width={12} height={12} viewBox="0 0 12 12">
|
||||||
|
|||||||
@@ -6,12 +6,17 @@ import {
|
|||||||
type TooltipProps,
|
type TooltipProps,
|
||||||
} from 'react-aria-components'
|
} from 'react-aria-components'
|
||||||
import { styled } from '@/styled-system/jsx'
|
import { styled } from '@/styled-system/jsx'
|
||||||
|
import { useOverlayPortalContainer } from './useOverlayPortalContainer'
|
||||||
|
import { VisualOnlyTooltip } from './VisualOnlyTooltip'
|
||||||
|
|
||||||
export type TooltipWrapperProps = {
|
export type TooltipWrapperProps = {
|
||||||
tooltip?: string
|
tooltip?: string
|
||||||
tooltipType?: 'instant' | 'delayed'
|
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)
|
* Wrap a component you want to apply a tooltip on (for example a Button)
|
||||||
*
|
*
|
||||||
@@ -24,11 +29,25 @@ export const TooltipWrapper = ({
|
|||||||
}: {
|
}: {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
} & TooltipWrapperProps) => {
|
} & TooltipWrapperProps) => {
|
||||||
|
const portalContainer = useOverlayPortalContainer()
|
||||||
|
const isExternalDocument =
|
||||||
|
portalContainer && portalContainer.ownerDocument !== document
|
||||||
|
|
||||||
return tooltip ? (
|
return tooltip ? (
|
||||||
<TooltipTrigger delay={tooltipType === 'instant' ? 150 : 1000}>
|
isExternalDocument ? (
|
||||||
{children}
|
<VisualOnlyTooltip tooltip={tooltip}>{children}</VisualOnlyTooltip>
|
||||||
<Tooltip>{tooltip}</Tooltip>
|
) : (
|
||||||
</TooltipTrigger>
|
<TooltipTrigger
|
||||||
|
delay={
|
||||||
|
tooltipType === 'instant'
|
||||||
|
? INSTANT_TOOLTIP_DELAY_MS
|
||||||
|
: DELAYED_TOOLTIP_DELAY_MS
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<Tooltip>{tooltip}</Tooltip>
|
||||||
|
</TooltipTrigger>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
children
|
children
|
||||||
)
|
)
|
||||||
@@ -53,11 +72,11 @@ const StyledTooltip = styled(RACTooltip, {
|
|||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
transform: 'translate3d(0, 0, 0)',
|
transform: 'translate3d(0, 0, 0)',
|
||||||
'&[data-placement=top]': {
|
'&[data-placement=top]': {
|
||||||
marginBottom: '8px',
|
marginBottom: '2px',
|
||||||
'--origin': 'translateY(4px)',
|
'--origin': 'translateY(4px)',
|
||||||
},
|
},
|
||||||
'&[data-placement=bottom]': {
|
'&[data-placement=bottom]': {
|
||||||
marginTop: '8px',
|
marginTop: '2px',
|
||||||
'--origin': 'translateY(-4px)',
|
'--origin': 'translateY(-4px)',
|
||||||
},
|
},
|
||||||
'&[data-placement=right]': {
|
'&[data-placement=right]': {
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import {
|
|||||||
type ReactElement,
|
type ReactElement,
|
||||||
cloneElement,
|
cloneElement,
|
||||||
isValidElement,
|
isValidElement,
|
||||||
useRef,
|
useMemo, useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { createPortal } from 'react-dom'
|
import { createPortal } from 'react-dom'
|
||||||
import { css } from '@/styled-system/css'
|
import { css } from '@/styled-system/css'
|
||||||
|
import { useUNSAFE_PortalContext } from '@react-aria/overlays'
|
||||||
|
|
||||||
export type VisualOnlyTooltipProps = {
|
export type VisualOnlyTooltipProps = {
|
||||||
children: ReactElement
|
children: ReactElement
|
||||||
@@ -32,6 +33,7 @@ export const VisualOnlyTooltip = ({
|
|||||||
tooltipPosition = 'top',
|
tooltipPosition = 'top',
|
||||||
}: VisualOnlyTooltipProps) => {
|
}: VisualOnlyTooltipProps) => {
|
||||||
const [isVisible, setIsVisible] = useState(false)
|
const [isVisible, setIsVisible] = useState(false)
|
||||||
|
const { getContainer } = useUNSAFE_PortalContext()
|
||||||
const wrapperRef = useRef<HTMLDivElement>(null)
|
const wrapperRef = useRef<HTMLDivElement>(null)
|
||||||
const [position, setPosition] = useState<{
|
const [position, setPosition] = useState<{
|
||||||
top: number
|
top: number
|
||||||
@@ -56,6 +58,11 @@ export const VisualOnlyTooltip = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tooltipData = isVisible && position ? { isVisible, position } : null
|
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)
|
const wrappedChild = isValidElement(children)
|
||||||
? cloneElement(children, {
|
? cloneElement(children, {
|
||||||
...(ariaLabel ? { 'aria-label': ariaLabel } : {}),
|
...(ariaLabel ? { 'aria-label': ariaLabel } : {}),
|
||||||
@@ -73,7 +80,7 @@ export const VisualOnlyTooltip = ({
|
|||||||
>
|
>
|
||||||
{wrappedChild}
|
{wrappedChild}
|
||||||
</div>
|
</div>
|
||||||
{tooltipData &&
|
{tooltipData && portalContainer &&
|
||||||
createPortal(
|
createPortal(
|
||||||
<div
|
<div
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@@ -116,7 +123,7 @@ export const VisualOnlyTooltip = ({
|
|||||||
>
|
>
|
||||||
{tooltip}
|
{tooltip}
|
||||||
</div>,
|
</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