mirror of
https://github.com/suitenumerique/meet.git
synced 2026-07-26 11:58:53 +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 { ReactionsToggle } from '@/features/reactions/components/ReactionsToggle'
|
||||
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 = ({
|
||||
showScreenShare,
|
||||
}: {
|
||||
showScreenShare: boolean
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const { width } = usePipElementSize(containerRef)
|
||||
const { t } = useTranslation('rooms', {
|
||||
keyPrefix: 'pictureInPicture',
|
||||
})
|
||||
|
||||
const hidden = useMemo(
|
||||
() => getHiddenControls(width, showScreenShare),
|
||||
[width, showScreenShare]
|
||||
)
|
||||
|
||||
return (
|
||||
<PipControls
|
||||
ref={containerRef}
|
||||
id="pip-control-bar"
|
||||
role="toolbar"
|
||||
aria-label={t('controlBar')}
|
||||
@@ -27,11 +75,13 @@ export const PipControlBar = ({
|
||||
<PipControlsCenter>
|
||||
<AudioDevicesControl hideMenu />
|
||||
<VideoDeviceControl hideMenu />
|
||||
<ReactionsToggle />
|
||||
{showScreenShare && <ScreenShareToggle />}
|
||||
<HandToggle />
|
||||
{!hidden.has(CollapsibleControls.REACTIONS) && <ReactionsToggle />}
|
||||
{showScreenShare && !hidden.has(CollapsibleControls.SCREEN_SHARE) && (
|
||||
<ScreenShareToggle />
|
||||
)}
|
||||
{!hidden.has(CollapsibleControls.HAND) && <HandToggle />}
|
||||
<StartMediaButton />
|
||||
<PipOptionsMenu />
|
||||
<PipOptionsMenu overflowControls={hidden} />
|
||||
<LeaveButton />
|
||||
</PipControlsCenter>
|
||||
</PipControls>
|
||||
|
||||
@@ -6,7 +6,11 @@ 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.
|
||||
*
|
||||
@@ -35,7 +39,7 @@ import { useDismissOnEscape } from '../../hooks/useDismissOnEscape'
|
||||
*
|
||||
* So in PiP we replace the primitive with this component.
|
||||
*/
|
||||
export const PipOptionsMenu = () => {
|
||||
export const PipOptionsMenu = ({ overflowControls }: PipOptionsMenuProps) => {
|
||||
const { t } = useTranslation('rooms')
|
||||
|
||||
const wrapperRef = useRef<HTMLDivElement>(null)
|
||||
@@ -115,7 +119,7 @@ export const PipOptionsMenu = () => {
|
||||
{/* eslint-disable-next-line jsx-a11y/no-autofocus */}
|
||||
<FocusScope autoFocus>
|
||||
<Box size="sm" type="popover" variant="dark">
|
||||
<PipOptionsMenuItems />
|
||||
<PipOptionsMenuItems overflowControls={overflowControls} />
|
||||
</Box>
|
||||
</FocusScope>
|
||||
</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 {
|
||||
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 (
|
||||
<RACMenu
|
||||
style={{
|
||||
@@ -10,6 +40,35 @@ export const PipOptionsMenuItems = () => {
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user