From bfbf67327c5ccd33886384ec552c47bab0933fa2 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:49:48 +0100 Subject: [PATCH] fix: put events on the right day in right-to-left languages and slide the phone sidebar in from the start edge --- .../__tests__/calendar-display-range.test.tsx | 2 +- .../calendar-month-view-rtl.test.tsx | 56 +++++++++++++++++++ .../calendar-month-view-tasks.test.tsx | 4 +- components/calendar/calendar-app.tsx | 6 +- components/calendar/calendar-day-view.tsx | 6 +- components/calendar/calendar-month-view.tsx | 23 +++++--- .../calendar/calendar-sidebar-panel.tsx | 2 +- components/calendar/calendar-toolbar.tsx | 4 +- components/calendar/calendar-week-view.tsx | 10 ++-- 9 files changed, 89 insertions(+), 24 deletions(-) create mode 100644 components/calendar/__tests__/calendar-month-view-rtl.test.tsx diff --git a/components/calendar/__tests__/calendar-display-range.test.tsx b/components/calendar/__tests__/calendar-display-range.test.tsx index 08b0c5934..2135445a7 100644 --- a/components/calendar/__tests__/calendar-display-range.test.tsx +++ b/components/calendar/__tests__/calendar-display-range.test.tsx @@ -127,7 +127,7 @@ describe('calendar display hours and working days (#1164)', () => { expect(eventBlock('weekend')).toBeNull(); const trip = screen.getByText('Event trip').closest('.absolute')!; // Column 5 of 6, two columns wide (jsdom rounds the percentages) - expect(trip.style.left).toBe('calc(66.6667% + 1px)'); + expect(trip.style.insetInlineStart).toBe('calc(66.6667% + 1px)'); expect(trip.style.width).toBe('calc(33.3333% - 2px)'); }); diff --git a/components/calendar/__tests__/calendar-month-view-rtl.test.tsx b/components/calendar/__tests__/calendar-month-view-rtl.test.tsx new file mode 100644 index 000000000..b98840097 --- /dev/null +++ b/components/calendar/__tests__/calendar-month-view-rtl.test.tsx @@ -0,0 +1,56 @@ +import { render, screen } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import type { Calendar, CalendarEvent } from '@/lib/jmap/types'; + +// In Arabic the day grid mirrors, but the event chips laid over it were +// placed with `left`, so Sunday's event was drawn under Monday. And the +// phone's weekday headers were the first two letters of each name ("اث"). +vi.mock('next-intl', () => ({ + useTranslations: () => (key: string) => ({ 'days.mon': 'الاثنين' } as Record)[key] ?? key, + useLocale: () => 'ar', + useFormatter: () => ({ + dateTime: (d: Date, options: Intl.DateTimeFormatOptions) => new Intl.DateTimeFormat('ar', options).format(d), + }), +})); + +import { CalendarMonthView } from '../calendar-month-view'; + +const calendars = [{ id: 'cal-1', name: 'Work', color: '#123456' }] as unknown as Calendar[]; +const birthday = { + id: 'e1', uid: 'u1', title: 'Birthday', start: '2026-10-07T00:00:00', duration: 'P1D', showWithoutTime: true, + calendarIds: { 'cal-1': true }, status: 'confirmed', +} as unknown as CalendarEvent; + +function renderMonth(isMobile = false) { + return render( + , + ); +} + +describe('CalendarMonthView in a right-to-left language', () => { + it('places event chips from the start edge, so they follow the mirrored grid', () => { + renderMonth(); + const chip = screen.getByRole('button', { name: /Birthday/ }).parentElement as HTMLElement; + // Wednesday is the third column of a week starting on Monday. + expect(chip.style.insetInlineStart).toMatch(/^calc\(28\.57\d*% \+ 1px\)$/); + expect(chip.style.left).toBe(''); + }); + + it('heads the phone grid with the language\'s short weekday names', () => { + renderMonth(true); + const monday = screen.getAllByRole('columnheader')[0]; + expect(monday.textContent).toBe(new Intl.DateTimeFormat('ar', { weekday: 'narrow' }).format(new Date(2026, 8, 28))); + expect(monday.textContent).not.toBe('ال'); + }); +}); diff --git a/components/calendar/__tests__/calendar-month-view-tasks.test.tsx b/components/calendar/__tests__/calendar-month-view-tasks.test.tsx index d5447eb9d..bf75c14bc 100644 --- a/components/calendar/__tests__/calendar-month-view-tasks.test.tsx +++ b/components/calendar/__tests__/calendar-month-view-tasks.test.tsx @@ -97,10 +97,10 @@ describe('CalendarMonthView tasks', () => { // already has an event in row 0; Friday has no events. // (jsdom rounds the calc() percentage.) const thu = chipSlot('thu'); - expect(thu.style.left).toMatch(/^calc\(42\.857\d*% \+ 1px\)$/); + expect(thu.style.insetInlineStart).toMatch(/^calc\(42\.857\d*% \+ 1px\)$/); expect(thu.style.top).toBe('22px'); const fri = chipSlot('fri'); - expect(fri.style.left).toMatch(/^calc\(57\.142\d*% \+ 1px\)$/); + expect(fri.style.insetInlineStart).toMatch(/^calc\(57\.142\d*% \+ 1px\)$/); expect(fri.style.top).toBe('0px'); }); diff --git a/components/calendar/calendar-app.tsx b/components/calendar/calendar-app.tsx index f9a0f47b9..f7db803f9 100644 --- a/components/calendar/calendar-app.tsx +++ b/components/calendar/calendar-app.tsx @@ -1961,9 +1961,9 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps = "border-e border-border bg-secondary flex flex-col overflow-hidden flex-shrink-0", !isResizing && "transition-[width] duration-300", isNarrow && cn( - "absolute inset-y-0 left-0 z-50 w-72 pt-[env(safe-area-inset-top)]", + "absolute inset-y-0 start-0 z-50 w-72 pt-[env(safe-area-inset-top)]", "transform transition-transform duration-300 ease-in-out", - !narrowSidebarOpen && "-translate-x-full" + !narrowSidebarOpen && "-translate-x-full rtl:translate-x-full" ) )} style={isNarrow ? undefined : { width: `${calSidebarWidth}px` }} @@ -2137,7 +2137,7 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps = {isMobile && ( diff --git a/components/calendar/calendar-toolbar.tsx b/components/calendar/calendar-toolbar.tsx index 27a8526a4..3f81d26a8 100644 --- a/components/calendar/calendar-toolbar.tsx +++ b/components/calendar/calendar-toolbar.tsx @@ -410,7 +410,7 @@ export function CalendarToolbar({ onClick={onPrev} aria-label={t("nav_prev")} > - + )} diff --git a/components/calendar/calendar-week-view.tsx b/components/calendar/calendar-week-view.tsx index c122cfdcf..6f51c2ef4 100644 --- a/components/calendar/calendar-week-view.tsx +++ b/components/calendar/calendar-week-view.tsx @@ -512,7 +512,7 @@ export function CalendarWeekView({ key={`${segment.event.id}-${segment.startIndex}-${segment.row}`} className="absolute px-0.5 pointer-events-auto" style={{ - left: `calc(${(segment.startIndex / colCount) * 100}% + 1px)`, + insetInlineStart: `calc(${(segment.startIndex / colCount) * 100}% + 1px)`, width: `calc(${(segment.span / colCount) * 100}% - 2px)`, top: segment.row * 24 + 2, height: 20, @@ -571,7 +571,7 @@ export function CalendarWeekView({ key={`task-${task.id}`} className="absolute px-0.5 pointer-events-auto" style={{ - left: `calc(${(dayIndex / colCount) * 100}% + 1px)`, + insetInlineStart: `calc(${(dayIndex / colCount) * 100}% + 1px)`, width: `calc(${(1 / colCount) * 100}% - 2px)`, top: (baseRow + taskIndex) * 24 + 2, height: 20, @@ -607,7 +607,7 @@ export function CalendarWeekView({ inside its row, where the header cannot cover it. */} {(h > firstHour || firstHour > 0) && ( firstHour ? "top-0 -translate-y-1/2" : "top-1", isMobile ? "text-[9px]" : "text-[10px]", )}> @@ -688,7 +688,7 @@ export function CalendarWeekView({ key={ev.id} className="absolute z-10 group/event" data-calendar-event - style={{ top, height, left: `${leftPct}%`, width: `${widthPct}%`, paddingLeft: 1, paddingRight: 1 }} + style={{ top, height, insetInlineStart: `${leftPct}%`, width: `${widthPct}%`, paddingInline: 1 }} >
-
+
{formatSnapTime(dropTarget.minutes, timeFormat)}