♿️(frontend) improve meeting a11y: blur, focus, hover, sr announcements
enhances keyboard nav and screen reader support for meeting interface Signed-off-by: Cyril <c.gromoff@gmail.com>
This commit is contained in:
@@ -35,6 +35,7 @@ export const ParticipantName = ({
|
|||||||
style={{
|
style={{
|
||||||
paddingBottom: '0.1rem',
|
paddingBottom: '0.1rem',
|
||||||
}}
|
}}
|
||||||
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
{displayedName}
|
{displayedName}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ import { ParticipantPlaceholder } from './ParticipantPlaceholder'
|
|||||||
import { ParticipantTileFocus } from './ParticipantTileFocus'
|
import { ParticipantTileFocus } from './ParticipantTileFocus'
|
||||||
import { FullScreenShareWarning } from './FullScreenShareWarning'
|
import { FullScreenShareWarning } from './FullScreenShareWarning'
|
||||||
import { ParticipantName } from './ParticipantName'
|
import { ParticipantName } from './ParticipantName'
|
||||||
|
import { getParticipantName } from '@/features/rooms/utils/getParticipantName'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
export function TrackRefContextIfNeeded(
|
export function TrackRefContextIfNeeded(
|
||||||
props: React.PropsWithChildren<{
|
props: React.PropsWithChildren<{
|
||||||
@@ -102,9 +104,37 @@ export const ParticipantTile: (
|
|||||||
})
|
})
|
||||||
|
|
||||||
const isScreenShare = trackReference.source != Track.Source.Camera
|
const isScreenShare = trackReference.source != Track.Source.Camera
|
||||||
|
const [hasKeyboardFocus, setHasKeyboardFocus] = React.useState(false)
|
||||||
|
|
||||||
|
const participantName = getParticipantName(trackReference.participant)
|
||||||
|
const { t } = useTranslation('rooms', { keyPrefix: 'participantTileFocus' })
|
||||||
|
|
||||||
|
// Avoid double announcements: LiveKit's useParticipantTile may set its own
|
||||||
|
// aria-label / aria-labelledby / aria-describedby. We strip them here
|
||||||
|
// and provide a single, explicit label for the focusable tile container.
|
||||||
|
const { 'aria-label': _ignoredAriaLabel, ...safeElementProps } = elementProps
|
||||||
|
void _ignoredAriaLabel
|
||||||
|
|
||||||
|
const interactiveProps = {
|
||||||
|
...safeElementProps,
|
||||||
|
// Ensure the tile is focusable to expose contextual controls to keyboard users.
|
||||||
|
tabIndex: 0,
|
||||||
|
'aria-label': t('containerLabel', { name: participantName }),
|
||||||
|
onFocus: (event: React.FocusEvent<HTMLDivElement>) => {
|
||||||
|
elementProps.onFocus?.(event)
|
||||||
|
setHasKeyboardFocus(true)
|
||||||
|
},
|
||||||
|
onBlur: (event: React.FocusEvent<HTMLDivElement>) => {
|
||||||
|
elementProps.onBlur?.(event)
|
||||||
|
const nextTarget = event.relatedTarget as Node | null
|
||||||
|
if (!event.currentTarget.contains(nextTarget)) {
|
||||||
|
setHasKeyboardFocus(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref} style={{ position: 'relative' }} {...elementProps}>
|
<div ref={ref} style={{ position: 'relative' }} {...interactiveProps}>
|
||||||
<TrackRefContextIfNeeded trackRef={trackReference}>
|
<TrackRefContextIfNeeded trackRef={trackReference}>
|
||||||
<ParticipantContextIfNeeded participant={trackReference.participant}>
|
<ParticipantContextIfNeeded participant={trackReference.participant}>
|
||||||
<FullScreenShareWarning trackReference={trackReference} />
|
<FullScreenShareWarning trackReference={trackReference} />
|
||||||
@@ -195,7 +225,10 @@ export const ParticipantTile: (
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!disableMetadata && (
|
{!disableMetadata && (
|
||||||
<ParticipantTileFocus trackRef={trackReference} />
|
<ParticipantTileFocus
|
||||||
|
trackRef={trackReference}
|
||||||
|
hasKeyboardFocus={hasKeyboardFocus}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</ParticipantContextIfNeeded>
|
</ParticipantContextIfNeeded>
|
||||||
</TrackRefContextIfNeeded>
|
</TrackRefContextIfNeeded>
|
||||||
|
|||||||
@@ -131,8 +131,10 @@ const MOUSE_IDLE_TIME = 3000
|
|||||||
|
|
||||||
export const ParticipantTileFocus = ({
|
export const ParticipantTileFocus = ({
|
||||||
trackRef,
|
trackRef,
|
||||||
|
hasKeyboardFocus,
|
||||||
}: {
|
}: {
|
||||||
trackRef: TrackReferenceOrPlaceholder
|
trackRef: TrackReferenceOrPlaceholder
|
||||||
|
hasKeyboardFocus: boolean
|
||||||
}) => {
|
}) => {
|
||||||
const [hovered, setHovered] = useState(false)
|
const [hovered, setHovered] = useState(false)
|
||||||
const [opacity, setOpacity] = useState(0)
|
const [opacity, setOpacity] = useState(0)
|
||||||
@@ -140,8 +142,10 @@ export const ParticipantTileFocus = ({
|
|||||||
const idleTimerRef = useRef<number | null>(null)
|
const idleTimerRef = useRef<number | null>(null)
|
||||||
const [isIdleRef, setIsIdleRef] = useState(false)
|
const [isIdleRef, setIsIdleRef] = useState(false)
|
||||||
|
|
||||||
|
const isVisible = hasKeyboardFocus || (hovered && !isIdleRef)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hovered && !isIdleRef) {
|
if (isVisible) {
|
||||||
// Wait for next frame to ensure element is mounted
|
// Wait for next frame to ensure element is mounted
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
setOpacity(0.6)
|
setOpacity(0.6)
|
||||||
@@ -149,7 +153,7 @@ export const ParticipantTileFocus = ({
|
|||||||
} else {
|
} else {
|
||||||
setOpacity(0)
|
setOpacity(0)
|
||||||
}
|
}
|
||||||
}, [hovered, isIdleRef])
|
}, [isVisible])
|
||||||
|
|
||||||
const handleMouseMove = () => {
|
const handleMouseMove = () => {
|
||||||
if (idleTimerRef.current) {
|
if (idleTimerRef.current) {
|
||||||
@@ -180,11 +184,12 @@ export const ParticipantTileFocus = ({
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
})}
|
})}
|
||||||
|
aria-hidden={!isVisible}
|
||||||
onMouseEnter={() => setHovered(true)}
|
onMouseEnter={() => setHovered(true)}
|
||||||
onMouseLeave={() => setHovered(false)}
|
onMouseLeave={() => setHovered(false)}
|
||||||
onMouseMove={handleMouseMove}
|
onMouseMove={handleMouseMove}
|
||||||
>
|
>
|
||||||
{hovered && (
|
{isVisible && (
|
||||||
<div
|
<div
|
||||||
className={css({
|
className={css({
|
||||||
backgroundColor: 'primaryDark.50',
|
backgroundColor: 'primaryDark.50',
|
||||||
|
|||||||
Reference in New Issue
Block a user