UMBRA/frontend/src/components/shared/MobileDetailOverlay.tsx
Kyle Pope 4e91944956 Fix code review findings: sort dropdown, overlay ref, CalendarPage
- C-01: Simplify EntityTable sort dropdown to toggle-based (select
  column, re-select to flip direction), add aria-label
- W-01: Convert CalendarPage mobile overlay to MobileDetailOverlay
- W-02: Use ref for onClose in MobileDetailOverlay to prevent
  listener churn from inline arrow functions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 03:46:40 +08:00

68 lines
1.6 KiB
TypeScript

import { useEffect, useRef } from 'react';
import { cn } from '@/lib/utils';
interface MobileDetailOverlayProps {
open: boolean;
onClose: () => void;
children: React.ReactNode;
className?: string;
}
/**
* Full-screen overlay for mobile detail panels.
* - Backdrop click closes the overlay
* - Escape key closes the overlay
* - Body scroll is locked while open
*/
export default function MobileDetailOverlay({
open,
onClose,
children,
className,
}: MobileDetailOverlayProps) {
// Stable ref to avoid re-registering listener on every render
const onCloseRef = useRef(onClose);
onCloseRef.current = onClose;
// Escape key handler
useEffect(() => {
if (!open) return;
const handler = (e: KeyboardEvent) => {
if (e.key === 'Escape') onCloseRef.current();
};
document.addEventListener('keydown', handler);
return () => document.removeEventListener('keydown', handler);
}, [open]);
// Body scroll lock
useEffect(() => {
if (!open) return;
const previous = document.body.style.overflow;
document.body.style.overflow = 'hidden';
return () => {
document.body.style.overflow = previous;
};
}, [open]);
if (!open) return null;
return (
<div
role="dialog"
aria-modal="true"
className="fixed inset-0 z-50 bg-background/80 backdrop-blur-sm animate-fade-in"
onClick={onClose}
>
<div
className={cn(
'absolute right-0 top-0 h-full w-full sm:max-w-md bg-card border-l border-border shadow-xl overflow-y-auto',
className,
)}
onClick={(e) => e.stopPropagation()}
>
{children}
</div>
</div>
);
}