feat: implement useOverlayDismiss hook for consistent modal dismissal behavior
This commit is contained in:
@@ -4,6 +4,7 @@ import { XMarkIcon } from '@heroicons/react/24/outline';
|
||||
import SearchableSelect from './ui/SearchableSelect';
|
||||
import { useBankAccounts, usePosDevices } from '../hooks/usePaymentMethods';
|
||||
import { formatRial, formatNumber, tomanToRial, digitsOnly } from '../lib/utils';
|
||||
import { useOverlayDismiss } from '../hooks/useOverlayDismiss';
|
||||
|
||||
export type WalletMode = 'charge' | 'withdraw';
|
||||
|
||||
@@ -59,12 +60,7 @@ export default function WalletTransactionModal({ open, balanceRials, submitting,
|
||||
if (open) { setMode('charge'); setAmountToman(0); setMethodValue('cash'); setDescription(''); }
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
|
||||
document.addEventListener('keydown', onKey);
|
||||
return () => document.removeEventListener('keydown', onKey);
|
||||
}, [open, onClose]);
|
||||
const dismiss = useOverlayDismiss(onClose, open);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
@@ -103,8 +99,8 @@ export default function WalletTransactionModal({ open, balanceRials, submitting,
|
||||
};
|
||||
|
||||
return createPortal(
|
||||
<div className="overlay" onClick={onClose}>
|
||||
<div className="modal" style={{ maxWidth: 560 }} onClick={(e) => e.stopPropagation()}>
|
||||
<div className="overlay" {...dismiss}>
|
||||
<div className="modal" style={{ maxWidth: 560 }}>
|
||||
<div className="modal-head">
|
||||
<h2>{isWithdraw ? 'برداشت از کیف پول' : 'شارژ کیف پول'}</h2>
|
||||
<button type="button" className="mini-btn" onClick={onClose}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { XMarkIcon, ExclamationTriangleIcon } from '@heroicons/react/24/outline';
|
||||
import Portal from './Portal';
|
||||
import { useOverlayDismiss } from '../../hooks/useOverlayDismiss';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
@@ -28,12 +29,14 @@ export default function ConfirmDialog({
|
||||
onCancel,
|
||||
children,
|
||||
}: Props) {
|
||||
const dismiss = useOverlayDismiss(onCancel, open);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<div className="overlay" onClick={onCancel}>
|
||||
<div className="modal" style={{ maxWidth: 420 }} onClick={(e) => e.stopPropagation()}>
|
||||
<div className="overlay" {...dismiss}>
|
||||
<div className="modal" style={{ maxWidth: 420 }}>
|
||||
<div className="modal-head">
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<span style={{
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { ApiResponse } from '../../lib/api';
|
||||
import MobileInput from './MobileInput';
|
||||
import Portal from './Portal';
|
||||
import { iranMobileSchema } from '../../lib/utils';
|
||||
import { useOverlayDismiss } from '../../hooks/useOverlayDismiss';
|
||||
|
||||
const inviteSchema = z.object({
|
||||
mobile: iranMobileSchema,
|
||||
@@ -39,10 +40,12 @@ export default function InviteDoctorModal({ clinicUuid, onClose, onInvited }: Pr
|
||||
onError: (e: Error) => toast.error(e.message),
|
||||
});
|
||||
|
||||
const dismiss = useOverlayDismiss(onClose);
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<div className="overlay" onClick={onClose}>
|
||||
<div className="modal" style={{ maxWidth: 420 }} onClick={e => e.stopPropagation()}>
|
||||
<div className="overlay" {...dismiss}>
|
||||
<div className="modal" style={{ maxWidth: 420 }}>
|
||||
<div className="modal-head">
|
||||
<b>دعوت پزشک به کلینیک</b>
|
||||
<button className="mini-btn" onClick={onClose}><XMarkIcon style={{ width: 16, height: 16 }} /></button>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { XMarkIcon } from '@heroicons/react/24/outline';
|
||||
import { useOverlayDismiss } from '../../hooks/useOverlayDismiss';
|
||||
|
||||
type ModalSize = 'sm' | 'md' | 'lg' | 'xl';
|
||||
|
||||
@@ -21,22 +22,13 @@ interface Props {
|
||||
}
|
||||
|
||||
export default function Modal({ open, title, size = 'md', onClose, children, footer }: Props) {
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
|
||||
document.addEventListener('keydown', onKey);
|
||||
return () => document.removeEventListener('keydown', onKey);
|
||||
}, [open, onClose]);
|
||||
const dismiss = useOverlayDismiss(onClose, open);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return createPortal(
|
||||
<div className="overlay" onClick={onClose}>
|
||||
<div
|
||||
className="modal"
|
||||
style={{ maxWidth: sizeMap[size] }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="overlay" {...dismiss}>
|
||||
<div className="modal" style={{ maxWidth: sizeMap[size] }}>
|
||||
<div className="modal-head">
|
||||
<h2>{title}</h2>
|
||||
<button type="button" className="mini-btn" onClick={onClose} aria-label="بستن">
|
||||
|
||||
@@ -120,7 +120,7 @@ export default function PersianCalendar({ value, onChange, onClose, enableYearPi
|
||||
while (cells.length % 7 !== 0) cells.push(null);
|
||||
|
||||
return (
|
||||
<div ref={ref} style={{
|
||||
<div ref={ref} className="cp-calendar" style={{
|
||||
position: 'absolute', top: '100%', right: 0, zIndex: 999, marginTop: 4,
|
||||
background: 'var(--surface)', border: '1px solid var(--border)',
|
||||
borderRadius: 'var(--r)', boxShadow: 'var(--shadow-lg)',
|
||||
|
||||
@@ -123,6 +123,10 @@ export default function SearchableSelect({
|
||||
|
||||
return (
|
||||
<Select<SelectOption>
|
||||
// پیشوند پایدار برای کلاسها: کلاسهای emotion در build تولیدی برچسب ندارند و
|
||||
// «منوی باز است یا نه» با آنها قابل تشخیص نیست. useOverlayDismiss به همین
|
||||
// نشانه نگاه میکند تا Esc اول منو را ببندد نه مودال را.
|
||||
classNamePrefix="cp-select"
|
||||
options={options}
|
||||
value={selected}
|
||||
onChange={(opt) => onChange?.(opt ? opt.value : null)}
|
||||
|
||||
Reference in New Issue
Block a user