diff --git a/src/frontend/src/features/recording/components/RecordingStateToast.tsx b/src/frontend/src/features/recording/components/RecordingStateToast.tsx index 8bdbee1f..e1b6008e 100644 --- a/src/frontend/src/features/recording/components/RecordingStateToast.tsx +++ b/src/frontend/src/features/recording/components/RecordingStateToast.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next' import { useSnapshot } from 'valtio' import { useRoomContext } from '@livekit/components-react' import { Spinner } from '@/primitives/Spinner' -import { useEffect, useMemo, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import { Text } from '@/primitives' import { RoomEvent } from 'livekit-client' import { decodeNotificationDataReceived } from '@/features/notifications/utils' @@ -31,6 +31,9 @@ export const RecordingStateToast = () => { const { openTranscript, openScreenRecording } = useSidePanel() const [isAlertOpen, setIsAlertOpen] = useState(false) + const [srMessage, setSrMessage] = useState('') + const lastKeyRef = useRef('') + const recordingSnap = useSnapshot(recordingStore) const hasTranscriptAccess = useHasRecordingAccess( @@ -128,6 +131,23 @@ export const RecordingStateToast = () => { } }, [recordingSnap]) + // Update screen reader message only when the key actually changes + // This prevents duplicate announcements caused by re-renders + useEffect(() => { + if (key && key !== lastKeyRef.current) { + lastKeyRef.current = key + const message = t(key) + setSrMessage(message) + + // Clear message after 3 seconds to prevent it from being announced again + const timer = setTimeout(() => { + setSrMessage('') + }, 3000) + + return () => clearTimeout(timer) + } + }, [key, t]) + if (!key) return isAdminOrOwner ? ( { const hasTranscriptAccessAndActive = isTranscriptActive && hasTranscriptAccess return ( -
- {isStarted ? ( - - ) : ( - - )} + <> + {/* Screen reader only message to announce state changes once */} +
+ {srMessage} +
- {!hasScreenRecordingAccessAndActive && !hasTranscriptAccessAndActive && ( - - {t(key)} - - )} - {hasScreenRecordingAccessAndActive && ( - - {t(key)} - - )} - {hasTranscriptAccessAndActive && ( - - {t(key)} - - )} -
+ {/* Visual banner (without aria-live to avoid duplicate announcements) */} +
+ {isStarted ? ( + + ) : ( + + )} + + {!hasScreenRecordingAccessAndActive && + !hasTranscriptAccessAndActive && ( + + {t(key)} + + )} + {hasScreenRecordingAccessAndActive && ( + + {t(key)} + + )} + {hasTranscriptAccessAndActive && ( + + {t(key)} + + )} +
+ ) }