Publish Fast Tabler Docker image / build-and-push-fast (push) Failing after 1m6s
179 lines
4.3 KiB
React
179 lines
4.3 KiB
React
import { useEffect, useRef } from 'react';
|
|
|
|
/**
|
|
* Универсальный Modal компонент
|
|
* Простой и надёжный - работает как HistoryModal
|
|
*/
|
|
function Modal({
|
|
show,
|
|
onClose,
|
|
onOpen,
|
|
title,
|
|
children,
|
|
footer,
|
|
size = 'md',
|
|
backdrop = true,
|
|
keyboard = true,
|
|
scrollable = false,
|
|
centered = false,
|
|
className = ''
|
|
}) {
|
|
const modalRef = useRef(null);
|
|
|
|
// Управление классом modal-open на body
|
|
useEffect(() => {
|
|
if (show) {
|
|
document.body.classList.add('modal-open');
|
|
setTimeout(() => modalRef.current?.focus(), 100);
|
|
onOpen?.();
|
|
} else {
|
|
document.body.classList.remove('modal-open');
|
|
}
|
|
|
|
return () => {
|
|
document.body.classList.remove('modal-open');
|
|
};
|
|
}, [show, onOpen]);
|
|
|
|
// Обработка ESC
|
|
useEffect(() => {
|
|
if (keyboard && show) {
|
|
const handleEsc = (e) => {
|
|
if (e.key === 'Escape') {
|
|
onClose?.();
|
|
}
|
|
};
|
|
document.addEventListener('keydown', handleEsc);
|
|
return () => document.removeEventListener('keydown', handleEsc);
|
|
}
|
|
}, [keyboard, show, onClose]);
|
|
|
|
if (!show) return null;
|
|
|
|
const handleBackdropClick = (e) => {
|
|
if (backdrop && e.target === e.currentTarget) {
|
|
onClose?.();
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{/* Modal Backdrop */}
|
|
<div className="modal-backdrop show" onClick={handleBackdropClick} />
|
|
|
|
{/* Modal */}
|
|
<div
|
|
className={`modal show d-block ${className}`}
|
|
role="dialog"
|
|
aria-modal="true"
|
|
tabIndex={-1}
|
|
onKeyDown={(e) => { if (e.key === 'Escape') onClose?.(); }}
|
|
>
|
|
<div
|
|
className={`modal-dialog${size !== 'md' ? ` modal-${size}` : ''}${scrollable ? ' modal-dialog-scrollable' : ''}${centered ? ' modal-dialog-centered' : ''}`}
|
|
role="document"
|
|
>
|
|
<div
|
|
className="modal-content"
|
|
ref={modalRef}
|
|
tabIndex={-1}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Tab') {
|
|
const c = e.currentTarget;
|
|
const focusable = c.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
|
if (!focusable || focusable.length === 0) return;
|
|
const first = focusable[0];
|
|
const last = focusable[focusable.length - 1];
|
|
if (e.shiftKey && document.activeElement === first) {
|
|
e.preventDefault();
|
|
last.focus();
|
|
} else if (!e.shiftKey && document.activeElement === last) {
|
|
e.preventDefault();
|
|
first.focus();
|
|
}
|
|
}
|
|
}}
|
|
>
|
|
{title && (
|
|
<div className="modal-header">
|
|
<h5 className="modal-title">{title}</h5>
|
|
<button
|
|
type="button"
|
|
className="btn-close"
|
|
onClick={onClose}
|
|
aria-label="Закрыть"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<div className="modal-body">
|
|
{children}
|
|
</div>
|
|
|
|
{footer && (
|
|
<div className="modal-footer">
|
|
{footer}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default Modal;
|
|
|
|
/**
|
|
* Готовые пресеты для быстрого использования
|
|
*/
|
|
|
|
export function SmallModal({ show, onClose, title, children, footer, ...props }) {
|
|
return (
|
|
<Modal
|
|
show={show}
|
|
onClose={onClose}
|
|
title={title}
|
|
size="sm"
|
|
centered
|
|
footer={footer}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
export function LargeModal({ show, onClose, title, children, footer, scrollable = true, ...props }) {
|
|
return (
|
|
<Modal
|
|
show={show}
|
|
onClose={onClose}
|
|
title={title}
|
|
size="lg"
|
|
centered
|
|
scrollable={scrollable}
|
|
footer={footer}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
export function FullscreenModal({ show, onClose, title, children, footer, ...props }) {
|
|
return (
|
|
<Modal
|
|
show={show}
|
|
onClose={onClose}
|
|
title={title}
|
|
size="xl"
|
|
scrollable
|
|
footer={footer}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</Modal>
|
|
);
|
|
}
|