✨(frontend) sr pin/unpin announcements with dedicated messages
improves accessibility by announcing pin/unpin on state change
This commit is contained in:
@@ -11,6 +11,7 @@ and this project adheres to
|
|||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- ♿️(frontend) adjust visual-only tooltip a11y labels #910
|
- ♿️(frontend) adjust visual-only tooltip a11y labels #910
|
||||||
|
- ♿️(frontend) sr pin/unpin announcements with dedicated messages #898
|
||||||
- ♿(frontend) adjust sr announcements for idle disconnect timer #908
|
- ♿(frontend) adjust sr announcements for idle disconnect timer #908
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ import {
|
|||||||
isWeb,
|
isWeb,
|
||||||
log,
|
log,
|
||||||
} from '@livekit/components-core'
|
} from '@livekit/components-core'
|
||||||
import { RoomEvent, Track } from 'livekit-client'
|
import { Participant, RoomEvent, Track } from 'livekit-client'
|
||||||
import * as React from 'react'
|
import React, { useCallback, useRef, useState, useEffect } from 'react'
|
||||||
import { useState } from 'react'
|
|
||||||
import {
|
import {
|
||||||
ConnectionStateToast,
|
ConnectionStateToast,
|
||||||
FocusLayoutContainer,
|
FocusLayoutContainer,
|
||||||
@@ -16,7 +15,9 @@ import {
|
|||||||
usePinnedTracks,
|
usePinnedTracks,
|
||||||
useTracks,
|
useTracks,
|
||||||
useCreateLayoutContext,
|
useCreateLayoutContext,
|
||||||
|
useRoomContext,
|
||||||
} from '@livekit/components-react'
|
} from '@livekit/components-react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { ControlBar } from './ControlBar/ControlBar'
|
import { ControlBar } from './ControlBar/ControlBar'
|
||||||
import { styled } from '@/styled-system/jsx'
|
import { styled } from '@/styled-system/jsx'
|
||||||
@@ -37,6 +38,7 @@ import { Subtitles } from '@/features/subtitle/component/Subtitles'
|
|||||||
import { CarouselLayout } from '../components/layout/CarouselLayout'
|
import { CarouselLayout } from '../components/layout/CarouselLayout'
|
||||||
import { GridLayout } from '../components/layout/GridLayout'
|
import { GridLayout } from '../components/layout/GridLayout'
|
||||||
import { IsIdleDisconnectModal } from '../components/IsIdleDisconnectModal'
|
import { IsIdleDisconnectModal } from '../components/IsIdleDisconnectModal'
|
||||||
|
import { getParticipantName } from '@/features/rooms/utils/getParticipantName'
|
||||||
|
|
||||||
const LayoutWrapper = styled(
|
const LayoutWrapper = styled(
|
||||||
'div',
|
'div',
|
||||||
@@ -89,7 +91,22 @@ export interface VideoConferenceProps
|
|||||||
*/
|
*/
|
||||||
export function VideoConference({ ...props }: VideoConferenceProps) {
|
export function VideoConference({ ...props }: VideoConferenceProps) {
|
||||||
const lastAutoFocusedScreenShareTrack =
|
const lastAutoFocusedScreenShareTrack =
|
||||||
React.useRef<TrackReferenceOrPlaceholder | null>(null)
|
useRef<TrackReferenceOrPlaceholder | null>(null)
|
||||||
|
const lastPinnedParticipantIdentityRef = useRef<string | null>(null)
|
||||||
|
const [pinAnnouncement, setPinAnnouncement] = useState('')
|
||||||
|
const { t } = useTranslation('rooms', { keyPrefix: 'pinAnnouncements' })
|
||||||
|
const { t: tRooms } = useTranslation('rooms')
|
||||||
|
const room = useRoomContext()
|
||||||
|
|
||||||
|
const getAnnouncementName = useCallback(
|
||||||
|
(participant?: Participant | null) => {
|
||||||
|
if (!participant) return tRooms('participants.unknown')
|
||||||
|
return participant.isLocal
|
||||||
|
? tRooms('participants.you')
|
||||||
|
: getParticipantName(participant)
|
||||||
|
},
|
||||||
|
[tRooms]
|
||||||
|
)
|
||||||
|
|
||||||
useConnectionObserver()
|
useConnectionObserver()
|
||||||
useVideoResolutionSubscription()
|
useVideoResolutionSubscription()
|
||||||
@@ -115,9 +132,61 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
|
|||||||
(track) => !isEqualTrackRef(track, focusTrack)
|
(track) => !isEqualTrackRef(track, focusTrack)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// handle pin announcements
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const participant = focusTrack?.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)
|
||||||
|
|
||||||
|
setPinAnnouncement(
|
||||||
|
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
|
||||||
|
|
||||||
|
setPinAnnouncement(
|
||||||
|
participant.isLocal ? t('self.pin') : t('pin', { name: participantName })
|
||||||
|
)
|
||||||
|
}, [
|
||||||
|
focusTrack,
|
||||||
|
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.
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
// If screen share tracks are published, and no pin is set explicitly, auto set the screen share.
|
// If screen share tracks are published, and no pin is set explicitly, auto set the screen share.
|
||||||
if (
|
if (
|
||||||
screenShareTracks.some((track) => track.publication.isSubscribed) &&
|
screenShareTracks.some((track) => track.publication.isSubscribed) &&
|
||||||
@@ -188,6 +257,14 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
|
|||||||
value={layoutContext}
|
value={layoutContext}
|
||||||
// onPinChange={handleFocusStateChange}
|
// onPinChange={handleFocusStateChange}
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
id="pin-announcer"
|
||||||
|
aria-live="polite"
|
||||||
|
aria-atomic="true"
|
||||||
|
className="sr-only"
|
||||||
|
>
|
||||||
|
{pinAnnouncement}
|
||||||
|
</div>
|
||||||
<ScreenShareErrorModal
|
<ScreenShareErrorModal
|
||||||
isOpen={isShareErrorVisible}
|
isOpen={isShareErrorVisible}
|
||||||
onClose={() => setIsShareErrorVisible(false)}
|
onClose={() => setIsShareErrorVisible(false)}
|
||||||
|
|||||||
@@ -505,6 +505,7 @@
|
|||||||
"participants": {
|
"participants": {
|
||||||
"subheading": "Im Raum",
|
"subheading": "Im Raum",
|
||||||
"you": "Du",
|
"you": "Du",
|
||||||
|
"unknown": "Unbekannter Teilnehmer",
|
||||||
"host": "Host",
|
"host": "Host",
|
||||||
"contributors": "Mitwirkende",
|
"contributors": "Mitwirkende",
|
||||||
"collapsable": {
|
"collapsable": {
|
||||||
@@ -552,6 +553,14 @@
|
|||||||
"ariaLabel": "Hefte {{name}} an"
|
"ariaLabel": "Hefte {{name}} an"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"pinAnnouncements": {
|
||||||
|
"pin": "Das Video von {{name}} ist angeheftet.",
|
||||||
|
"unpin": "Das Video von {{name}} ist nicht mehr angeheftet.",
|
||||||
|
"self": {
|
||||||
|
"pin": "Dein Video ist angeheftet.",
|
||||||
|
"unpin": "Dein Video ist nicht mehr angeheftet."
|
||||||
|
}
|
||||||
|
},
|
||||||
"recordingStateToast": {
|
"recordingStateToast": {
|
||||||
"transcript": {
|
"transcript": {
|
||||||
"started": "Transkription läuft",
|
"started": "Transkription läuft",
|
||||||
|
|||||||
@@ -505,6 +505,7 @@
|
|||||||
"participants": {
|
"participants": {
|
||||||
"subheading": "In room",
|
"subheading": "In room",
|
||||||
"you": "You",
|
"you": "You",
|
||||||
|
"unknown": "Unknown participant",
|
||||||
"host": "Host",
|
"host": "Host",
|
||||||
"contributors": "Contributors",
|
"contributors": "Contributors",
|
||||||
"collapsable": {
|
"collapsable": {
|
||||||
@@ -552,6 +553,14 @@
|
|||||||
"ariaLabel": "Pin {{name}}"
|
"ariaLabel": "Pin {{name}}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"pinAnnouncements": {
|
||||||
|
"pin": "The video of {{name}} is pinned.",
|
||||||
|
"unpin": "The video of {{name}} is no longer pinned.",
|
||||||
|
"self": {
|
||||||
|
"pin": "Your video is pinned.",
|
||||||
|
"unpin": "Your video is no longer pinned."
|
||||||
|
}
|
||||||
|
},
|
||||||
"recordingStateToast": {
|
"recordingStateToast": {
|
||||||
"transcript": {
|
"transcript": {
|
||||||
"started": "Transcribing",
|
"started": "Transcribing",
|
||||||
|
|||||||
@@ -505,6 +505,7 @@
|
|||||||
"participants": {
|
"participants": {
|
||||||
"subheading": "Dans la réunion",
|
"subheading": "Dans la réunion",
|
||||||
"you": "Vous",
|
"you": "Vous",
|
||||||
|
"unknown": "Participant inconnu",
|
||||||
"contributors": "Contributeurs",
|
"contributors": "Contributeurs",
|
||||||
"host": "Organisateur de la réunion",
|
"host": "Organisateur de la réunion",
|
||||||
"collapsable": {
|
"collapsable": {
|
||||||
@@ -552,6 +553,14 @@
|
|||||||
"ariaLabel": "Épingler {{name}}"
|
"ariaLabel": "Épingler {{name}}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"pinAnnouncements": {
|
||||||
|
"pin": "La vidéo de {{name}} est épinglée.",
|
||||||
|
"unpin": "La vidéo de {{name}} n’est plus épinglée.",
|
||||||
|
"self": {
|
||||||
|
"pin": "Votre vidéo est épinglée.",
|
||||||
|
"unpin": "Votre vidéo n’est plus épinglée."
|
||||||
|
}
|
||||||
|
},
|
||||||
"recordingStateToast": {
|
"recordingStateToast": {
|
||||||
"transcript": {
|
"transcript": {
|
||||||
"started": "Transcription en cours",
|
"started": "Transcription en cours",
|
||||||
|
|||||||
@@ -505,6 +505,7 @@
|
|||||||
"participants": {
|
"participants": {
|
||||||
"subheading": "In de ruimte",
|
"subheading": "In de ruimte",
|
||||||
"you": "U",
|
"you": "U",
|
||||||
|
"unknown": "Onbekende deelnemer",
|
||||||
"host": "Host",
|
"host": "Host",
|
||||||
"contributors": "Deelnemers",
|
"contributors": "Deelnemers",
|
||||||
"collapsable": {
|
"collapsable": {
|
||||||
@@ -552,6 +553,14 @@
|
|||||||
"ariaLabel": "Maak {{name}} vast"
|
"ariaLabel": "Maak {{name}} vast"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"pinAnnouncements": {
|
||||||
|
"pin": "De video van {{name}} is vastgezet.",
|
||||||
|
"unpin": "De video van {{name}} is niet meer vastgezet.",
|
||||||
|
"self": {
|
||||||
|
"pin": "Uw video is vastgezet.",
|
||||||
|
"unpin": "Uw video is niet meer vastgezet."
|
||||||
|
}
|
||||||
|
},
|
||||||
"recordingStateToast": {
|
"recordingStateToast": {
|
||||||
"transcript": {
|
"transcript": {
|
||||||
"started": "Transcriptie bezig",
|
"started": "Transcriptie bezig",
|
||||||
|
|||||||
Reference in New Issue
Block a user