53 lines
2.1 KiB
TypeScript
53 lines
2.1 KiB
TypeScript
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 };
|
|
}
|