diff --git a/components/calendar/__tests__/calendar-app.test.tsx b/components/calendar/__tests__/calendar-app.test.tsx
index 87de6b7da..bb1fce2ce 100644
--- a/components/calendar/__tests__/calendar-app.test.tsx
+++ b/components/calendar/__tests__/calendar-app.test.tsx
@@ -580,3 +580,14 @@ describe('CalendarApp in the demo', () => {
}
});
});
+
+describe('CalendarApp view menu', () => {
+ it('opens as a menu the keyboard can use and switches the view', async () => {
+ renderApp();
+ fireEvent.click(screen.getByTestId('calendar-view-menu'), { detail: 0 });
+ const day = await screen.findByRole('menuitemradio', { name: /views\.day/ });
+ await waitFor(() => expect(document.activeElement?.getAttribute('role')).toBe('menuitemradio'));
+ fireEvent.click(day);
+ expect(useCalendarStore.getState().viewMode).toBe('day');
+ });
+});
diff --git a/components/calendar/calendar-app.tsx b/components/calendar/calendar-app.tsx
index d02991ab5..3dce4c269 100644
--- a/components/calendar/calendar-app.tsx
+++ b/components/calendar/calendar-app.tsx
@@ -20,6 +20,7 @@ import { toast } from "@/stores/toast-store";
import { useIsDesktop, useIsMobile } from "@/hooks/use-media-query";
import { useAccountPrincipalEmails } from "@/hooks/use-account-principal-emails";
import { Button } from "@/components/ui/button";
+import { PaneSpinner, Spinner } from "@/components/ui/spinner";
import { CalendarToolbar } from "@/components/calendar/calendar-toolbar";
import { CalendarMonthView } from "@/components/calendar/calendar-month-view";
import { CalendarWeekView } from "@/components/calendar/calendar-week-view";
@@ -1719,11 +1720,7 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps =
const renderView = () => {
if (isLoading && calendars.length === 0) {
- return (
-
-
{t("status.loading_calendars")}
-
- );
+ return ;
}
const viewContent = (() => {
@@ -1867,7 +1864,7 @@ export function CalendarApp({ linkSegments: routeSegments }: CalendarAppProps =
{viewContent}
{isLoadingEvents && calendars.length > 0 && events.length === 0 && (
)}
diff --git a/components/calendar/calendar-sidebar-panel.tsx b/components/calendar/calendar-sidebar-panel.tsx
index ae5f09512..b837980c2 100644
--- a/components/calendar/calendar-sidebar-panel.tsx
+++ b/components/calendar/calendar-sidebar-panel.tsx
@@ -416,7 +416,7 @@ export function CalendarSidebarPanel({
)}
{canShare && (
{ closeContextMenu(); onShareCalendar(cal); }}
/>
diff --git a/components/calendar/calendar-toolbar.tsx b/components/calendar/calendar-toolbar.tsx
index b5b40bad0..543c938e9 100644
--- a/components/calendar/calendar-toolbar.tsx
+++ b/components/calendar/calendar-toolbar.tsx
@@ -101,8 +101,12 @@ export function CalendarToolbar({
const menuViews: CalendarViewMode[] = enableCalendarTasks
? ["day", "week", "month", "agenda", "tasks"]
: ["day", "week", "month", "agenda"];
- const [showViewMenu, setShowViewMenu] = useState(false);
- const viewMenuRef = useRef(null);
+ const {
+ contextMenu: viewMenu,
+ openContextMenuFrom: openViewMenuFrom,
+ closeContextMenu: closeViewMenu,
+ menuRef: viewMenuRef,
+ } = useContextMenu();
const [showCalendarDropdown, setShowCalendarDropdown] = useState(false);
const dropdownRef = useRef(null);
@@ -182,25 +186,6 @@ export function CalendarToolbar({
>
) : null;
- useEffect(() => {
- if (!showViewMenu) return;
- function handleClickOutside(e: MouseEvent) {
- if (viewMenuRef.current && !viewMenuRef.current.contains(e.target as Node)) {
- setShowViewMenu(false);
- }
- }
- function handleKey(e: KeyboardEvent) {
- if (e.key === "Escape") {
- setShowViewMenu(false);
- }
- }
- document.addEventListener("mousedown", handleClickOutside);
- document.addEventListener("keydown", handleKey);
- return () => {
- document.removeEventListener("mousedown", handleClickOutside);
- document.removeEventListener("keydown", handleKey);
- };
- }, [showViewMenu]);
@@ -429,42 +414,39 @@ export function CalendarToolbar({
{moreButton}
-
+
- {showViewMenu && (
-
- {menuViews.map((v) => (
-
- ))}
-
- )}
+
+ {menuViews.map((v) => (
+
+ ))}
+
);
diff --git a/components/calendar/event-context-menu.tsx b/components/calendar/event-context-menu.tsx
index 39e0d3213..81786575b 100644
--- a/components/calendar/event-context-menu.tsx
+++ b/components/calendar/event-context-menu.tsx
@@ -8,11 +8,11 @@ import {
} from "@/components/ui/context-menu";
import {
Pencil,
+ CopyPlus,
Copy,
Download,
- ClipboardCopy,
+ Video,
Link as LinkIcon,
- CalendarArrowUp as CalendarLinkIcon,
Trash2,
} from "@/components/icons";
import type { CalendarEvent } from "@/lib/jmap/types";
@@ -69,7 +69,7 @@ export function EventContextMenu({
return (
{editable && }
-
+
{hasMeetingLink && onCopyMeetingLink && (
@@ -91,7 +91,7 @@ export function EventContextMenu({
{/* Permalink into Bulwark (#733) - distinct from the meeting URL above,
which points at whatever conferencing tool the organiser used. */}
{
void copyLink(buildCalendarPath({
diff --git a/components/calendar/event-detail-popover.tsx b/components/calendar/event-detail-popover.tsx
index 42a90c0db..cec55f9c6 100644
--- a/components/calendar/event-detail-popover.tsx
+++ b/components/calendar/event-detail-popover.tsx
@@ -7,7 +7,7 @@ import { Button } from "@/components/ui/button";
import { LinkifiedText } from "@/components/ui/linkified-text";
import {
X, Clock, MapPin, Video, Users, Repeat, Bell, AlignLeft,
- Pencil, Trash2, Copy, Send, Check, MoreVertical, Calendar as CalendarIcon,
+ Pencil, Trash2, Copy, CopyPlus, Send, Check, MoreVertical, Calendar as CalendarIcon,
} from "@/components/icons";
import { format, isSameDay } from "date-fns";
import { cn } from "@/lib/utils";
@@ -383,7 +383,7 @@ export function EventDetailPopover({
aria-label={onMore ? t("toolbar.more") : t("events.duplicate")}
title={onMore ? t("toolbar.more") : t("events.duplicate")}
>
- {onMore ? : }
+ {onMore ? : }
)}
@@ -1621,7 +1621,7 @@ export function EventModal({
aria-label={t("events.duplicate")}
className="w-full"
>
-
+
{t("events.duplicate")}
)}