diff --git a/components/settings/__tests__/calendar-management-phone.test.tsx b/components/settings/__tests__/calendar-management-phone.test.tsx new file mode 100644 index 000000000..f9f67fbc6 --- /dev/null +++ b/components/settings/__tests__/calendar-management-phone.test.tsx @@ -0,0 +1,68 @@ +import { render, screen } from '@testing-library/react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import type { Calendar } from '@/lib/jmap/types'; +import { useAuthStore } from '@/stores/auth-store'; +import { useCalendarStore } from '@/stores/calendar-store'; + +// Settings > Calendar on a phone: no right-click hint on a touch screen, a +// colour dot that can be hit, and no Import or Subscribe in the demo, which +// Calendar itself hides there too. + +const env = vi.hoisted(() => ({ touch: false, demoMode: false })); +vi.mock('@/hooks/use-media-query', () => ({ + useMediaQuery: (query: string) => (query === '(pointer: coarse)' ? env.touch : false), + useIsMobile: () => env.touch, + useIsDesktop: () => !env.touch, +})); +vi.mock('@/hooks/use-config', () => ({ useConfig: () => ({ demoMode: env.demoMode }) })); + +const { CalendarManagementSettings } = await import('../calendar-management-settings'); + +const work = { + id: 'c', name: 'Work', color: '#3b82f6', isDefault: true, isShared: false, shareWith: null, + myRights: { mayShare: true, mayDelete: true, mayWriteAll: true }, +} as unknown as Calendar; + +const initial = { auth: useAuthStore.getState(), calendar: useCalendarStore.getState() }; +beforeEach(() => { + env.touch = false; + env.demoMode = false; + useAuthStore.setState({ client: { getAccountId: () => 'acc' }, serverUrl: null, username: null } as never); + useCalendarStore.setState({ calendars: [work], icalSubscriptions: [] } as never); +}); +afterEach(() => { + useAuthStore.setState(initial.auth, true); + useCalendarStore.setState(initial.calendar, true); +}); + +describe('Settings > Calendar on a phone', () => { + it('mentions right-click only where there is a mouse', () => { + const { unmount } = render(); + expect(screen.getByText('description')).toBeInTheDocument(); + unmount(); + + env.touch = true; + render(); + expect(screen.queryByText('description')).toBeNull(); + expect(screen.getByText('description_touch')).toBeInTheDocument(); + }); + + it('gives the colour dot a 44px hit area on touch', () => { + render(); + const dot = screen.getByRole('button', { name: 'change_color' }); + expect(dot.className).toContain('pointer-coarse:after:min-h-11'); + expect(dot.className).toContain('pointer-coarse:after:min-w-11'); + }); + + it('offers Import and Subscribe only outside the demo', () => { + const { unmount } = render(); + // Both dialogs' titles are "title" here: one button each. + expect(screen.getAllByRole('button', { name: 'title' })).toHaveLength(2); + unmount(); + + env.demoMode = true; + render(); + expect(screen.queryByRole('button', { name: 'title' })).toBeNull(); + expect(screen.getByRole('button', { name: 'add_calendar' })).toBeInTheDocument(); + }); +}); diff --git a/components/settings/calendar-management-settings.tsx b/components/settings/calendar-management-settings.tsx index f8841ba76..00d901eef 100644 --- a/components/settings/calendar-management-settings.tsx +++ b/components/settings/calendar-management-settings.tsx @@ -29,6 +29,9 @@ import { IS_LITE } from '@/lib/lite'; import { shareRecipientNames, useShareRecipientLabels } from '@/hooks/use-share-recipient-labels'; import { calendarAccountKey, moveCalendar } from '@/lib/calendar-sort'; import { ConfirmDialog } from '@/components/ui/confirm-dialog'; +import { TOUCH_HIT_AREA } from '@/components/ui/button'; +import { useMediaQuery } from '@/hooks/use-media-query'; +import { useConfig } from '@/hooks/use-config'; function CalendarColorPicker({ value, @@ -291,6 +294,10 @@ export function CalendarManagementSettings() { const colorPickerRef = useRef(null); const [draggingId, setDraggingId] = useState(null); const { contextMenu, openContextMenu, openContextMenuFrom, closeContextMenu, menuRef } = useContextMenu(); + // No right-click on a touch screen, so the hint about it goes there. + const touch = useMediaQuery('(pointer: coarse)'); + // The demo has no server to import into or subscribe from, as in Calendar. + const { demoMode } = useConfig(); const listedCalendars = calendars .filter(cal => !isSubscriptionCalendar(cal.id)) @@ -620,7 +627,7 @@ export function CalendarManagementSettings() { }; return ( - + {renderRowMenu()}
{listedCalendars.map((cal) => { @@ -686,7 +693,10 @@ export function CalendarManagementSettings() { - + {!demoMode && ( + + )} {/* ICS URL subscriptions refresh via /api/fetch-ical, absent in the static build. */} - {!IS_LITE && ( + {!IS_LITE && !demoMode && (