feat: implement useOverlayDismiss hook for consistent modal dismissal behavior

This commit is contained in:
hamed
2026-08-10 11:27:16 +03:30
parent 8a18457751
commit 3365a0427e
12 changed files with 227 additions and 36 deletions
+52
View File
@@ -0,0 +1,52 @@
import { useCallback, useEffect, useRef } from 'react';
import type { MouseEvent as ReactMouseEvent } from 'react';
/**
* لایه‌هایی که خودشان با Esc بسته می‌شوند و مودال نباید جایشان بسته شود:
* منوی باز react-select و تقویم شمسی.
*/
const OPEN_LAYER_SELECTOR = '.cp-select__menu, .cp-calendar';
/**
* رفتار بستنِ یک مودال: کلیک روی پس‌زمینه و کلید Esc.
*
* قبلاً پس‌زمینه یک `onClick={onClose}` ساده داشت و هر کلیکی که *روی آن تمام می‌شد*
* مودال را می‌بست — انتخاب متن داخل فرم و رها کردن موس بیرون، یا کلیک روی گزینه‌ای
* که همان لحظه unmount می‌شد. بستنِ ناخواستهٔ فرمِ نیمه‌پرشده آزاردهنده‌ترین باگ
* پنل بود، پس شرط سخت‌تر شد: هم فشردن و هم رها کردن باید روی خودِ پس‌زمینه باشد.
*
* ```tsx
* const dismiss = useOverlayDismiss(onClose);
* <div className="overlay" {...dismiss}> … </div>
* ```
*/
export function useOverlayDismiss(onClose: () => void, enabled = true) {
const pressedOnOverlay = useRef(false);
useEffect(() => {
if (!enabled) return;
const onKey = (e: KeyboardEvent) => {
if (e.key !== 'Escape') return;
// درا‌پ‌داون یا تقویمِ باز، خودش با Esc بسته می‌شود؛ مودال باید بماند.
if (document.querySelector(OPEN_LAYER_SELECTOR)) return;
onClose();
};
document.addEventListener('keydown', onKey);
return () => document.removeEventListener('keydown', onKey);
}, [enabled, onClose]);
const onMouseDown = useCallback((e: ReactMouseEvent<HTMLElement>) => {
pressedOnOverlay.current = e.target === e.currentTarget;
}, []);
const onClick = useCallback((e: ReactMouseEvent<HTMLElement>) => {
const started = pressedOnOverlay.current;
pressedOnOverlay.current = false;
if (started && e.target === e.currentTarget) onClose();
}, [onClose]);
return { onMouseDown, onClick };
}