fix: show the full text of cut-off calendar, event and participant names on hover

This commit is contained in:
Linus Rath
2026-10-28 04:50:07 +01:00
parent fd8fd04423
commit fe19755d47
11 changed files with 32 additions and 22 deletions
@@ -591,3 +591,12 @@ describe('CalendarApp view menu', () => {
expect(useCalendarStore.getState().viewMode).toBe('day');
});
});
describe('CalendarApp long names', () => {
it('shows the full name of a cut-off calendar on hover', () => {
const long = { ...calendars[0], name: 'Stalwart Calendar (usera@internal.bulwarkmail.org)' };
useCalendarStore.setState({ calendars: [long] } as never);
renderApp();
expect(screen.getByTestId('calendar-item').querySelector('[title]')).toHaveAttribute('title', long.name);
});
});
+2 -2
View File
@@ -264,13 +264,13 @@ export function CalendarAgendaView({
/>
<div className="flex-1 min-w-0">
<div className={cn("text-sm font-medium truncate", isInactive && "line-through text-muted-foreground")}>
<div className={cn("text-sm font-medium truncate", isInactive && "line-through text-muted-foreground")} title={ev.title || t("events.no_title")}>
{ev.title || t("events.no_title")}
</div>
{(meta || getParticipantCount(ev) > 0) && (
<div className="flex items-center gap-2 text-xs text-muted-foreground mt-0.5 min-w-0">
{locationName && <MapPin className="w-3 h-3 flex-shrink-0" />}
{meta && <span className="truncate">{meta}</span>}
{meta && <span className="truncate" title={meta}>{meta}</span>}
{getParticipantCount(ev) > 0 && (
<span className="flex items-center gap-1 flex-shrink-0">
<Users className="w-3 h-3" />
+1 -1
View File
@@ -415,7 +415,7 @@ export function CalendarDayView({
>
{isCompleted && <Check className="h-2.5 w-2.5" />}
</button>
<span className={cn("truncate", isCompleted && "line-through text-muted-foreground")}>
<span className={cn("truncate", isCompleted && "line-through text-muted-foreground")} title={task.title || t("tasks.no_title")}>
{task.title || t("tasks.no_title")}
</span>
</div>
@@ -315,7 +315,7 @@ export function CalendarSidebarPanel({
>
{isVisible && <Check className="w-2.5 h-2.5" stroke={3.5} />}
</span>
<span className={cn("truncate", !isVisible && "text-muted-foreground")}>
<span className={cn("truncate", !isVisible && "text-muted-foreground")} title={cal.name}>
{cal.name}
</span>
{isSubscriptionCalendar(cal.id) && (
@@ -483,7 +483,7 @@ export function CalendarSidebarPanel({
) : (
<ChevronRight className="w-3 h-3 text-muted-foreground flex-shrink-0" />
)}
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider truncate">{label}</span>
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider truncate" title={label}>{label}</span>
</button>
{onAdd && (
<IconButton
@@ -545,7 +545,7 @@ export function CalendarSidebarPanel({
<ChevronRight className="w-3.5 h-3.5 text-muted-foreground flex-shrink-0" />
)}
<User className="w-3.5 h-3.5 text-muted-foreground flex-shrink-0" />
<span className="text-xs font-semibold text-foreground/90 truncate">
<span className="text-xs font-semibold text-foreground/90 truncate" title={group.label}>
{group.label}
</span>
{isActive && onCreateCalendar && (
+4 -4
View File
@@ -212,7 +212,7 @@ export function CalendarToolbar({
<ChevronLeft />
</IconButton>
)}
<span className="text-sm font-semibold text-center flex-1 select-none truncate">
<span className="text-sm font-semibold text-center flex-1 select-none truncate" title={getDateLabel()}>
{getDateLabel()}
</span>
{hasDateNav && (
@@ -284,7 +284,7 @@ export function CalendarToolbar({
)}
style={isVisible ? { backgroundColor: color, borderColor: color } : undefined}
/>
<span className={cn("truncate", !isVisible && "text-muted-foreground")}>
<span className={cn("truncate", !isVisible && "text-muted-foreground")} title={cal.name}>
{cal.name}
</span>
</button>
@@ -324,7 +324,7 @@ export function CalendarToolbar({
)}
style={isVisible ? { backgroundColor: color, borderColor: color } : undefined}
/>
<span className={cn("truncate", !isVisible && "text-muted-foreground")}>
<span className={cn("truncate", !isVisible && "text-muted-foreground")} title={cal.name}>
{cal.name}
</span>
</button>
@@ -406,7 +406,7 @@ export function CalendarToolbar({
</Button>
</>
)}
<h1 className="ms-2 text-base font-semibold truncate select-none">
<h1 className="ms-2 text-base font-semibold truncate select-none" title={getDateLabel()}>
{getDateLabel()}
</h1>
+2 -2
View File
@@ -541,7 +541,7 @@ export function EventDetailPopover({
<div className="mt-1 space-y-0.5">
{participants.slice(0, 5).map((p) => (
<div key={p.id} className="flex items-center justify-between gap-2 text-xs">
<span className="truncate text-foreground">
<span className="truncate text-foreground" title={p.email}>
{p.name || p.email}
{p.isOrganizer && (
<span className="text-muted-foreground ms-1">
@@ -582,7 +582,7 @@ export function EventDetailPopover({
{calendar && (
<div className="flex items-start gap-2.5">
<CalendarIcon className="w-4 h-4 text-muted-foreground mt-0.5 flex-shrink-0" />
<span className="text-sm text-foreground truncate">{calendar.name}</span>
<span className="text-sm text-foreground truncate" title={calendar.name}>{calendar.name}</span>
</div>
)}
+4 -4
View File
@@ -967,7 +967,7 @@ export function EventModal({
return (
<div ref={modalRef} role="dialog" aria-modal={isMobile || undefined} aria-label={event.title || t("events.no_title")} className={isMobile ? mobileRootClass : "flex flex-col h-full bg-background"}>
<div className="flex items-center justify-between gap-2 px-6 py-4 border-b border-border flex-shrink-0">
<h2 className="text-lg font-semibold truncate">{event.title || t("events.no_title")}</h2>
<h2 className="text-lg font-semibold truncate" title={event.title || t("events.no_title")}>{event.title || t("events.no_title")}</h2>
{onMore && (
<IconButton label={t("toolbar.more")} aria-haspopup="menu" className="ms-auto flex-shrink-0" onClick={(e) => onMore(e.currentTarget)}>
<MoreVertical />
@@ -1048,7 +1048,7 @@ export function EventModal({
<div className="space-y-1 ps-5">
{participants.map(p => (
<div key={p.id} className="flex items-center justify-between text-sm">
<span className="truncate">
<span className="truncate" title={p.email}>
{p.name || p.email}
{p.isOrganizer && (
<span className="text-muted-foreground ms-1">({t("participants.organizer").toLowerCase()})</span>
@@ -1095,7 +1095,7 @@ export function EventModal({
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="w-2.5 h-2.5 rounded-full flex-shrink-0" style={{ backgroundColor: color }} />
<h2 className="text-lg font-semibold truncate">{event.title || t("events.no_title")}</h2>
<h2 className="text-lg font-semibold truncate" title={event.title || t("events.no_title")}>{event.title || t("events.no_title")}</h2>
</div>
{eventCalendar && (
<p className="text-xs text-muted-foreground mt-0.5 ps-[18px]">{eventCalendar.name}</p>
@@ -1205,7 +1205,7 @@ export function EventModal({
<div className="mt-1 space-y-0.5">
{viewParticipants.map((p) => (
<div key={p.id} className="flex items-center justify-between gap-2 text-xs">
<span className="truncate text-foreground">
<span className="truncate text-foreground" title={p.email}>
{p.name || p.email}
{p.isOrganizer && (
<span className="text-muted-foreground ms-1">({t("participants.organizer").toLowerCase()})</span>
+1 -1
View File
@@ -380,7 +380,7 @@ export function ICalImportModal({ calendars, client, onClose, initialUrl }: ICal
className="mt-0.5 rounded border-input"
/>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">
<p className="text-sm font-medium truncate" title={event.title || tCal("events.no_title")}>
{event.title || tCal("events.no_title")}
</p>
<div className="flex items-center gap-2 mt-0.5">
@@ -188,7 +188,7 @@ export function ParticipantAvailability({ attendees, start, end }: ParticipantAv
)}
aria-hidden="true"
/>
<span className="min-w-0 truncate text-foreground/90">{a.name || a.email}</span>
<span className="min-w-0 truncate text-foreground/90" title={a.email}>{a.name || a.email}</span>
<span className="ms-auto flex shrink-0 items-center gap-1 text-muted-foreground">
{status === "checking" && <Loader2 className="h-3 w-3 animate-spin" />}
{t(status)}
+4 -3
View File
@@ -182,9 +182,9 @@ export const ParticipantInput = forwardRef<ParticipantInputHandle, ParticipantIn
>
<Avatar name={s.name} email={s.email} size="sm" className="shrink-0 w-7 h-7 text-[10px]" />
<div className="min-w-0">
<div className="font-medium truncate">{s.name || s.email}</div>
<div className="font-medium truncate" title={s.name || s.email}>{s.name || s.email}</div>
{s.name && (
<div className="text-xs text-muted-foreground truncate">{s.email}</div>
<div className="text-xs text-muted-foreground truncate" title={s.email}>{s.email}</div>
)}
</div>
</li>
@@ -211,11 +211,12 @@ export const ParticipantInput = forwardRef<ParticipantInputHandle, ParticipantIn
}}
className="truncate hover:underline focus:outline-none focus:underline cursor-text"
aria-label={`${t("edit")} ${p.name || p.email}`}
title={p.email}
>
{p.name || p.email}
</button>
) : (
<span className="truncate">{p.name || p.email}</span>
<span className="truncate" title={p.email}>{p.name || p.email}</span>
)}
{!disabled && (
<button
+1 -1
View File
@@ -234,7 +234,7 @@ export function TaskListView({
<span className={cn(
"text-sm font-medium truncate",
isCompleted && "line-through text-muted-foreground"
)}>
)} title={task.title || t("tasks.no_title")}>
{task.title || t("tasks.no_title")}
</span>
{priorityIcon}