mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-08 01:43:17 +00:00
79245389ce
Add react-aria 3.49.0 + react-stately 3.47.0 as direct deps and bump react-aria-components to 1.18.0 so react-stately resolves to one version. Fixes the nominal Timer type clash and missing @react-stately/toast imports introduced by the React Aria v1.17 monopackage consolidation.
130 lines
4.3 KiB
TypeScript
130 lines
4.3 KiB
TypeScript
import { useEffect, useRef, useState } from 'react'
|
|
import { RiMoreFill } from '@remixicon/react'
|
|
import { FocusScope } from 'react-aria'
|
|
import { Box, Button } from '@/primitives'
|
|
import { css } from '@/styled-system/css'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { PipOptionsMenuItems } from './PipOptionsMenuItems'
|
|
import { useDismissOnEscape } from '../../hooks/useDismissOnEscape'
|
|
import { CollapsibleControl } from '@/features/pip/components/PipControlBar'
|
|
|
|
type PipOptionsMenuProps = {
|
|
overflowControls: Set<CollapsibleControl>
|
|
}
|
|
/**
|
|
* PiP-native options menu.
|
|
*
|
|
* Why not use the shared `<Menu>` primitive (React Aria's MenuTrigger +
|
|
* Popover)?
|
|
*
|
|
* React Aria positions popovers by reading `window.innerWidth` and
|
|
* `window.innerHeight` to know where the viewport edges are. The problem:
|
|
* it reads them from the *module-global* `window`, which is always the
|
|
* main browser window — even when the trigger button lives inside the
|
|
* Picture-in-Picture window (a separate `document` with its own, smaller
|
|
* viewport). React Aria therefore thinks it has the full main-window
|
|
* space to work with, and places the popover using those coordinates.
|
|
* The result is a menu that appears off-screen, clipped, or in the wrong
|
|
* corner of the PiP.
|
|
*
|
|
* The same single-document assumption breaks focus: React Aria's focus
|
|
* management and outside-click detection listen on the main `document`,
|
|
* so when the user interacts in the PiP, the menu doesn't receive focus
|
|
* on open, Escape doesn't restore focus to the trigger, and clicking
|
|
* outside doesn't dismiss it.
|
|
*
|
|
* These aren't bugs we can fix from the outside — the `window` and
|
|
* `document` references are baked into React Aria internals, with no
|
|
* prop or context to override them.
|
|
*
|
|
* So in PiP we replace the primitive with this component.
|
|
*/
|
|
export const PipOptionsMenu = ({ overflowControls }: PipOptionsMenuProps) => {
|
|
const { t } = useTranslation('rooms')
|
|
|
|
const wrapperRef = useRef<HTMLDivElement>(null)
|
|
const triggerRef = useRef<HTMLButtonElement>(null)
|
|
|
|
const [isOpen, setIsOpen] = useState(false)
|
|
const label = t('options.buttonLabel')
|
|
|
|
useDismissOnEscape(wrapperRef, isOpen, () => {
|
|
setIsOpen(false)
|
|
requestAnimationFrame(() => triggerRef.current?.focus())
|
|
})
|
|
|
|
useEffect(() => {
|
|
if (!isOpen) return
|
|
const doc = wrapperRef.current?.ownerDocument ?? document
|
|
|
|
const handleMenuItemClick = (event: MouseEvent) => {
|
|
const target = event.target as HTMLElement | null
|
|
const wrapper = wrapperRef.current
|
|
if (!wrapper || !target) return
|
|
if (wrapper.querySelector('button')?.contains(target)) return
|
|
if (target.closest('[role="menuitem"]')) {
|
|
requestAnimationFrame(() => {
|
|
setIsOpen(false)
|
|
triggerRef.current?.focus()
|
|
})
|
|
}
|
|
}
|
|
|
|
const handleOutsideClick = (event: MouseEvent) => {
|
|
const target = event.target as HTMLElement | null
|
|
const wrapper = wrapperRef.current
|
|
if (!wrapper || !target) return
|
|
if (wrapper.contains(target)) return
|
|
setIsOpen(false)
|
|
}
|
|
|
|
doc.addEventListener('click', handleMenuItemClick, true)
|
|
doc.addEventListener('mousedown', handleOutsideClick, true)
|
|
return () => {
|
|
doc.removeEventListener('click', handleMenuItemClick, true)
|
|
doc.removeEventListener('mousedown', handleOutsideClick, true)
|
|
}
|
|
}, [isOpen])
|
|
|
|
return (
|
|
<div
|
|
ref={wrapperRef}
|
|
className={css({
|
|
position: 'relative',
|
|
})}
|
|
>
|
|
<Button
|
|
ref={triggerRef}
|
|
id="room-options-trigger"
|
|
square
|
|
variant="primaryDark"
|
|
aria-label={label}
|
|
aria-haspopup="menu"
|
|
aria-expanded={isOpen}
|
|
tooltip={label}
|
|
onPress={() => setIsOpen(!isOpen)}
|
|
>
|
|
<RiMoreFill />
|
|
</Button>
|
|
{isOpen && (
|
|
<div
|
|
className={css({
|
|
position: 'absolute',
|
|
left: '50%',
|
|
bottom: 'calc(100% + 0.85rem)',
|
|
transform: 'translateX(-50%)',
|
|
zIndex: 10,
|
|
})}
|
|
>
|
|
{/* eslint-disable-next-line jsx-a11y/no-autofocus*/}
|
|
<FocusScope autoFocus>
|
|
<Box size="sm" type="popover" variant="dark">
|
|
<PipOptionsMenuItems overflowControls={overflowControls} />
|
|
</Box>
|
|
</FocusScope>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|