fix: calendar invitation picker clipping #250

This commit is contained in:
Linus Rath
2026-05-05 17:17:01 +02:00
parent 853b0eb855
commit 7b058ed0ac
@@ -1,6 +1,7 @@
'use client'; 'use client';
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback, useRef } from 'react';
import { createPortal } from 'react-dom';
import { import {
ArrowRight, ArrowRight,
Calendar, Calendar,
@@ -371,6 +372,8 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
const [actionError, setActionError] = useState<string | null>(null); const [actionError, setActionError] = useState<string | null>(null);
const [isProcessing, setIsProcessing] = useState(false); const [isProcessing, setIsProcessing] = useState(false);
const [showCalendarPicker, setShowCalendarPicker] = useState(false); const [showCalendarPicker, setShowCalendarPicker] = useState(false);
const [pickerPosition, setPickerPosition] = useState<{ top: number; left: number } | null>(null);
const pickerTriggerRef = useRef<HTMLButtonElement>(null);
const [selectedCalendarId, setSelectedCalendarId] = useState<string>(''); const [selectedCalendarId, setSelectedCalendarId] = useState<string>('');
const [rawIcsMethod, setRawIcsMethod] = useState<InvitationMethod>('unknown'); const [rawIcsMethod, setRawIcsMethod] = useState<InvitationMethod>('unknown');
const [isCollapsed, setIsCollapsed] = useState(true); const [isCollapsed, setIsCollapsed] = useState(true);
@@ -437,6 +440,17 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
} }
}, [calendars, selectedCalendarId]); }, [calendars, selectedCalendarId]);
useEffect(() => {
if (!showCalendarPicker) return;
const close = () => setShowCalendarPicker(false);
window.addEventListener('scroll', close, true);
window.addEventListener('resize', close);
return () => {
window.removeEventListener('scroll', close, true);
window.removeEventListener('resize', close);
};
}, [showCalendarPicker]);
if (!attachment || !calendarInvitationParsingEnabled) return null; if (!attachment || !calendarInvitationParsingEnabled) return null;
const detectedMethod = parsedEvent ? getInvitationMethod(parsedEvent, { email, attachment }) : 'unknown'; const detectedMethod = parsedEvent ? getInvitationMethod(parsedEvent, { email, attachment }) : 'unknown';
@@ -966,14 +980,23 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
)} )}
{supportsCalendar && !existingEvent && allowsImport && !isResponseOnly && !isCancellation && ( {supportsCalendar && !existingEvent && allowsImport && !isResponseOnly && !isCancellation && (
<div className="relative"> <>
<button <button
ref={pickerTriggerRef}
onClick={() => { onClick={() => {
if (calendars.length <= 1) { if (calendars.length <= 1) {
handleImport(); handleImport();
} else { return;
setShowCalendarPicker(!showCalendarPicker);
} }
if (showCalendarPicker) {
setShowCalendarPicker(false);
return;
}
if (pickerTriggerRef.current) {
const rect = pickerTriggerRef.current.getBoundingClientRect();
setPickerPosition({ top: rect.bottom + 4, left: rect.left });
}
setShowCalendarPicker(true);
}} }}
disabled={isProcessing} disabled={isProcessing}
className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground px-3 py-1.5 rounded-md border border-border hover:bg-muted transition-colors min-h-[36px] disabled:opacity-50" className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground px-3 py-1.5 rounded-md border border-border hover:bg-muted transition-colors min-h-[36px] disabled:opacity-50"
@@ -983,8 +1006,11 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
{calendars.length > 1 && <ChevronDown className="w-3 h-3" />} {calendars.length > 1 && <ChevronDown className="w-3 h-3" />}
</button> </button>
{showCalendarPicker && calendars.length > 1 && ( {showCalendarPicker && calendars.length > 1 && pickerPosition && typeof document !== 'undefined' && createPortal(
<div className="absolute left-0 top-full mt-1 w-52 bg-background rounded-lg shadow-lg border border-border z-10 py-1"> <div
className="fixed w-52 bg-background rounded-lg shadow-lg border border-border z-50 py-1"
style={{ top: pickerPosition.top, left: pickerPosition.left }}
>
<div className="px-3 py-1.5 text-xs font-medium text-muted-foreground"> <div className="px-3 py-1.5 text-xs font-medium text-muted-foreground">
{t('select_calendar')} {t('select_calendar')}
</div> </div>
@@ -1004,9 +1030,10 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
<span className="truncate text-foreground">{cal.name}</span> <span className="truncate text-foreground">{cal.name}</span>
</button> </button>
))} ))}
</div> </div>,
document.body,
)} )}
</div> </>
)} )}
{canApplyProposal && ( {canApplyProposal && (