diff --git a/src/frontend/src/features/rooms/livekit/components/controls/Device/AudioDevicesControl.tsx b/src/frontend/src/features/rooms/livekit/components/controls/Device/AudioDevicesControl.tsx index 6132bbc0..3da291cd 100644 --- a/src/frontend/src/features/rooms/livekit/components/controls/Device/AudioDevicesControl.tsx +++ b/src/frontend/src/features/rooms/livekit/components/controls/Device/AudioDevicesControl.tsx @@ -1,3 +1,4 @@ +import React from 'react' import { useTranslation } from 'react-i18next' import { useTrackToggle, UseTrackToggleProps } from '@livekit/components-react' import { Button, Popover } from '@/primitives' @@ -9,13 +10,13 @@ import { css } from '@/styled-system/css' import { usePersistentUserChoices } from '../../../hooks/usePersistentUserChoices' import { useCanPublishTrack } from '../../../hooks/useCanPublishTrack' import { useCannotUseDevice } from '../../../hooks/useCannotUseDevice' -import * as React from 'react' import { SelectDevice } from './SelectDevice' import { SettingsButton } from './SettingsButton' import { SettingsDialogExtendedKey } from '@/features/settings/type' import { TrackSource } from '@livekit/protocol' import Source = Track.Source import { isSafari } from '@/utils/livekit' +import { useFocusOnOpen } from '@/hooks/useFocusOnOpen' type AudioDevicesControlProps = Omit< UseTrackToggleProps, @@ -29,6 +30,8 @@ export const AudioDevicesControl = ({ ...props }: AudioDevicesControlProps) => { const { t } = useTranslation('rooms', { keyPrefix: 'selectDevice' }) + const [isMenuOpen, setIsMenuOpen] = React.useState(false) + const popoverContentRef = React.useRef(null) const { userChoices: { audioDeviceId, audioOutputDeviceId }, @@ -55,6 +58,12 @@ export const AudioDevicesControl = ({ const canPublishTrack = useCanPublishTrack(TrackSource.MICROPHONE) + useFocusOnOpen(isMenuOpen, popoverContentRef, { + selector: + '[data-attr="audio-input-select"] button, [data-attr="audio-input-select"] [role="combobox"]', + delayMs: 250, + }) + return (
{!hideMenu && ( - + {({ close }) => (
{ admin: null, options: null, effects: null, + cameraMenu: null, }) const setTrigger = (key: SidePanelTriggerKey, el: HTMLElement | null) => { diff --git a/src/frontend/src/features/rooms/livekit/contexts/sidePanelContextValue.ts b/src/frontend/src/features/rooms/livekit/contexts/sidePanelContextValue.ts index 65b9cf6a..25d9249f 100644 --- a/src/frontend/src/features/rooms/livekit/contexts/sidePanelContextValue.ts +++ b/src/frontend/src/features/rooms/livekit/contexts/sidePanelContextValue.ts @@ -7,6 +7,7 @@ export type SidePanelTriggerKey = | 'admin' | 'options' | 'effects' + | 'cameraMenu' export type SidePanelContextValue = { panelRef: React.RefObject diff --git a/src/frontend/src/hooks/useFocusOnOpen.ts b/src/frontend/src/hooks/useFocusOnOpen.ts new file mode 100644 index 00000000..d36390ba --- /dev/null +++ b/src/frontend/src/hooks/useFocusOnOpen.ts @@ -0,0 +1,24 @@ +import { useEffect } from 'react' + +type UseFocusOnOpenOptions = { + selector: string + delayMs?: number + preventScroll?: boolean +} + +export const useFocusOnOpen = ( + isOpen: boolean, + containerRef: React.RefObject, + { selector, delayMs = 0, preventScroll = true }: UseFocusOnOpenOptions +) => { + useEffect(() => { + if (!isOpen) return + const timer = setTimeout(() => { + requestAnimationFrame(() => { + const first = containerRef.current?.querySelector(selector) + first?.focus({ preventScroll }) + }) + }, delayMs) + return () => clearTimeout(timer) + }, [containerRef, delayMs, isOpen, preventScroll, selector]) +} diff --git a/src/frontend/src/hooks/useRestoreFocus.ts b/src/frontend/src/hooks/useRestoreFocus.ts index 14992d10..95ec47e4 100644 --- a/src/frontend/src/hooks/useRestoreFocus.ts +++ b/src/frontend/src/hooks/useRestoreFocus.ts @@ -41,6 +41,13 @@ export function useRestoreFocus( document.addEventListener('keydown', handleKeyDown) document.addEventListener('mousedown', handleMouseDown) + return () => { + document.removeEventListener('keydown', handleKeyDown) + document.removeEventListener('mousedown', handleMouseDown) + } + }, []) + + useEffect(() => { const wasOpen = prevIsOpenRef.current // Just opened @@ -87,8 +94,6 @@ export function useRestoreFocus( // Cleanup: remove focus ring if component unmounts before focus changes return () => { - document.removeEventListener('keydown', handleKeyDown) - document.removeEventListener('mousedown', handleMouseDown) cleanupRef.current?.() cleanupRef.current = null } diff --git a/src/frontend/src/primitives/Popover.tsx b/src/frontend/src/primitives/Popover.tsx index 5ccd995f..6ddb4db2 100644 --- a/src/frontend/src/primitives/Popover.tsx +++ b/src/frontend/src/primitives/Popover.tsx @@ -70,6 +70,9 @@ export const Popover = ({ children, variant = 'light', withArrow = true, + isOpen, + defaultOpen, + onOpenChange, ...dialogProps }: { children: [ @@ -80,10 +83,17 @@ export const Popover = ({ ] variant?: 'dark' | 'light' withArrow?: boolean + isOpen?: boolean + defaultOpen?: boolean + onOpenChange?: (isOpen: boolean) => void } & Omit) => { const [trigger, popoverContent] = children return ( - + {trigger} {withArrow && (