mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-13 12:17:24 +00:00
♻️(frontend) extract pinned track a11y announcement into a leaf component
Extract the code responsible for announcing pinned track changes (for accessibility) into a well-scoped leaf component. Previously, calling the useScreenReaderAnnounce hook inside StageLayout triggered a re-render of the whole layout subtree. Encapsulating the announcement logic in a narrow component keeps those re-renders local to that leaf.
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
import { usePinAnnouncements } from '../hooks/usePinAnnouncements'
|
||||||
|
|
||||||
|
export const PinAnnouncer = () => {
|
||||||
|
usePinAnnouncements()
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -1,8 +1,4 @@
|
|||||||
import {
|
import { FocusLayoutContainer, useTracks } from '@livekit/components-react'
|
||||||
FocusLayoutContainer,
|
|
||||||
useRoomContext,
|
|
||||||
useTracks,
|
|
||||||
} from '@livekit/components-react'
|
|
||||||
import { CarouselLayout } from '@/features/layout/components/CarouselLayout'
|
import { CarouselLayout } from '@/features/layout/components/CarouselLayout'
|
||||||
import { FocusLayout } from '@/features/layout/components/FocusLayout'
|
import { FocusLayout } from '@/features/layout/components/FocusLayout'
|
||||||
import { ParticipantTile } from '@/features/rooms/livekit/components/ParticipantTile'
|
import { ParticipantTile } from '@/features/rooms/livekit/components/ParticipantTile'
|
||||||
@@ -13,36 +9,15 @@ import {
|
|||||||
log,
|
log,
|
||||||
type TrackReferenceOrPlaceholder,
|
type TrackReferenceOrPlaceholder,
|
||||||
} from '@livekit/components-core'
|
} from '@livekit/components-core'
|
||||||
import { type Participant, RoomEvent, Track } from 'livekit-client'
|
import { RoomEvent, Track } from 'livekit-client'
|
||||||
import { useSnapshot } from 'valtio'
|
import { useSnapshot } from 'valtio'
|
||||||
import { clearPinnedTrack, layoutStore, setPinnedTrack } from '@/stores/layout'
|
import { clearPinnedTrack, layoutStore, setPinnedTrack } from '@/stores/layout'
|
||||||
import { useCallback, useEffect, useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { getParticipantName } from '@/features/rooms/utils/getParticipantName'
|
|
||||||
|
|
||||||
export const StageLayout = () => {
|
export const StageLayout = () => {
|
||||||
const lastAutoFocusedScreenShareTrack =
|
const lastAutoFocusedScreenShareTrack =
|
||||||
useRef<TrackReferenceOrPlaceholder | null>(null)
|
useRef<TrackReferenceOrPlaceholder | null>(null)
|
||||||
|
|
||||||
const lastPinnedParticipantIdentityRef = useRef<string | null>(null)
|
|
||||||
|
|
||||||
const { t } = useTranslation('rooms', { keyPrefix: 'pinAnnouncements' })
|
|
||||||
const { t: tRooms } = useTranslation('rooms')
|
|
||||||
|
|
||||||
const room = useRoomContext()
|
|
||||||
const announce = useScreenReaderAnnounce()
|
|
||||||
|
|
||||||
const getAnnouncementName = useCallback(
|
|
||||||
(participant?: Participant | null) => {
|
|
||||||
if (!participant) return tRooms('participants.unknown')
|
|
||||||
return participant.isLocal
|
|
||||||
? tRooms('participants.you')
|
|
||||||
: getParticipantName(participant)
|
|
||||||
},
|
|
||||||
[tRooms]
|
|
||||||
)
|
|
||||||
|
|
||||||
const tracks = useTracks(
|
const tracks = useTracks(
|
||||||
[
|
[
|
||||||
{ source: Track.Source.Camera, withPlaceholder: true },
|
{ source: Track.Source.Camera, withPlaceholder: true },
|
||||||
@@ -61,58 +36,6 @@ export const StageLayout = () => {
|
|||||||
(track) => !isEqualTrackRef(track, pinnedTrackRef)
|
(track) => !isEqualTrackRef(track, pinnedTrackRef)
|
||||||
)
|
)
|
||||||
|
|
||||||
// handle pin announcements
|
|
||||||
useEffect(() => {
|
|
||||||
const participant = pinnedTrackRef?.participant
|
|
||||||
|
|
||||||
// 1. unpin
|
|
||||||
if (!participant) {
|
|
||||||
if (!lastPinnedParticipantIdentityRef.current) return
|
|
||||||
|
|
||||||
const lastIdentity = lastPinnedParticipantIdentityRef.current
|
|
||||||
const lastParticipant =
|
|
||||||
room.localParticipant.identity === lastIdentity
|
|
||||||
? room.localParticipant
|
|
||||||
: room.remoteParticipants.get(lastIdentity)
|
|
||||||
const announcementName = getAnnouncementName(lastParticipant)
|
|
||||||
|
|
||||||
announce(
|
|
||||||
lastParticipant?.isLocal
|
|
||||||
? t('self.unpin')
|
|
||||||
: t('unpin', {
|
|
||||||
name: announcementName,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
lastPinnedParticipantIdentityRef.current = null
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. same pin → do nothing
|
|
||||||
if (lastPinnedParticipantIdentityRef.current === participant.identity) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. new pin
|
|
||||||
const participantName = participant.isLocal
|
|
||||||
? tRooms('participants.you')
|
|
||||||
: getParticipantName(participant)
|
|
||||||
|
|
||||||
lastPinnedParticipantIdentityRef.current = participant.identity
|
|
||||||
|
|
||||||
announce(
|
|
||||||
participant.isLocal ? t('self.pin') : t('pin', { name: participantName })
|
|
||||||
)
|
|
||||||
}, [
|
|
||||||
announce,
|
|
||||||
pinnedTrackRef,
|
|
||||||
getAnnouncementName,
|
|
||||||
room.localParticipant,
|
|
||||||
room.remoteParticipants,
|
|
||||||
t,
|
|
||||||
tRooms,
|
|
||||||
])
|
|
||||||
|
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
// Code duplicated from LiveKit; this warning will be addressed in the refactoring.
|
// Code duplicated from LiveKit; this warning will be addressed in the refactoring.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { useEffect, useRef, useCallback } from 'react'
|
||||||
|
import { useRoomContext } from '@livekit/components-react'
|
||||||
|
import { Participant } from 'livekit-client'
|
||||||
|
import { useSnapshot } from 'valtio'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { layoutStore } from '@/stores/layout'
|
||||||
|
import { getParticipantName } from '@/features/rooms/utils/getParticipantName'
|
||||||
|
import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce'
|
||||||
|
|
||||||
|
export const usePinAnnouncements = () => {
|
||||||
|
const { t } = useTranslation('rooms', { keyPrefix: 'pinAnnouncements' })
|
||||||
|
const { t: tRooms } = useTranslation('rooms')
|
||||||
|
|
||||||
|
const room = useRoomContext()
|
||||||
|
const announce = useScreenReaderAnnounce()
|
||||||
|
|
||||||
|
const { pinnedTrackRef } = useSnapshot(layoutStore)
|
||||||
|
const lastPinnedIdentityRef = useRef<string | null>(null)
|
||||||
|
|
||||||
|
const getAnnouncementName = useCallback(
|
||||||
|
(participant?: Participant | null) => {
|
||||||
|
if (!participant) return tRooms('participants.unknown')
|
||||||
|
return participant.isLocal
|
||||||
|
? tRooms('participants.you')
|
||||||
|
: getParticipantName(participant)
|
||||||
|
},
|
||||||
|
[tRooms]
|
||||||
|
)
|
||||||
|
|
||||||
|
const pinnedIdentity = pinnedTrackRef?.participant?.identity ?? null
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 1. unpin
|
||||||
|
if (!pinnedIdentity) {
|
||||||
|
const lastIdentity = lastPinnedIdentityRef.current
|
||||||
|
if (!lastIdentity) return
|
||||||
|
|
||||||
|
const lastParticipant =
|
||||||
|
room.localParticipant.identity === lastIdentity
|
||||||
|
? room.localParticipant
|
||||||
|
: room.remoteParticipants.get(lastIdentity)
|
||||||
|
|
||||||
|
announce(
|
||||||
|
lastParticipant?.isLocal
|
||||||
|
? t('self.unpin')
|
||||||
|
: t('unpin', { name: getAnnouncementName(lastParticipant) })
|
||||||
|
)
|
||||||
|
|
||||||
|
lastPinnedIdentityRef.current = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. same pin → do nothing
|
||||||
|
if (lastPinnedIdentityRef.current === pinnedIdentity) return
|
||||||
|
|
||||||
|
// 3. new pin
|
||||||
|
const participant =
|
||||||
|
room.localParticipant.identity === pinnedIdentity
|
||||||
|
? room.localParticipant
|
||||||
|
: room.remoteParticipants.get(pinnedIdentity)
|
||||||
|
|
||||||
|
lastPinnedIdentityRef.current = pinnedIdentity
|
||||||
|
|
||||||
|
announce(
|
||||||
|
participant?.isLocal
|
||||||
|
? t('self.pin')
|
||||||
|
: t('pin', { name: getAnnouncementName(participant) })
|
||||||
|
)
|
||||||
|
}, [pinnedIdentity, announce, getAnnouncementName, room, t])
|
||||||
|
}
|
||||||
@@ -24,7 +24,8 @@ import { ReactionPortals } from '@/features/reactions/components/ReactionPortals
|
|||||||
import { RoomContentArea } from '@/features/layout/components/RoomContentArea'
|
import { RoomContentArea } from '@/features/layout/components/RoomContentArea'
|
||||||
import { usePictureInPicture } from '@/features/pip/hooks/usePictureInPicture'
|
import { usePictureInPicture } from '@/features/pip/hooks/usePictureInPicture'
|
||||||
import { PipRoomPlaceholder } from '@/features/pip/components/PipRoomPlaceholder'
|
import { PipRoomPlaceholder } from '@/features/pip/components/PipRoomPlaceholder'
|
||||||
import { StageLayout } from '@/features/layout/components/StageLayout.tsx'
|
import { StageLayout } from '@/features/layout/components/StageLayout'
|
||||||
|
import { PinAnnouncer } from '@/features/layout/components/PinAnnouncer'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
@@ -88,6 +89,7 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
|
|||||||
onClose={() => setIsShareErrorVisible(false)}
|
onClose={() => setIsShareErrorVisible(false)}
|
||||||
/>
|
/>
|
||||||
<IsIdleDisconnectModal />
|
<IsIdleDisconnectModal />
|
||||||
|
<PinAnnouncer />
|
||||||
<RoomContentArea>
|
<RoomContentArea>
|
||||||
{isPictureInPictureOpen ? <PipRoomPlaceholder /> : <StageLayout />}
|
{isPictureInPictureOpen ? <PipRoomPlaceholder /> : <StageLayout />}
|
||||||
</RoomContentArea>
|
</RoomContentArea>
|
||||||
|
|||||||
Reference in New Issue
Block a user