From fa853431470bf4b91be04e9654c486c1e3586da5 Mon Sep 17 00:00:00 2001 From: Cyril Date: Wed, 1 Jul 2026 10:15:06 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BF=EF=B8=8F(frontend)=20fix=20focus=20re?= =?UTF-8?q?store=20when=20switching=20side=20panels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit restore trigger focus on panel switch while panel remains open --- .../rooms/livekit/components/SidePanel.tsx | 12 ++++++--- .../rooms/livekit/components/Tools.tsx | 10 ++++++-- .../features/rooms/livekit/prefabs/Chat.tsx | 3 ++- src/frontend/src/hooks/useRestoreFocus.ts | 25 ++++++++++++++++++- 4 files changed, 42 insertions(+), 8 deletions(-) diff --git a/src/frontend/src/features/rooms/livekit/components/SidePanel.tsx b/src/frontend/src/features/rooms/livekit/components/SidePanel.tsx index dedb495f..444aa927 100644 --- a/src/frontend/src/features/rooms/livekit/components/SidePanel.tsx +++ b/src/frontend/src/features/rooms/livekit/components/SidePanel.tsx @@ -177,17 +177,21 @@ export const SidePanel = () => { const asideRef = useRef(null) - const panelManagesFocus = activePanelId === PanelId.CHAT - const focusAside = useCallback(() => { requestAnimationFrame(() => { asideRef.current?.focus({ preventScroll: true }) }) }, []) - useRestoreFocus(isSidePanelOpen && !panelManagesFocus, { - onOpened: focusAside, + const handlePanelOpened = useCallback(() => { + if (activePanelId === PanelId.CHAT) return + focusAside() + }, [activePanelId, focusAside]) + + useRestoreFocus(isSidePanelOpen, { + onOpened: handlePanelOpened, preventScroll: true, + activeKey: activePanelId, }) return ( diff --git a/src/frontend/src/features/rooms/livekit/components/Tools.tsx b/src/frontend/src/features/rooms/livekit/components/Tools.tsx index a187ed26..7449077d 100644 --- a/src/frontend/src/features/rooms/livekit/components/Tools.tsx +++ b/src/frontend/src/features/rooms/livekit/components/Tools.tsx @@ -95,8 +95,13 @@ const ToolButton = ({ export const Tools = () => { const { data } = useConfig() - const { openTranscript, openScreenRecording, activeSubPanelId, isToolsOpen } = - useSidePanel() + const { + openTranscript, + openScreenRecording, + activeSubPanelId, + isToolsOpen, + isSidePanelOpen, + } = useSidePanel() const { t } = useTranslation('rooms', { keyPrefix: 'moreTools' }) // Restore focus to the element that opened the Tools panel @@ -113,6 +118,7 @@ export const Tools = () => { }, restoreFocusRaf: true, preventScroll: true, + shouldRestoreOnClose: () => !isSidePanelOpen, }) const isTranscriptEnabled = useIsRecordingModeEnabled( diff --git a/src/frontend/src/features/rooms/livekit/prefabs/Chat.tsx b/src/frontend/src/features/rooms/livekit/prefabs/Chat.tsx index d795468b..72fc8999 100644 --- a/src/frontend/src/features/rooms/livekit/prefabs/Chat.tsx +++ b/src/frontend/src/features/rooms/livekit/prefabs/Chat.tsx @@ -37,7 +37,7 @@ export function Chat({ ...props }: ChatProps) { const room = useRoomContext() const { send, chatMessages, isSending } = useChat() - const { isChatOpen } = useSidePanel() + const { isChatOpen, isSidePanelOpen } = useSidePanel() const chatSnap = useSnapshot(chatStore) // Keep track of the element that opened the chat so we can restore focus @@ -51,6 +51,7 @@ export function Chat({ ...props }: ChatProps) { }) }, preventScroll: true, + shouldRestoreOnClose: () => !isSidePanelOpen, }) // Use useParticipants hook to trigger a re-render when the participant list changes. diff --git a/src/frontend/src/hooks/useRestoreFocus.ts b/src/frontend/src/hooks/useRestoreFocus.ts index 359003b6..70c670f7 100644 --- a/src/frontend/src/hooks/useRestoreFocus.ts +++ b/src/frontend/src/hooks/useRestoreFocus.ts @@ -6,6 +6,10 @@ export type RestoreFocusOptions = { onClosed?: () => void restoreFocusRaf?: boolean preventScroll?: boolean + /** When the panel stays open but its content changes, update the restore target. */ + activeKey?: string | null + /** Return false to skip restoring focus on close (e.g. when switching to another panel). */ + shouldRestoreOnClose?: () => boolean } /** @@ -22,9 +26,12 @@ export function useRestoreFocus( onClosed, restoreFocusRaf = false, preventScroll = true, + activeKey, + shouldRestoreOnClose, } = options const prevIsOpenRef = useRef(false) + const prevActiveKeyRef = useRef(activeKey) const triggerRef = useRef(null) useEffect(() => { @@ -37,25 +44,41 @@ export function useRestoreFocus( onOpened?.() } + // Panel switched while staying open + if (wasOpen && isOpen && activeKey !== prevActiveKeyRef.current) { + const activeEl = document.activeElement as HTMLElement | null + triggerRef.current = resolveTrigger ? resolveTrigger(activeEl) : activeEl + onOpened?.() + } + // Just closed if (wasOpen && !isOpen) { const trigger = triggerRef.current - if (trigger && document.contains(trigger)) { + const shouldRestore = + (shouldRestoreOnClose?.() ?? true) && + trigger && + document.contains(trigger) + + if (shouldRestore) { const focus = () => trigger.focus({ preventScroll }) if (restoreFocusRaf) requestAnimationFrame(focus) else focus() } + triggerRef.current = null onClosed?.() } prevIsOpenRef.current = isOpen + prevActiveKeyRef.current = activeKey }, [ isOpen, + activeKey, onClosed, onOpened, preventScroll, resolveTrigger, restoreFocusRaf, + shouldRestoreOnClose, ]) }