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:
2026-07-12 15:22:05 -07:00
co-authored by Claude Opus 4.8
parent 8f51b5144a
commit b88b242b6a
5 changed files with 216 additions and 73 deletions
+4 -27
View File
@@ -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,