diff --git a/components/calendar/__tests__/calendar-rtl-text.test.tsx b/components/calendar/__tests__/calendar-rtl-text.test.tsx new file mode 100644 index 000000000..b43bbc478 --- /dev/null +++ b/components/calendar/__tests__/calendar-rtl-text.test.tsx @@ -0,0 +1,65 @@ +import { render, screen, within } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import { EventCard } from '../event-card'; +import { CalendarSearchResults } from '../calendar-search'; +import { CalendarAgendaView } from '../calendar-agenda-view'; +import type { Calendar, CalendarEvent } from '@/lib/jmap/types'; + +// In Arabic, an English title took the page's direction (".Dinner with +// Carlos and co", cut at the start) and "09:15 – 09:30" read "09:30 – 09:15". + +vi.mock('@/lib/timezone', async (importOriginal) => ({ + ...(await importOriginal()), + displayNow: () => new Date(2026, 9, 28, 8, 0), +})); + +const calendars = [{ id: 'cal-1', name: 'Work', color: '#123456' }] as unknown as Calendar[]; +const dinner = { + id: 'e1', '@type': 'Event', uid: 'e1', title: 'Dinner with Carlos and co.', start: '2026-10-28T09:15:00', + duration: 'PT1H', showWithoutTime: false, status: 'confirmed', calendarIds: { 'cal-1': true }, +} as unknown as CalendarEvent; + +const isolatedLtr = (el: HTMLElement) => el.closest('bdi')?.getAttribute('dir') === 'ltr'; + +describe('user text and times in a right-to-left layout', () => { + it.each(['chip', 'span', 'block'] as const)('gives the event title its own direction (%s)', (variant) => { + render(); + expect(screen.getByText('Dinner with Carlos and co.')).toHaveAttribute('dir', 'auto'); + }); + + it('keeps start before end in a block', () => { + render(); + expect(isolatedLtr(screen.getByText('09:15 – 10:15'))).toBe(true); + }); + + it('does the same in search results', () => { + render( + , + ); + const row = screen.getByRole('button', { name: /Dinner/ }); + expect(within(row).getByText('Dinner with Carlos and co.')).toHaveAttribute('dir', 'auto'); + expect(isolatedLtr(within(row).getByText('09:15 – 10:15'))).toBe(true); + }); + + it('does the same in the agenda', () => { + render( + , + ); + expect(screen.getByText('Dinner with Carlos and co.')).toHaveAttribute('dir', 'auto'); + expect(isolatedLtr(screen.getByText('09:15 – 10:15'))).toBe(true); + }); +}); diff --git a/components/calendar/calendar-agenda-view.tsx b/components/calendar/calendar-agenda-view.tsx index 850321a93..4dd69715a 100644 --- a/components/calendar/calendar-agenda-view.tsx +++ b/components/calendar/calendar-agenda-view.tsx @@ -263,7 +263,7 @@ export function CalendarAgendaView({ style={{ paddingBlock: 'var(--density-item-py)' }} > - {ev.showWithoutTime ? t("events.all_day") : `${formatTime(start)} – ${formatTime(end)}`} + {ev.showWithoutTime ? t("events.all_day") : {`${formatTime(start)} – ${formatTime(end)}`}} {/* Calendar colour as a dot on the title line; hollow when @@ -275,13 +275,17 @@ export function CalendarAgendaView({ />
-
- {ev.title || t("events.no_title")} + {/* Sized to the title, which keeps its own direction: it sits at + the start of the row and is cut at its own end. */} +
+ + {ev.title || t("events.no_title")} +
{(meta || getParticipantCount(ev) > 0) && (
{locationName && } - {meta && {meta}} + {meta && {meta}} {getParticipantCount(ev) > 0 && ( diff --git a/components/calendar/calendar-day-view.tsx b/components/calendar/calendar-day-view.tsx index 0d532d52c..e5ee06ff6 100644 --- a/components/calendar/calendar-day-view.tsx +++ b/components/calendar/calendar-day-view.tsx @@ -620,7 +620,7 @@ export function CalendarDayView({ }} >
- {formatSnapTime(dragCreate.startMinutes, timeFormat)} – {formatSnapTime(dragCreate.endMinutes, timeFormat)} + {formatSnapTime(dragCreate.startMinutes, timeFormat)} – {formatSnapTime(dragCreate.endMinutes, timeFormat)}
)} @@ -666,7 +666,7 @@ export function CalendarDayView({ {pendingPreview.title}
- {formatSnapTime(startMin, timeFormat)} – {formatSnapTime(startMin + durationMin, timeFormat)} + {formatSnapTime(startMin, timeFormat)} – {formatSnapTime(startMin + durationMin, timeFormat)}
); diff --git a/components/calendar/calendar-search.tsx b/components/calendar/calendar-search.tsx index a46b9d5e6..b12ef8aef 100644 --- a/components/calendar/calendar-search.tsx +++ b/components/calendar/calendar-search.tsx @@ -151,7 +151,7 @@ export function CalendarSearchResults({ : { month: "short", day: "numeric", year: "numeric" })} - {ev.showWithoutTime ? t("events.all_day") : `${formatTime(start)} – ${formatTime(end)}`} + {ev.showWithoutTime ? t("events.all_day") : {`${formatTime(start)} – ${formatTime(end)}`}}