From b957bbcd8a76a60cba99ee2082b8295ae04e9c3b Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 28 Oct 2026 12:56:47 +0100 Subject: [PATCH] fix: fit settings > calendar to touch screens and hide import and subscribe in the demo --- .../calendar-management-phone.test.tsx | 68 +++++++++++++++++++ .../settings/calendar-management-settings.tsx | 43 ++++++++---- locales/ar/common.json | 1 + locales/ca/common.json | 1 + locales/cs/common.json | 1 + locales/da/common.json | 1 + locales/de/common.json | 1 + locales/en/common.json | 1 + locales/es/common.json | 1 + locales/et/common.json | 1 + locales/fa/common.json | 1 + locales/fr/common.json | 1 + locales/he/common.json | 1 + locales/hu/common.json | 1 + locales/it/common.json | 1 + locales/ja/common.json | 1 + locales/ko/common.json | 1 + locales/lv/common.json | 1 + locales/mn/common.json | 1 + locales/nb/common.json | 1 + locales/nl/common.json | 1 + locales/pl/common.json | 1 + locales/pt/common.json | 1 + locales/ro/common.json | 1 + locales/ru/common.json | 1 + locales/sk/common.json | 1 + locales/th/common.json | 1 + locales/tr/common.json | 1 + locales/uk/common.json | 1 + locales/zh-TW/common.json | 1 + locales/zh/common.json | 1 + 31 files changed, 125 insertions(+), 15 deletions(-) create mode 100644 components/settings/__tests__/calendar-management-phone.test.tsx 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 && (