️(frontend) reduce re-renders of the ParticipantTile focus overlay

Optimize the idle mouse handling and the FocusOverlay component so
they no longer trigger frequent re-renders of the ParticipantTile
focus.

State related to hover and focus is now scoped closer to where it
is used, keeping updates local instead of propagating up the tile.
This commit is contained in:
lebaudantoine
2026-07-23 21:33:08 +02:00
committed by aleb_the_flash
parent a3851842e9
commit 73aa162dc8
2 changed files with 78 additions and 76 deletions
+4
View File
@@ -114,6 +114,10 @@ const config: Config = {
clipPath: 'polygon(50% 50%, 0 0, 100% 0, 100% 100%, 0 100%, 0 0)', clipPath: 'polygon(50% 50%, 0 0, 100% 0, 100% 100%, 0 100%, 0 0)',
}, },
}, },
overlayIn: {
from: { opacity: 0 },
to: { opacity: 0.6 },
},
}, },
tokens: defineTokens({ tokens: defineTokens({
/* we take a few things from the panda preset but for now we clear out some stuff. /* we take a few things from the panda preset but for now we clear out some stuff.
@@ -1,7 +1,7 @@
import { css } from '@/styled-system/css' import { css } from '@/styled-system/css'
import { HStack } from '@/styled-system/jsx' import { HStack } from '@/styled-system/jsx'
import { TrackReferenceOrPlaceholder } from '@livekit/components-core' import { TrackReferenceOrPlaceholder } from '@livekit/components-core'
import { useEffect, useRef, useState } from 'react' import { ReactNode, useEffect, useRef, useState } from 'react'
import { Track } from 'livekit-client' import { Track } from 'livekit-client'
import { useCanMute } from '@/features/rooms/livekit/hooks/useCanMute' import { useCanMute } from '@/features/rooms/livekit/hooks/useCanMute'
import { FocusButton } from './FocusButton' import { FocusButton } from './FocusButton'
@@ -11,49 +11,34 @@ import { ZoomButton } from './ZoomButton'
const MOUSE_IDLE_TIME = 3000 const MOUSE_IDLE_TIME = 3000
export const ParticipantTileFocus = ({ type FadeOverlayProps = {
trackRef, children: ReactNode
hasKeyboardFocus,
}: {
trackRef: TrackReferenceOrPlaceholder
hasKeyboardFocus: boolean hasKeyboardFocus: boolean
}) => { }
const [hovered, setHovered] = useState(false)
const [opacity, setOpacity] = useState(0)
const FadeOverlay = ({ children, hasKeyboardFocus }: FadeOverlayProps) => {
const [active, setActive] = useState(false)
const idleTimerRef = useRef<number | null>(null) const idleTimerRef = useRef<number | null>(null)
const [isIdleRef, setIsIdleRef] = useState(false)
const isVisible = hasKeyboardFocus || (hovered && !isIdleRef) const clearIdleTimer = () => {
if (idleTimerRef.current) window.clearTimeout(idleTimerRef.current)
useEffect(() => {
if (isVisible) {
// Wait for next frame to ensure element is mounted
requestAnimationFrame(() => {
setOpacity(0.6)
})
} else {
setOpacity(0)
}
}, [isVisible])
const handleMouseMove = () => {
if (idleTimerRef.current) {
window.clearTimeout(idleTimerRef.current)
}
idleTimerRef.current = window.setTimeout(() => {
setIsIdleRef(true)
}, MOUSE_IDLE_TIME)
setIsIdleRef(false)
} }
const participant = trackRef.participant const armIdleTimer = () => {
clearIdleTimer()
idleTimerRef.current = window.setTimeout(() => {
setActive(false)
}, MOUSE_IDLE_TIME)
}
const isScreenShare = trackRef.source == Track.Source.ScreenShare const handleActivity = () => {
const isLocal = trackRef.participant.isLocal setActive(true)
armIdleTimer()
}
const canMute = useCanMute(participant) useEffect(() => clearIdleTimer, [])
const isVisible = hasKeyboardFocus || active
return ( return (
<div <div
className={css({ className={css({
@@ -66,49 +51,62 @@ export const ParticipantTileFocus = ({
width: '100%', width: '100%',
height: '100%', height: '100%',
})} })}
data-visible={isVisible || undefined}
aria-hidden={!isVisible} aria-hidden={!isVisible}
onMouseEnter={() => setHovered(true)} onMouseEnter={handleActivity}
onMouseLeave={() => setHovered(false)} onMouseMove={handleActivity}
onMouseMove={handleMouseMove} onMouseLeave={() => {
clearIdleTimer()
setActive(false)
}}
> >
{isVisible && ( {isVisible && children}
<div
className={css({
backgroundColor: 'primaryDark.50',
transition: 'opacity 200ms linear',
zIndex: 1,
borderRadius: '0.25rem',
display: 'flex',
_hover: {
opacity: '0.95 !important',
},
})}
style={{ opacity }}
>
<HStack
gap={0.5}
className={css({
padding: '0.5rem',
_hover: {
opacity: '1 !important',
},
})}
>
<FocusButton trackRef={trackRef} />
{!isScreenShare ? (
<>
{participant.isLocal ? (
<EffectsButton />
) : (
canMute && <MuteButton participant={participant} />
)}
</>
) : (
!isLocal && <ZoomButton trackRef={trackRef} />
)}
</HStack>
</div>
)}
</div> </div>
) )
} }
export const ParticipantTileFocus = ({
trackRef,
hasKeyboardFocus,
}: {
trackRef: TrackReferenceOrPlaceholder
hasKeyboardFocus: boolean
}) => {
const participant = trackRef.participant
const isScreenShare = trackRef.source == Track.Source.ScreenShare
const isLocal = participant.isLocal
const canMute = useCanMute(participant)
return (
<FadeOverlay hasKeyboardFocus={hasKeyboardFocus}>
<div
className={css({
backgroundColor: 'primaryDark.50',
zIndex: 1,
borderRadius: '0.25rem',
display: 'flex',
opacity: 0.6,
animation: 'overlayIn 200ms linear 300ms backwards',
_hover: {
opacity: 0.95,
},
})}
>
<HStack gap={0.5} padding={0.5}>
<FocusButton trackRef={trackRef} />
{!isScreenShare ? (
<>
{isLocal ? (
<EffectsButton />
) : (
canMute && <MuteButton participant={participant} />
)}
</>
) : (
!isLocal && <ZoomButton trackRef={trackRef} />
)}
</HStack>
</div>
</FadeOverlay>
)
}