refactor(builder): extract shared Modal component for the three editor modals
New src/ui/Modal.tsx ({ open, onClose, title?, children, width?,
closeOnEscape?, closeOnBackdropClick?, backdropStyle?, backdropProps? })
owns the backdrop, Escape-to-close, backdrop-click-to-close, and a new
body-scroll-lock while open. Adopted by TemplateModal, HeadCodeModal, and
SitesmithModal; each keeps its own panel styling/header/footer as children
since those differ per modal.
- TemplateModal: Escape/backdrop-click still close the confirm-template
sub-dialog first via a wrapped onClose passed to Modal; the header's X
button keeps using the raw onClose prop (always fully closes), matching
prior asymmetric behavior.
- HeadCodeModal: straightforward adoption, no prior custom close logic.
- SitesmithModal: previously had no `open` prop, no Escape-to-close, and no
backdrop-click-to-close. Preserved via open (always mounted-open by its
parent already), closeOnEscape={false}, closeOnBackdropClick={false}; role
and aria-modal are passed through via backdropProps to keep them on the
same backdrop element as before.
Body scroll-lock while a modal is open is a small new addition (requested by
the task) applied uniformly; it has no visible effect since each modal's
opaque fixed-position backdrop already fully covers the viewport.
Added a light Modal.test.tsx (renders children, Escape/backdrop-click close
behavior, scroll-lock) using the existing react-dom/client + act harness.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { useSiteDesign } from '../../state/SiteDesignContext';
|
||||
import { Modal } from '../../ui/Modal';
|
||||
|
||||
interface HeadCodeModalProps {
|
||||
open: boolean;
|
||||
@@ -9,19 +10,8 @@ interface HeadCodeModalProps {
|
||||
export const HeadCodeModal: React.FC<HeadCodeModalProps> = ({ open, onClose }) => {
|
||||
const { design, updateDesign } = useSiteDesign();
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
};
|
||||
window.addEventListener('keydown', handler);
|
||||
return () => window.removeEventListener('keydown', handler);
|
||||
}, [open, onClose]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div style={backdropStyle} onClick={onClose}>
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<div style={modalStyle} onClick={(e) => e.stopPropagation()}>
|
||||
{/* Header */}
|
||||
<div style={modalHeaderStyle}>
|
||||
@@ -92,25 +82,12 @@ export const HeadCodeModal: React.FC<HeadCodeModalProps> = ({ open, onClose }) =
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
/* ---------- Styles ---------- */
|
||||
|
||||
const backdropStyle: React.CSSProperties = {
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.65)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 10000,
|
||||
};
|
||||
|
||||
const modalStyle: React.CSSProperties = {
|
||||
width: '90vw',
|
||||
maxWidth: 700,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from '../../templates';
|
||||
import { componentResolver } from '../../components/resolver';
|
||||
import { clickableProps } from '../../utils/a11y';
|
||||
import { Modal } from '../../ui/Modal';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
@@ -60,18 +61,15 @@ export const TemplateModal: React.FC<TemplateModalProps> = ({ open, onClose }) =
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Close on Escape key
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
if (confirmTemplate) setConfirmTemplate(null);
|
||||
else onClose();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handler);
|
||||
return () => document.removeEventListener('keydown', handler);
|
||||
}, [open, confirmTemplate, onClose]);
|
||||
// Escape / backdrop-click close the confirmation dialog first if it's open,
|
||||
// otherwise close the modal. Passed to <Modal> below, which handles the
|
||||
// Escape listener and the backdrop-click detection itself; the header's own
|
||||
// close button (X) below still uses the raw `onClose` prop directly, so it
|
||||
// always fully closes the modal even mid-confirmation (unchanged behavior).
|
||||
const handleModalClose = useCallback(() => {
|
||||
if (confirmTemplate) setConfirmTemplate(null);
|
||||
else onClose();
|
||||
}, [confirmTemplate, onClose]);
|
||||
|
||||
// Filter templates by category
|
||||
const filtered = useMemo(() => {
|
||||
@@ -236,24 +234,8 @@ export const TemplateModal: React.FC<TemplateModalProps> = ({ open, onClose }) =
|
||||
}
|
||||
}, [confirmTemplate, applyDesign, pages, addPage, switchPage, deletePage, updateDesign, addTemplateComponents, clearCanvas, applyHeaderFooter, wait, onClose]);
|
||||
|
||||
// Close on backdrop click
|
||||
const handleBackdropClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
if (confirmTemplate) {
|
||||
setConfirmTemplate(null);
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
},
|
||||
[confirmTemplate, onClose],
|
||||
);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div style={backdropStyle} onClick={handleBackdropClick}>
|
||||
<Modal open={open} onClose={handleModalClose}>
|
||||
<div style={modalStyle}>
|
||||
{/* Header */}
|
||||
<div style={modalHeaderStyle}>
|
||||
@@ -377,7 +359,7 @@ export const TemplateModal: React.FC<TemplateModalProps> = ({ open, onClose }) =
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -473,19 +455,6 @@ const TemplateCard: React.FC<{
|
||||
// Styles
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const backdropStyle: React.CSSProperties = {
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.65)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 10000,
|
||||
};
|
||||
|
||||
const modalStyle: React.CSSProperties = {
|
||||
width: '90vw',
|
||||
maxWidth: 900,
|
||||
|
||||
Reference in New Issue
Block a user