mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
fix: show the full text of cut-off calendar, event and participant names on hover
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user