2a8a26687b
- Force font-size:16px !important on Styles-sheet/topbar/Sitesmith inputs inside the mobile media query so inline 12px/14px styles stop triggering iOS zoom-on-focus. - Lift sheet-open + Templates/Head Code modal-open state out of private useState into a shared MobileChromeContext (EditorShell), so Phase B can open/close sheets from outside MobilePanelBar. - Add an explicit z-index layer scale, portal TemplateModal to document.body (was trapped under the tab bar inside .topbar's stacking context), align Sitesmith to the same --z-modal layer, and make opening a sheet close any open modal. Also fix modal backdrops swallowing tab bar taps (mirrors the sheet backdrop's existing tab-bar cutout). - Drop BottomSheet's incorrect aria-modal; mobile-aware AssetsPanel empty state copy. - Tests: useIsMobile (matchMedia mock incl. legacy fallback + cleanup), MobileChromeContext invariants (one sheet open, sheet closes modals), MobilePanelBar wiring. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
99 lines
3.4 KiB
TypeScript
99 lines
3.4 KiB
TypeScript
import React, { useEffect } from 'react';
|
|
|
|
export interface ModalProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
title?: string;
|
|
children: React.ReactNode;
|
|
width?: number | string;
|
|
/** Override the backdrop's own inline styles (e.g. a different overlay
|
|
* opacity/z-index). Merged on top of the shared defaults. */
|
|
backdropStyle?: React.CSSProperties;
|
|
/** Escape key closes the modal. Default true — set false to preserve a
|
|
* modal that never handled Escape before this component existed. */
|
|
closeOnEscape?: boolean;
|
|
/** Clicking the backdrop (outside the modal content) closes the modal.
|
|
* Default true — set false to preserve a modal that never handled
|
|
* backdrop clicks before this component existed. */
|
|
closeOnBackdropClick?: boolean;
|
|
/** Extra attributes spread onto the backdrop div (e.g. role="dialog",
|
|
* aria-modal) for a modal that previously set those directly. */
|
|
backdropProps?: React.HTMLAttributes<HTMLDivElement>;
|
|
}
|
|
|
|
const defaultBackdropStyle: 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',
|
|
/* z-index comes from the `.modal-backdrop` class (--z-modal in the
|
|
shared layer scale, editor.css) rather than an inline value, so every
|
|
Modal consumer -- including ones that pass a custom `backdropStyle`
|
|
without a zIndex, like SitesmithModal -- lands on the same layer. */
|
|
};
|
|
|
|
/* ---------- Shared modal chrome ----------
|
|
Backdrop + centered panel wrapper + Escape-to-close + scroll-lock, extracted
|
|
from TemplateModal / HeadCodeModal / SitesmithModal (task E4.3). Each
|
|
modal's own content — including its own header, close button, and panel
|
|
styling (background, radius, shadow, dimensions) — stays in that modal's
|
|
own file and is passed in as `children`, since those visuals differ across
|
|
the three. `closeOnEscape` / `closeOnBackdropClick` default to true (the
|
|
behavior TemplateModal and HeadCodeModal already had); SitesmithModal had
|
|
neither before this extraction, so it opts out of both to keep its exact
|
|
prior behavior. */
|
|
export const Modal: React.FC<ModalProps> = ({
|
|
open,
|
|
onClose,
|
|
title,
|
|
children,
|
|
width,
|
|
backdropStyle,
|
|
closeOnEscape = true,
|
|
closeOnBackdropClick = true,
|
|
backdropProps,
|
|
}) => {
|
|
useEffect(() => {
|
|
if (!open || !closeOnEscape) return;
|
|
const handler = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape') onClose();
|
|
};
|
|
window.addEventListener('keydown', handler);
|
|
return () => window.removeEventListener('keydown', handler);
|
|
}, [open, closeOnEscape, onClose]);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
const prevOverflow = document.body.style.overflow;
|
|
document.body.style.overflow = 'hidden';
|
|
return () => {
|
|
document.body.style.overflow = prevOverflow;
|
|
};
|
|
}, [open]);
|
|
|
|
if (!open) return null;
|
|
|
|
return (
|
|
<div
|
|
className="modal-backdrop"
|
|
{...backdropProps}
|
|
style={backdropStyle ? { ...defaultBackdropStyle, ...backdropStyle } : defaultBackdropStyle}
|
|
onClick={(e) => {
|
|
if (closeOnBackdropClick && e.target === e.currentTarget) onClose();
|
|
}}
|
|
>
|
|
<div style={width !== undefined ? { width } : undefined}>
|
|
{title !== undefined && (
|
|
<div style={{ fontSize: 15, fontWeight: 600, marginBottom: 8 }}>{title}</div>
|
|
)}
|
|
{children}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|