mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-04 00:14:12 +00:00
fix: keep the event popover inside the window when a plugin section makes it taller
This commit is contained in:
@@ -56,6 +56,47 @@ describe('calendar-event-detail', () => {
|
||||
expect(framePropsOf(frame).event).toMatchObject({ id: 'e1', uid: 'uid-1', title: 'Planning', calendarId: 'cal-1' });
|
||||
});
|
||||
|
||||
it('moves the popover up when the plugin section makes it taller, so it stays in the window', () => {
|
||||
let resize: (() => void) | null = null;
|
||||
vi.stubGlobal('ResizeObserver', class {
|
||||
constructor(cb: () => void) { resize = cb; }
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
});
|
||||
Object.defineProperty(window, 'innerHeight', { value: 800, configurable: true });
|
||||
Object.defineProperty(window, 'innerWidth', { value: 1280, configurable: true });
|
||||
let height = 200;
|
||||
const spy = vi.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockImplementation(function (this: HTMLElement) {
|
||||
return this.getAttribute('role') === 'dialog' ? height : 0;
|
||||
});
|
||||
try {
|
||||
offerSlots('p1', ['calendar-event-detail']);
|
||||
// An event at 14:00 in the week view of a 1280x800 window.
|
||||
render(
|
||||
<EventDetailPopover
|
||||
event={existing}
|
||||
calendar={calendars[0]}
|
||||
anchorRect={new DOMRect(600, 520, 120, 60)}
|
||||
onEdit={vi.fn()}
|
||||
onDelete={vi.fn()}
|
||||
onDuplicate={vi.fn()}
|
||||
onClose={vi.fn()}
|
||||
onSaveNote={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
const popover = screen.getByRole('dialog');
|
||||
expect(popover.style.top).toBe('520px');
|
||||
|
||||
height = 420;
|
||||
act(() => resize?.());
|
||||
expect(popover.style.top).toBe(`${800 - 420 - 12}px`);
|
||||
} finally {
|
||||
spy.mockRestore();
|
||||
vi.unstubAllGlobals();
|
||||
}
|
||||
});
|
||||
|
||||
it('shows the event in the modal view, and not in the form', () => {
|
||||
offerSlots('p1', ['calendar-event-detail']);
|
||||
render(<EventModal event={existing} calendars={calendars} onSave={vi.fn()} onClose={vi.fn()} />);
|
||||
|
||||
@@ -54,13 +54,18 @@ const POPOVER_GAP = 8;
|
||||
const VIEWPORT_MARGIN = 12;
|
||||
const MAX_HEIGHT = 480;
|
||||
|
||||
/** As tall as MAX_HEIGHT, but never taller than the window. */
|
||||
function maxPopoverHeight(): number {
|
||||
return Math.max(0, Math.min(MAX_HEIGHT, window.innerHeight - 2 * VIEWPORT_MARGIN));
|
||||
}
|
||||
|
||||
function computePosition(
|
||||
anchorRect: DOMRect,
|
||||
popoverHeight: number
|
||||
): { top: number; left: number } {
|
||||
const vw = window.innerWidth;
|
||||
const vh = window.innerHeight;
|
||||
const clampedHeight = Math.min(popoverHeight, MAX_HEIGHT);
|
||||
const clampedHeight = Math.min(popoverHeight, maxPopoverHeight());
|
||||
|
||||
const clampTop = (top: number) =>
|
||||
Math.min(Math.max(VIEWPORT_MARGIN, top), vh - clampedHeight - VIEWPORT_MARGIN);
|
||||
@@ -226,6 +231,19 @@ export function EventDetailPopover({
|
||||
if (!ready) requestAnimationFrame(() => setReady(true));
|
||||
}, [anchorRect, noteExpanded, showDeleteConfirm, ready]);
|
||||
|
||||
// Content that grows after the popover was placed (a plugin section reports
|
||||
// its height a moment later) moves it again, so it stays in the window.
|
||||
useEffect(() => {
|
||||
const node = popoverRef.current;
|
||||
if (!node || isMobile || typeof ResizeObserver === "undefined") return;
|
||||
const observer = new ResizeObserver(() => {
|
||||
const next = computePosition(anchorRect, node.offsetHeight);
|
||||
setPosition((prev) => (prev && prev.top === next.top && prev.left === next.left ? prev : next));
|
||||
});
|
||||
observer.observe(node);
|
||||
return () => observer.disconnect();
|
||||
}, [anchorRect, isMobile]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") onClose();
|
||||
@@ -302,14 +320,14 @@ export function EventDetailPopover({
|
||||
"fixed z-[60] bg-background border border-border shadow-xl overflow-hidden transition-[opacity,transform] duration-150 ease-out",
|
||||
isMobile
|
||||
? "inset-0 rounded-none flex flex-col"
|
||||
: "rounded-lg"
|
||||
: "rounded-lg flex flex-col"
|
||||
)}
|
||||
style={isMobile ? {
|
||||
opacity: 1,
|
||||
transform: "none",
|
||||
} : {
|
||||
width: POPOVER_WIDTH,
|
||||
maxHeight: MAX_HEIGHT,
|
||||
maxHeight: maxPopoverHeight(),
|
||||
top: position?.top ?? -9999,
|
||||
left: position?.left ?? -9999,
|
||||
opacity: ready ? 1 : 0,
|
||||
@@ -413,7 +431,7 @@ export function EventDetailPopover({
|
||||
{/* Content */}
|
||||
<div className={cn(
|
||||
"px-4 py-2 space-y-2.5 overflow-y-auto",
|
||||
isMobile ? "flex-1" : ""
|
||||
isMobile ? "flex-1" : "min-h-0"
|
||||
)} style={isMobile ? undefined : { maxHeight: MAX_HEIGHT - 140 }}>
|
||||
{/* Date & Time */}
|
||||
<div className="flex items-start gap-2.5">
|
||||
|
||||
Reference in New Issue
Block a user