mirror of
https://github.com/suitenumerique/meet.git
synced 2026-09-02 13:48:26 +00:00
✨(frontend) make the PiP control bar collapsible for responsiveness
The PiP window can be resized to a wide range of dimensions, so the control bar needs to adapt. Introduce a collapsible mechanism that hides or condenses controls as available width shrinks, keeping the UX usable at small sizes. Original works by @ovgdd Co-authored-by: Cyril <c.gromoff@gmail.com>
This commit is contained in:
committed by
aleb_the_flash
parent
60828ed895
commit
dee1e46173
@@ -8,18 +8,66 @@ import { HandToggle } from '@/features/rooms/livekit/components/controls/HandTog
|
|||||||
import { StartMediaButton } from '@/features/rooms/livekit/components/controls/StartMediaButton'
|
import { StartMediaButton } from '@/features/rooms/livekit/components/controls/StartMediaButton'
|
||||||
import { ReactionsToggle } from '@/features/reactions/components/ReactionsToggle'
|
import { ReactionsToggle } from '@/features/reactions/components/ReactionsToggle'
|
||||||
import { PipOptionsMenu } from './controls/PipOptionsMenu'
|
import { PipOptionsMenu } from './controls/PipOptionsMenu'
|
||||||
|
import { usePipElementSize } from '../hooks/usePipElementSize'
|
||||||
|
import { useMemo, useRef } from 'react'
|
||||||
|
|
||||||
|
export const CollapsibleControls = {
|
||||||
|
HAND: 'hand',
|
||||||
|
SCREEN_SHARE: 'screenShare',
|
||||||
|
REACTIONS: 'reactions',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type CollapsibleControl =
|
||||||
|
(typeof CollapsibleControls)[keyof typeof CollapsibleControls]
|
||||||
|
|
||||||
|
const COLLAPSE_ORDER: CollapsibleControl[] = [
|
||||||
|
CollapsibleControls.HAND,
|
||||||
|
CollapsibleControls.SCREEN_SHARE,
|
||||||
|
CollapsibleControls.REACTIONS,
|
||||||
|
]
|
||||||
|
|
||||||
|
const BUTTON_SLOT = 50
|
||||||
|
const ESSENTIAL_WIDTH = 260
|
||||||
|
|
||||||
|
const getHiddenControls = (
|
||||||
|
containerWidth: number,
|
||||||
|
showScreenShare: boolean
|
||||||
|
): Set<CollapsibleControl> => {
|
||||||
|
const hidden = new Set<CollapsibleControl>()
|
||||||
|
if (containerWidth <= 0) return hidden
|
||||||
|
|
||||||
|
const collapsible = showScreenShare
|
||||||
|
? COLLAPSE_ORDER
|
||||||
|
: COLLAPSE_ORDER.filter((c) => c !== CollapsibleControls.SCREEN_SHARE)
|
||||||
|
|
||||||
|
const available = containerWidth - ESSENTIAL_WIDTH
|
||||||
|
const maxVisible = Math.max(0, Math.floor(available / BUTTON_SLOT))
|
||||||
|
|
||||||
|
for (let i = 0; i < collapsible.length - maxVisible; i++) {
|
||||||
|
hidden.add(collapsible[i])
|
||||||
|
}
|
||||||
|
return hidden
|
||||||
|
}
|
||||||
|
|
||||||
export const PipControlBar = ({
|
export const PipControlBar = ({
|
||||||
showScreenShare,
|
showScreenShare,
|
||||||
}: {
|
}: {
|
||||||
showScreenShare: boolean
|
showScreenShare: boolean
|
||||||
}) => {
|
}) => {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const { width } = usePipElementSize(containerRef)
|
||||||
const { t } = useTranslation('rooms', {
|
const { t } = useTranslation('rooms', {
|
||||||
keyPrefix: 'pictureInPicture',
|
keyPrefix: 'pictureInPicture',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const hidden = useMemo(
|
||||||
|
() => getHiddenControls(width, showScreenShare),
|
||||||
|
[width, showScreenShare]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PipControls
|
<PipControls
|
||||||
|
ref={containerRef}
|
||||||
id="pip-control-bar"
|
id="pip-control-bar"
|
||||||
role="toolbar"
|
role="toolbar"
|
||||||
aria-label={t('controlBar')}
|
aria-label={t('controlBar')}
|
||||||
@@ -27,11 +75,13 @@ export const PipControlBar = ({
|
|||||||
<PipControlsCenter>
|
<PipControlsCenter>
|
||||||
<AudioDevicesControl hideMenu />
|
<AudioDevicesControl hideMenu />
|
||||||
<VideoDeviceControl hideMenu />
|
<VideoDeviceControl hideMenu />
|
||||||
<ReactionsToggle />
|
{!hidden.has(CollapsibleControls.REACTIONS) && <ReactionsToggle />}
|
||||||
{showScreenShare && <ScreenShareToggle />}
|
{showScreenShare && !hidden.has(CollapsibleControls.SCREEN_SHARE) && (
|
||||||
<HandToggle />
|
<ScreenShareToggle />
|
||||||
|
)}
|
||||||
|
{!hidden.has(CollapsibleControls.HAND) && <HandToggle />}
|
||||||
<StartMediaButton />
|
<StartMediaButton />
|
||||||
<PipOptionsMenu />
|
<PipOptionsMenu overflowControls={hidden} />
|
||||||
<LeaveButton />
|
<LeaveButton />
|
||||||
</PipControlsCenter>
|
</PipControlsCenter>
|
||||||
</PipControls>
|
</PipControls>
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ import { css } from '@/styled-system/css'
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { PipOptionsMenuItems } from './PipOptionsMenuItems'
|
import { PipOptionsMenuItems } from './PipOptionsMenuItems'
|
||||||
import { useDismissOnEscape } from '../../hooks/useDismissOnEscape'
|
import { useDismissOnEscape } from '../../hooks/useDismissOnEscape'
|
||||||
|
import { CollapsibleControl } from '@/features/pip/components/PipControlBar'
|
||||||
|
|
||||||
|
type PipOptionsMenuProps = {
|
||||||
|
overflowControls: Set<CollapsibleControl>
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* PiP-native options menu.
|
* PiP-native options menu.
|
||||||
*
|
*
|
||||||
@@ -35,7 +39,7 @@ import { useDismissOnEscape } from '../../hooks/useDismissOnEscape'
|
|||||||
*
|
*
|
||||||
* So in PiP we replace the primitive with this component.
|
* So in PiP we replace the primitive with this component.
|
||||||
*/
|
*/
|
||||||
export const PipOptionsMenu = () => {
|
export const PipOptionsMenu = ({ overflowControls }: PipOptionsMenuProps) => {
|
||||||
const { t } = useTranslation('rooms')
|
const { t } = useTranslation('rooms')
|
||||||
|
|
||||||
const wrapperRef = useRef<HTMLDivElement>(null)
|
const wrapperRef = useRef<HTMLDivElement>(null)
|
||||||
@@ -115,7 +119,7 @@ export const PipOptionsMenu = () => {
|
|||||||
{/* eslint-disable-next-line jsx-a11y/no-autofocus */}
|
{/* eslint-disable-next-line jsx-a11y/no-autofocus */}
|
||||||
<FocusScope autoFocus>
|
<FocusScope autoFocus>
|
||||||
<Box size="sm" type="popover" variant="dark">
|
<Box size="sm" type="popover" variant="dark">
|
||||||
<PipOptionsMenuItems />
|
<PipOptionsMenuItems overflowControls={overflowControls} />
|
||||||
</Box>
|
</Box>
|
||||||
</FocusScope>
|
</FocusScope>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,37 @@
|
|||||||
import { Menu as RACMenu } from 'react-aria-components'
|
import { Menu as RACMenu, MenuItem } from 'react-aria-components'
|
||||||
import { PictureInPictureMenuItem } from '@/features/rooms/livekit/components/controls/Options/PictureInPictureMenuItem'
|
import { PictureInPictureMenuItem } from '@/features/rooms/livekit/components/controls/Options/PictureInPictureMenuItem'
|
||||||
|
import {
|
||||||
|
CollapsibleControl,
|
||||||
|
CollapsibleControls,
|
||||||
|
} from '../PipControlBar'
|
||||||
|
import { RiArrowUpLine, RiEmotionLine, RiHand } from '@remixicon/react'
|
||||||
|
import { menuRecipe } from '@/primitives/menuRecipe.ts'
|
||||||
|
import { useReactionsToolbar } from '@/features/reactions/hooks/useReactionsToolbar'
|
||||||
|
import { useRoomContext, useTrackToggle } from '@livekit/components-react'
|
||||||
|
import { useRaisedHand } from '@/features/rooms/livekit/hooks/useRaisedHand'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Track } from 'livekit-client'
|
||||||
|
|
||||||
|
type PipOverflowItemsProps = {
|
||||||
|
overflowControls: Set<CollapsibleControl>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PipOptionsMenuItems = ({
|
||||||
|
overflowControls,
|
||||||
|
}: PipOverflowItemsProps) => {
|
||||||
|
const { t } = useTranslation('rooms')
|
||||||
|
const room = useRoomContext()
|
||||||
|
const { isHandRaised, toggleRaisedHand } = useRaisedHand({
|
||||||
|
participant: room.localParticipant,
|
||||||
|
})
|
||||||
|
const { buttonProps: screenShareProps, enabled: isScreenSharing } =
|
||||||
|
useTrackToggle({
|
||||||
|
source: Track.Source.ScreenShare,
|
||||||
|
captureOptions: { audio: true, selfBrowserSurface: 'include' },
|
||||||
|
})
|
||||||
|
const { toggle: toggleReactions } = useReactionsToolbar()
|
||||||
|
const itemClass = menuRecipe({ icon: true, variant: 'dark' }).item
|
||||||
|
|
||||||
export const PipOptionsMenuItems = () => {
|
|
||||||
return (
|
return (
|
||||||
<RACMenu
|
<RACMenu
|
||||||
style={{
|
style={{
|
||||||
@@ -10,6 +40,35 @@ export const PipOptionsMenuItems = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PictureInPictureMenuItem />
|
<PictureInPictureMenuItem />
|
||||||
|
{overflowControls.has(CollapsibleControls.REACTIONS) && (
|
||||||
|
<MenuItem onAction={toggleReactions} className={itemClass}>
|
||||||
|
<RiEmotionLine size={20} />
|
||||||
|
{t('controls.reactions.button')}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
{overflowControls.has(CollapsibleControls.SCREEN_SHARE) && (
|
||||||
|
<MenuItem
|
||||||
|
onAction={() =>
|
||||||
|
screenShareProps.onClick?.(
|
||||||
|
{} as React.MouseEvent<HTMLButtonElement>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className={itemClass}
|
||||||
|
>
|
||||||
|
<RiArrowUpLine size={20} />
|
||||||
|
{t(
|
||||||
|
isScreenSharing
|
||||||
|
? 'controls.screenShare.stop'
|
||||||
|
: 'controls.screenShare.start'
|
||||||
|
)}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
{overflowControls.has(CollapsibleControls.HAND) && (
|
||||||
|
<MenuItem onAction={toggleRaisedHand} className={itemClass}>
|
||||||
|
<RiHand size={20} />
|
||||||
|
{isHandRaised ? t('controls.hand.lower') : t('controls.hand.raise')}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
</RACMenu>
|
</RACMenu>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user