From 86b56d490af86b1ec2c72eb6ad77f55cbb291774 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 28 Oct 2026 10:01:34 +0100 Subject: [PATCH] fix: give tasks in the phone day view a full row with a tappable circle and drop the empty all-day block --- .../calendar-day-view-phone.test.tsx | 66 +++++++++++++++++++ components/calendar/calendar-day-view.tsx | 29 ++++---- components/calendar/task-chip.tsx | 23 +++++-- 3 files changed, 101 insertions(+), 17 deletions(-) create mode 100644 components/calendar/__tests__/calendar-day-view-phone.test.tsx diff --git a/components/calendar/__tests__/calendar-day-view-phone.test.tsx b/components/calendar/__tests__/calendar-day-view-phone.test.tsx new file mode 100644 index 000000000..c7365c4c4 --- /dev/null +++ b/components/calendar/__tests__/calendar-day-view-phone.test.tsx @@ -0,0 +1,66 @@ +import { render, screen, within } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import { CalendarDayView } from '../calendar-day-view'; +import type { Calendar, CalendarEvent, CalendarTask } from '@/lib/jmap/types'; + +// CAL-27 follow-up: the phone day view drew tasks as 10px chips with a +// circle too small to tap, and a busy neighbouring day left an empty +// all-day block of ~150px above the hours of a day with none. + +const calendars = [{ id: 'cal-1', name: 'Work', color: '#123456' }] as unknown as Calendar[]; + +const holiday = { + id: 'ev-1', calendarIds: { 'cal-1': true }, title: 'Holiday', start: '2026-09-10T00:00:00', + duration: 'P1D', showWithoutTime: true, +} as unknown as CalendarEvent; + +const task = { + id: 't1', '@type': 'Task', title: 'Buy milk', calendarIds: { 'cal-1': true }, + progress: 'needs-action', priority: 0, due: '2026-09-09', showWithoutTime: true, +} as unknown as CalendarTask; + +function renderDay(isMobile: boolean, tasks: CalendarTask[] = [task]) { + return render( + , + ); +} + +describe('day view on a phone', () => { + it('gives each task a full row with a 44px target for the circle', () => { + renderDay(true); + const chip = document.querySelector('[data-calendar-task="t1"]')!; + expect(chip).toHaveClass('min-h-11', 'text-xs'); + expect(within(chip).getByRole('button', { name: /mark_complete|Mark as complete/i })).toHaveClass('w-11', 'self-stretch'); + }); + + it('keeps the small shared chip on a wide screen', () => { + renderDay(false); + const chip = document.querySelector('[data-calendar-task="t1"]')!; + expect(chip).toHaveClass('h-full', 'text-[10px]'); + }); + + it('sizes the all-day row by the day in view, not by a busy neighbour', () => { + renderDay(true, []); + const today = document.querySelector('[data-day="2026-09-09"]')!; + const next = document.querySelector('[data-day="2026-09-10"]')!; + // The day in view has nothing all-day: no empty block under its date. + expect(within(today).queryByText(/all_day|All day/i)).toBeNull(); + expect(today).not.toHaveClass('h-0'); + // The neighbour's all-day event takes no height of its own. + expect(within(next).getByRole('button', { name: /Holiday/ })).toBeInTheDocument(); + expect(next).toHaveClass('h-0', 'min-h-full', 'overflow-hidden'); + expect(screen.getAllByRole('columnheader')).toHaveLength(3); + }); +}); diff --git a/components/calendar/calendar-day-view.tsx b/components/calendar/calendar-day-view.tsx index d927ee602..b2518068b 100644 --- a/components/calendar/calendar-day-view.tsx +++ b/components/calendar/calendar-day-view.tsx @@ -197,14 +197,6 @@ export function CalendarDayView({ return map; }, [days, eventsByDay, hours]); - const hasAllDayArea = useMemo( - () => days.some((day) => { - const key = format(day, "yyyy-MM-dd"); - return (eventsByDay.get(key)?.allDay.length ?? 0) > 0 || (tasksByDay.get(key)?.length ?? 0) > 0; - }), - [days, eventsByDay, tasksByDay], - ); - const scrollToFocus = useCallback(() => { const root = rootRef.current; if (!root) return; @@ -232,6 +224,10 @@ export function CalendarDayView({ // not used: browsers re-snap on their own when columns are prepended, // which would double the scroll correction.) const visibleKeyRef = useRef(null); + // The day in view: its all-day events and tasks size the row under the + // dates, so a busy neighbour doesn't leave an empty block above the hours. + const [visibleKey, setVisibleKey] = useState(() => dayKey(focus.date)); + useEffect(() => { setVisibleKey(dayKey(focus.date)); }, [focus.date, focus.nonce]); const scrollFrameRef = useRef(null); const snapTimerRef = useRef | null>(null); const handleStripScroll = useCallback(() => { @@ -255,6 +251,7 @@ export function CalendarDayView({ const key = dayKey(day); if (key === visibleKeyRef.current) return; visibleKeyRef.current = key; + setVisibleKey(key); onVisibleDateChange?.(day); }); }, [colWidth, colCount, days, onVisibleDateChange]); @@ -352,7 +349,16 @@ export function CalendarDayView({ const allDayEvents = eventsByDay.get(key)?.allDay ?? []; const dayTasks = tasksByDay.get(key) ?? []; return ( -
+
- {hasAllDayArea && ( + {(allDayEvents.length > 0 || dayTasks.length > 0) && (
{ @@ -414,12 +420,13 @@ export function CalendarDayView({
{/* The same chip as the month and week grids. */} {dayTasks.map((task) => ( -
+
task.calendarIds[c.id])} onToggleComplete={onToggleTaskComplete} onSelect={onSelectTask} + touch={isMobile} />
))} diff --git a/components/calendar/task-chip.tsx b/components/calendar/task-chip.tsx index ccea5a78d..7df48a12d 100644 --- a/components/calendar/task-chip.tsx +++ b/components/calendar/task-chip.tsx @@ -10,6 +10,8 @@ interface CalendarTaskChipProps { calendar?: Calendar; onToggleComplete?: (task: CalendarTask) => void; onSelect?: (task: CalendarTask) => void; + /** A full row for a finger: 44px tall, a bigger circle with a 44px hit area (phone day view). */ + touch?: boolean; className?: string; } @@ -17,7 +19,7 @@ interface CalendarTaskChipProps { * A task on its due day in the month and week grids (#1107). The circle * toggles completion; the title opens the task in the editor. */ -export function CalendarTaskChip({ task, calendar, onToggleComplete, onSelect, className }: CalendarTaskChipProps) { +export function CalendarTaskChip({ task, calendar, onToggleComplete, onSelect, touch = false, className }: CalendarTaskChipProps) { const t = useTranslations("calendar"); const isCompleted = task.progress === "completed"; const color = calendar?.color || "#3b82f6"; @@ -26,30 +28,39 @@ export function CalendarTaskChip({ task, calendar, onToggleComplete, onSelect, c return (