diff --git a/components/calendar/__tests__/event-card.test.tsx b/components/calendar/__tests__/event-card.test.tsx index 11316f99e..71fd47d19 100644 --- a/components/calendar/__tests__/event-card.test.tsx +++ b/components/calendar/__tests__/event-card.test.tsx @@ -1,5 +1,5 @@ -import { render, screen, cleanup } from '@testing-library/react'; -import { describe, it, expect, afterEach, beforeEach } from 'vitest'; +import { render, screen, cleanup, fireEvent } from '@testing-library/react'; +import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest'; import { EventCard } from '../event-card'; import { useSettingsStore } from '@/stores/settings-store'; import type { Calendar, CalendarEvent, CalendarParticipant } from '@/lib/jmap/types'; @@ -139,3 +139,20 @@ describe('EventCard colour settings', () => { expect(calendar.color).toBe('#22c55e'); }); }); + +describe('EventCard from the keyboard (MA-05)', () => { + afterEach(cleanup); + + it.each(['chip', 'span', 'block'] as const)('opens the event menu with Shift+F10 on the %s', (variant) => { + const onContextMenu = vi.fn(); + const event = makeOccurrence('a', 'accepted'); + render(); + const card = screen.getByRole('button'); + fireEvent.keyDown(card, { key: 'F10' }); + expect(onContextMenu).not.toHaveBeenCalled(); + fireEvent.keyDown(card, { key: 'F10', shiftKey: true }); + expect(onContextMenu).toHaveBeenCalledOnce(); + expect(onContextMenu.mock.calls[0][1]).toBe(event); + expect(onContextMenu.mock.calls[0][0].currentTarget).toBe(card); + }); +}); diff --git a/components/calendar/event-card.tsx b/components/calendar/event-card.tsx index 41c0fa219..3c4ee7357 100644 --- a/components/calendar/event-card.tsx +++ b/components/calendar/event-card.tsx @@ -191,6 +191,21 @@ export function EventCard({ event, calendar, variant, onClick, onMouseEnter, onM preventDefault: () => {}, stopPropagation: () => {}, } as unknown as React.MouseEvent, event); }, !!onContextMenu); + // Shift+F10 opens the same menu from the keyboard (the menu key already + // fires contextmenu by itself). Prevented here, so a browser that also + // turns Shift+F10 into a contextmenu event doesn't open it twice. + const handleKeyDown = onContextMenu ? (e: React.KeyboardEvent) => { + if (!(e.shiftKey && e.key === "F10")) return; + e.preventDefault(); + e.stopPropagation(); + const target = e.currentTarget; + const rect = target.getBoundingClientRect(); + onContextMenu({ + type: "contextmenu", clientX: rect.left, clientY: rect.bottom, detail: 0, currentTarget: target, target, + preventDefault: () => {}, stopPropagation: () => {}, + } as unknown as React.MouseEvent, event); + } : undefined; + const touchProps = onContextMenu ? { onTouchStart: (e: React.TouchEvent) => { pressedRef.current = e.currentTarget; longPress.onTouchStart(e); }, onTouchMove: longPress.onTouchMove, @@ -205,6 +220,7 @@ export function EventCard({ event, calendar, variant, onClick, onMouseEnter, onM onMouseEnter={(e) => onMouseEnter?.(e.currentTarget.getBoundingClientRect())} onMouseLeave={() => onMouseLeave?.()} onContextMenu={handleContextMenu} + onKeyDown={handleKeyDown} {...touchProps} aria-label={ariaLabel} tabIndex={tabbable ? undefined : -1} @@ -236,6 +252,7 @@ export function EventCard({ event, calendar, variant, onClick, onMouseEnter, onM onMouseEnter={(e) => onMouseEnter?.(e.currentTarget.getBoundingClientRect())} onMouseLeave={() => onMouseLeave?.()} onContextMenu={handleContextMenu} + onKeyDown={handleKeyDown} {...touchProps} aria-label={ariaLabel} tabIndex={tabbable ? undefined : -1} @@ -270,6 +287,7 @@ export function EventCard({ event, calendar, variant, onClick, onMouseEnter, onM onMouseEnter={(e) => onMouseEnter?.(e.currentTarget.getBoundingClientRect())} onMouseLeave={() => onMouseLeave?.()} onContextMenu={handleContextMenu} + onKeyDown={handleKeyDown} {...touchProps} aria-label={ariaLabel} tabIndex={tabbable ? undefined : -1}