️(frontend) fix focus restore when switching side panels

restore trigger focus on panel switch while panel remains open
This commit is contained in:
Cyril
2026-07-01 10:15:06 +02:00
committed by lebaudantoine
parent ba6a4e971f
commit fa85343147
4 changed files with 42 additions and 8 deletions
@@ -177,17 +177,21 @@ export const SidePanel = () => {
const asideRef = useRef<HTMLElement>(null) const asideRef = useRef<HTMLElement>(null)
const panelManagesFocus = activePanelId === PanelId.CHAT
const focusAside = useCallback(() => { const focusAside = useCallback(() => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
asideRef.current?.focus({ preventScroll: true }) asideRef.current?.focus({ preventScroll: true })
}) })
}, []) }, [])
useRestoreFocus(isSidePanelOpen && !panelManagesFocus, { const handlePanelOpened = useCallback(() => {
onOpened: focusAside, if (activePanelId === PanelId.CHAT) return
focusAside()
}, [activePanelId, focusAside])
useRestoreFocus(isSidePanelOpen, {
onOpened: handlePanelOpened,
preventScroll: true, preventScroll: true,
activeKey: activePanelId,
}) })
return ( return (
@@ -95,8 +95,13 @@ const ToolButton = ({
export const Tools = () => { export const Tools = () => {
const { data } = useConfig() const { data } = useConfig()
const { openTranscript, openScreenRecording, activeSubPanelId, isToolsOpen } = const {
useSidePanel() openTranscript,
openScreenRecording,
activeSubPanelId,
isToolsOpen,
isSidePanelOpen,
} = useSidePanel()
const { t } = useTranslation('rooms', { keyPrefix: 'moreTools' }) const { t } = useTranslation('rooms', { keyPrefix: 'moreTools' })
// Restore focus to the element that opened the Tools panel // Restore focus to the element that opened the Tools panel
@@ -113,6 +118,7 @@ export const Tools = () => {
}, },
restoreFocusRaf: true, restoreFocusRaf: true,
preventScroll: true, preventScroll: true,
shouldRestoreOnClose: () => !isSidePanelOpen,
}) })
const isTranscriptEnabled = useIsRecordingModeEnabled( const isTranscriptEnabled = useIsRecordingModeEnabled(
@@ -37,7 +37,7 @@ export function Chat({ ...props }: ChatProps) {
const room = useRoomContext() const room = useRoomContext()
const { send, chatMessages, isSending } = useChat() const { send, chatMessages, isSending } = useChat()
const { isChatOpen } = useSidePanel() const { isChatOpen, isSidePanelOpen } = useSidePanel()
const chatSnap = useSnapshot(chatStore) const chatSnap = useSnapshot(chatStore)
// Keep track of the element that opened the chat so we can restore focus // 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, preventScroll: true,
shouldRestoreOnClose: () => !isSidePanelOpen,
}) })
// Use useParticipants hook to trigger a re-render when the participant list changes. // Use useParticipants hook to trigger a re-render when the participant list changes.
+24 -1
View File
@@ -6,6 +6,10 @@ export type RestoreFocusOptions = {
onClosed?: () => void onClosed?: () => void
restoreFocusRaf?: boolean restoreFocusRaf?: boolean
preventScroll?: 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, onClosed,
restoreFocusRaf = false, restoreFocusRaf = false,
preventScroll = true, preventScroll = true,
activeKey,
shouldRestoreOnClose,
} = options } = options
const prevIsOpenRef = useRef(false) const prevIsOpenRef = useRef(false)
const prevActiveKeyRef = useRef(activeKey)
const triggerRef = useRef<HTMLElement | null>(null) const triggerRef = useRef<HTMLElement | null>(null)
useEffect(() => { useEffect(() => {
@@ -37,25 +44,41 @@ export function useRestoreFocus(
onOpened?.() 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 // Just closed
if (wasOpen && !isOpen) { if (wasOpen && !isOpen) {
const trigger = triggerRef.current const trigger = triggerRef.current
if (trigger && document.contains(trigger)) { const shouldRestore =
(shouldRestoreOnClose?.() ?? true) &&
trigger &&
document.contains(trigger)
if (shouldRestore) {
const focus = () => trigger.focus({ preventScroll }) const focus = () => trigger.focus({ preventScroll })
if (restoreFocusRaf) requestAnimationFrame(focus) if (restoreFocusRaf) requestAnimationFrame(focus)
else focus() else focus()
} }
triggerRef.current = null triggerRef.current = null
onClosed?.() onClosed?.()
} }
prevIsOpenRef.current = isOpen prevIsOpenRef.current = isOpen
prevActiveKeyRef.current = activeKey
}, [ }, [
isOpen, isOpen,
activeKey,
onClosed, onClosed,
onOpened, onOpened,
preventScroll, preventScroll,
resolveTrigger, resolveTrigger,
restoreFocusRaf, restoreFocusRaf,
shouldRestoreOnClose,
]) ])
} }