From dee1e461739924ffad8f979d08736e97fd785a72 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Fri, 22 May 2026 14:42:55 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20make=20the=20PiP=20contro?= =?UTF-8?q?l=20bar=20collapsible=20for=20responsiveness?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../features/pip/components/PipControlBar.tsx | 58 +++++++++++++++-- .../components/controls/PipOptionsMenu.tsx | 8 ++- .../controls/PipOptionsMenuItems.tsx | 63 ++++++++++++++++++- 3 files changed, 121 insertions(+), 8 deletions(-) diff --git a/src/frontend/src/features/pip/components/PipControlBar.tsx b/src/frontend/src/features/pip/components/PipControlBar.tsx index bcee1ad6..6f849db5 100644 --- a/src/frontend/src/features/pip/components/PipControlBar.tsx +++ b/src/frontend/src/features/pip/components/PipControlBar.tsx @@ -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 => { + const hidden = new Set() + 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(null) + const { width } = usePipElementSize(containerRef) const { t } = useTranslation('rooms', { keyPrefix: 'pictureInPicture', }) + const hidden = useMemo( + () => getHiddenControls(width, showScreenShare), + [width, showScreenShare] + ) + return ( - - {showScreenShare && } - + {!hidden.has(CollapsibleControls.REACTIONS) && } + {showScreenShare && !hidden.has(CollapsibleControls.SCREEN_SHARE) && ( + + )} + {!hidden.has(CollapsibleControls.HAND) && } - + diff --git a/src/frontend/src/features/pip/components/controls/PipOptionsMenu.tsx b/src/frontend/src/features/pip/components/controls/PipOptionsMenu.tsx index 6a76a983..0ed4dbc6 100644 --- a/src/frontend/src/features/pip/components/controls/PipOptionsMenu.tsx +++ b/src/frontend/src/features/pip/components/controls/PipOptionsMenu.tsx @@ -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 +} /** * 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(null) @@ -115,7 +119,7 @@ export const PipOptionsMenu = () => { {/* eslint-disable-next-line jsx-a11y/no-autofocus */} - + diff --git a/src/frontend/src/features/pip/components/controls/PipOptionsMenuItems.tsx b/src/frontend/src/features/pip/components/controls/PipOptionsMenuItems.tsx index 690248f4..a2834183 100644 --- a/src/frontend/src/features/pip/components/controls/PipOptionsMenuItems.tsx +++ b/src/frontend/src/features/pip/components/controls/PipOptionsMenuItems.tsx @@ -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 +} + +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 ( { }} > + {overflowControls.has(CollapsibleControls.REACTIONS) && ( + + + {t('controls.reactions.button')} + + )} + {overflowControls.has(CollapsibleControls.SCREEN_SHARE) && ( + + screenShareProps.onClick?.( + {} as React.MouseEvent + ) + } + className={itemClass} + > + + {t( + isScreenSharing + ? 'controls.screenShare.stop' + : 'controls.screenShare.start' + )} + + )} + {overflowControls.has(CollapsibleControls.HAND) && ( + + + {isHandRaised ? t('controls.hand.lower') : t('controls.hand.raise')} + + )} ) }