diff --git a/craft/src/App.mobile-chrome.test.tsx b/craft/src/App.mobile-chrome.test.tsx new file mode 100644 index 0000000..459620f --- /dev/null +++ b/craft/src/App.mobile-chrome.test.tsx @@ -0,0 +1,40 @@ +import { describe, test, expect, vi, afterEach } from 'vitest'; +import React from 'react'; +import { createRoot, Root } from 'react-dom/client'; +import { act } from 'react-dom/test-utils'; +import { useMobileChrome } from './state/MobileChromeContext'; +import { ensureJsdomShims } from './test-utils/editorHarness'; + +/** + * The overlays (section library, command palette) mount NEXT TO the shell in + * App, so the mobile chrome state (open sheet) must be provided above both: + * otherwise the section library's closeSheet() after an insert hits the + * default no-op and the Blocks sheet stays open over the canvas. + */ +const seen: { shell?: ReturnType; overlays?: ReturnType } = {}; +vi.mock('./editor/EditorShell', () => ({ + EditorShell: () => { seen.shell = useMobileChrome(); return null; }, +})); +vi.mock('./editor/CanvasOverlays', () => ({ + CanvasOverlays: () => { seen.overlays = useMobileChrome(); return null; }, +})); + +import { App } from './App'; + +let root: Root; +let container: HTMLDivElement; +afterEach(() => { act(() => { root.unmount(); }); container.remove(); }); + +describe('App mobile chrome wiring', () => { + test('the shell and the overlays share one MobileChromeContext', () => { + ensureJsdomShims(); + container = document.createElement('div'); + document.body.appendChild(container); + act(() => { root = createRoot(container); root.render(); }); + act(() => seen.shell!.openSheet('blocks')); + expect(seen.shell!.activeSheet).toBe('blocks'); + expect(seen.overlays!.activeSheet).toBe('blocks'); + act(() => seen.overlays!.closeSheet()); + expect(seen.shell!.activeSheet).toBeNull(); + }); +}); diff --git a/craft/src/App.tsx b/craft/src/App.tsx index eb05005..ffa26bf 100644 --- a/craft/src/App.tsx +++ b/craft/src/App.tsx @@ -15,6 +15,7 @@ import { PageProvider } from './state/PageContext'; import { GlobalSectionsProvider } from './state/GlobalSectionsContext'; import { SitesmithProvider, useSitesmithModal } from './state/SitesmithContext'; import { SitesmithModal } from './panels/sitesmith/SitesmithModal'; +import { MobileChromeProvider } from './state/MobileChromeContext'; interface AppProps { whpConfig: WhpConfig | null; @@ -71,9 +72,13 @@ export const App: React.FC = ({ whpConfig }) => { - - - + {/* Shared by the shell and the overlays (e.g. the section + library closes the mobile Blocks sheet after an insert). */} + + + + + diff --git a/craft/src/editor/EditorShell.tsx b/craft/src/editor/EditorShell.tsx index 72d0314..b05706f 100644 --- a/craft/src/editor/EditorShell.tsx +++ b/craft/src/editor/EditorShell.tsx @@ -10,7 +10,6 @@ import { ContextMenu } from '../panels/context-menu/ContextMenu'; import { useContextMenu } from '../hooks/useContextMenu'; import { useKeyboardShortcuts } from '../hooks/useKeyboardShortcuts'; import { useIsMobile } from '../hooks/useIsMobile'; -import { MobileChromeProvider } from '../state/MobileChromeContext'; import { LayerFocusProvider } from '../panels/left/LayerFocusContext'; import { LoadGate } from './LoadGate'; import { DirtyTracker } from './DirtyTracker'; @@ -78,12 +77,10 @@ const EditorShellBody: React.FC = () => { }, [query, showMenu]); return ( - // Mobile-A2: shared sheet/modal-open state (see MobileChromeContext) -- - // provided around the whole shell so TopBar's Templates/Head Code modal - // state and MobilePanelBar's sheet state live in one place. Desktop - // doesn't render MobilePanelBar and TopBar's desktop branch behaves - // identically to before (same booleans, just sourced from context). - + // Mobile-A2: shared sheet/modal-open state (MobileChromeContext) is + // provided in App.tsx, around this shell AND CanvasOverlays, so overlays + // (section library, command palette) share it with TopBar/MobilePanelBar. + <>
@@ -112,6 +109,6 @@ const EditorShellBody: React.FC = () => { />
-
+ ); }; diff --git a/craft/src/panels/sections/SectionLibraryModal.test.tsx b/craft/src/panels/sections/SectionLibraryModal.test.tsx index bc76de8..8995867 100644 --- a/craft/src/panels/sections/SectionLibraryModal.test.tsx +++ b/craft/src/panels/sections/SectionLibraryModal.test.tsx @@ -7,6 +7,7 @@ import { SiteDesignProvider } from '../../state/SiteDesignContext'; import { __resetCommandsForTests, getCommand } from '../../editor/commands'; import { SectionLibraryModal, openSectionLibrary, closeSectionLibrary } from './SectionLibraryModal'; import { BlocksPanel } from '../left/BlocksPanel'; +import { MobileChromeProvider, useMobileChrome } from '../../state/MobileChromeContext'; let h: EditorHarness; let extrasApi: ReturnType; @@ -117,3 +118,28 @@ describe('SectionLibraryModal', () => { expect($$('.section-lib-card').map((c) => c.dataset.sectionId)).toContain('custom-loaded'); }); }); + +describe('SectionLibraryModal on mobile', () => { + let chrome: ReturnType; + const Chrome: React.FC = () => { chrome = useMobileChrome(); return null; }; + function mountMobile() { + h = renderEditorHarness({ + wrap: (tree) => ( + + {tree} + + ), + }); + h.mountChild(); + } + + test('inserting a section closes the Blocks sheet so the canvas is visible', () => { + mountMobile(); + h.act(() => chrome.openSheet('blocks')); + expect(chrome.activeSheet).toBe('blocks'); + h.act(() => openSectionLibrary()); + h.act(() => { ($('[data-section-id="hero-centered"] .section-lib-pick') as HTMLElement).click(); }); + expect(rootKids()).toHaveLength(1); + expect(chrome.activeSheet).toBeNull(); + }); +}); diff --git a/craft/src/panels/sections/SectionLibraryModal.tsx b/craft/src/panels/sections/SectionLibraryModal.tsx index af0cc1c..697d029 100644 --- a/craft/src/panels/sections/SectionLibraryModal.tsx +++ b/craft/src/panels/sections/SectionLibraryModal.tsx @@ -14,6 +14,7 @@ import { exportToHtml } from '../../utils/html-export'; import { masterZoneState, type GlobalMaster } from '../../utils/global-sections'; import { useGlobalSections } from '../../state/GlobalSectionsContext'; import { usePages } from '../../state/PageContext'; +import { useMobileChrome } from '../../state/MobileChromeContext'; // ---- open state: module-level so the command, the empty-state button and the // Blocks panel button can open the modal without a provider ---- @@ -79,6 +80,8 @@ export const SectionLibraryModal: React.FC = () => { const [globalTab, setGlobalTab] = useState(false); const globals = useGlobalSections(); const { isEditingGlobal } = usePages(); + // Mobile: an insert also closes the Blocks sheet so the new section is visible. + const { closeSheet } = useMobileChrome(); const [q, setQ] = useState(''); const [saveName, setSaveName] = useState(''); @@ -118,6 +121,7 @@ export const SectionLibraryModal: React.FC = () => { return; } closeSectionLibrary(); + closeSheet(); notify({ kind: 'success', message: `Added global section "${name}"` }); }; const onDeleteGlobal = (libraryId: string, name: string) => { @@ -132,6 +136,7 @@ export const SectionLibraryModal: React.FC = () => { try { const id = insertSection(query, actions, def, { afterSelection: true, select: true }); closeSectionLibrary(); + closeSheet(); notify({ kind: 'success', message: `Added "${def.name}"` }); requestAnimationFrame(() => requestAnimationFrame(() => { try { query.node(id).get()?.dom?.scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch { /* best effort */ } @@ -140,7 +145,7 @@ export const SectionLibraryModal: React.FC = () => { console.error('Failed to insert section:', e); notify({ kind: 'error', message: 'Could not add that section.' }); } - }, [query, actions, notify]); + }, [query, actions, notify, closeSheet]); const onSave = () => { const name = saveName.trim();