From d9bf6efa2ae3925601680122b1ad9fa76d714125 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Tue, 28 Jul 2026 22:16:03 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=84(frontend)=20improve=20participant?= =?UTF-8?q?=20name=20rendering=20in=20the=20list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rework how the participant name is displayed in the participant list to show as much of the name as possible before truncating. When the name has to be truncated, add a tooltip so users can hover to see the full name. Requested by users. --- CHANGELOG.md | 2 +- .../components/ParticipantName.tsx | 68 +++++++++++++++++++ .../components/ParticipantRow.tsx | 60 ++++------------ .../participants/components/RaisedHandRow.tsx | 45 +++--------- .../components/WaitingParticipantRow.tsx | 40 ++--------- .../src/primitives/VisualOnlyTooltip.tsx | 42 ++++++++---- 6 files changed, 126 insertions(+), 131 deletions(-) create mode 100644 src/frontend/src/features/participants/components/ParticipantName.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ee4b4c7..f3d6816a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to - ⚡️(frontend) limit unnecessary re-renders #1510 - 📝(legal) update terms of service - 💄(frontend) render Avatar initials in uppercase +- 💄(frontend) improve participant name rendering in the list ## Fixed @@ -35,7 +36,6 @@ and this project adheres to - 🐛(backend) allow any string as sub in the API serializer - 🐛(frontend) fall back to user.full_name on request-entry - ## [1.24.0] - 2026-07-21 ### Added diff --git a/src/frontend/src/features/participants/components/ParticipantName.tsx b/src/frontend/src/features/participants/components/ParticipantName.tsx new file mode 100644 index 00000000..d8fec50c --- /dev/null +++ b/src/frontend/src/features/participants/components/ParticipantName.tsx @@ -0,0 +1,68 @@ +import React, { useLayoutEffect, useRef, useState } from 'react' +import { Text } from '@/primitives' +import { css } from '@/styled-system/css' +import { useTranslation } from 'react-i18next' +import { VisualOnlyTooltip } from '@/primitives/VisualOnlyTooltip' + +export const ParticipantName = React.memo( + ({ displayedName, isLocal }: { displayedName: string; isLocal: boolean }) => { + const { t } = useTranslation('rooms') + + const nameRef = useRef(null) + const [isTruncated, setIsTruncated] = useState(false) + + useLayoutEffect(() => { + const el = nameRef.current + if (!el) return + const truncated = el.scrollWidth > el.clientWidth + setIsTruncated((prev) => (prev === truncated ? prev : truncated)) + }, [displayedName]) + + return ( + + +

+ {displayedName} +

+
+ {isLocal && ( + + ({t('participants.you')}) + + )} +
+ ) + } +) + +ParticipantName.displayName = 'ParticipantName' diff --git a/src/frontend/src/features/participants/components/ParticipantRow.tsx b/src/frontend/src/features/participants/components/ParticipantRow.tsx index 1b9e0198..3e7efc11 100644 --- a/src/frontend/src/features/participants/components/ParticipantRow.tsx +++ b/src/frontend/src/features/participants/components/ParticipantRow.tsx @@ -26,6 +26,7 @@ import { ParticipantMenuButton } from './menu/ParticipantMenuButton' import { PinBadge } from './PinBadge' import { UnauthenticatedBadge } from './UnauthenticatedBadge' import { MuteAlertDialog } from '@/features/rooms/livekit/components/MuteAlertDialog' +import { ParticipantName } from './ParticipantName' type MicIndicatorProps = { participant: Participant @@ -111,59 +112,26 @@ export const ParticipantRow = ({ participant }: ParticipantListItemProps) => { width: 'full', })} > - -
+ +
- - - - {name} - - {isLocal(participant) && ( - - ({t('participants.you')}) - - )} + + + + {getParticipantIsRoomOwner(participant) && t('participants.host')} + {getParticipantIsRoomAdmin(participant) && t('participants.cohost')} + {getParticipantIsRoomMember(participant) && + t('participants.member')} - {getParticipantIsRoomOwner(participant) && ( - {t('participants.host')} - )} - {getParticipantIsRoomAdmin(participant) && ( - {t('participants.cohost')} - )} - {getParticipantIsRoomMember(participant) && ( - {t('participants.member')} - )}
- + diff --git a/src/frontend/src/features/participants/components/RaisedHandRow.tsx b/src/frontend/src/features/participants/components/RaisedHandRow.tsx index 65572220..aa052692 100644 --- a/src/frontend/src/features/participants/components/RaisedHandRow.tsx +++ b/src/frontend/src/features/participants/components/RaisedHandRow.tsx @@ -1,7 +1,6 @@ import { css } from '@/styled-system/css' import { HStack } from '@/styled-system/jsx' -import { Text } from '@/primitives/Text' import { useTranslation } from 'react-i18next' import { Avatar } from '@/components/Avatar' import { useLowerHandParticipant } from '../api/lowerHandParticipant' @@ -11,6 +10,7 @@ import { isLocal } from '@/utils/livekit' import { RiHand } from '@remixicon/react' import { Button } from '@/primitives' import { AdminOrOwnerOnly } from '@/features/rooms/components/AdminOrOwnerOnly' +import { ParticipantName } from './ParticipantName' const ActionButton = ({ participant, @@ -42,9 +42,7 @@ type HandRaisedListItemProps = { } export const RaisedHandRow = ({ participant }: HandRaisedListItemProps) => { - const { t } = useTranslation('rooms') const name = participant.name || participant.identity - return ( { width: 'full', })} > - + - - - {name} - - {isLocal(participant) && ( - - ({t('participants.you')}) - - )} - + + + + + + - - - ) } diff --git a/src/frontend/src/features/participants/components/WaitingParticipantRow.tsx b/src/frontend/src/features/participants/components/WaitingParticipantRow.tsx index 66cdeaeb..6babb8a8 100644 --- a/src/frontend/src/features/participants/components/WaitingParticipantRow.tsx +++ b/src/frontend/src/features/participants/components/WaitingParticipantRow.tsx @@ -1,10 +1,11 @@ -import { Button, Text } from '@/primitives' +import { Button } from '@/primitives' import { HStack } from '@/styled-system/jsx' import { css } from '@/styled-system/css' import { Avatar } from '@/components/Avatar' import { useTranslation } from 'react-i18next' import { WaitingParticipant } from '../api/listWaitingParticipants' import { RiCloseLine } from '@remixicon/react' +import { ParticipantName } from './ParticipantName' export const WaitingParticipantRow = ({ participant, @@ -26,42 +27,11 @@ export const WaitingParticipantRow = ({ width: 'full', })} > - + - - - {participant.username} - - + - +
@@ -183,3 +179,25 @@ export const VisualOnlyTooltip = ({ ) } + +/** + * Wrapper component that displays a tooltip visually only (not announced by screen readers). + * + * This is necessary because TooltipTrigger from react-aria-components automatically adds + * aria-describedby on the button, which links the tooltip for accessibility. + * Even with aria-hidden="true" on the tooltip, screen readers still announce its content → duplication. + * This CSS wrapper avoids TooltipTrigger → no automatic aria-describedby → no duplication. + * + * Uses a portal to avoid being clipped by parent containers with overflow: hidden. + */ +export const VisualOnlyTooltip = ({ + children, + disabled, + ...props +}: VisualOnlyTooltipProps) => { + if (disabled) { + return children + } + + return {children} +}