- 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>
68 lines
1.6 KiB
TypeScript
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>
|
|
);
|
|
}
|