fix(sections): inserting from the section library closes the mobile Blocks sheet

The section library now calls closeSheet() after inserting a section (or a
global section). It mounts in CanvasOverlays, beside EditorShell, so it could
not reach the MobileChromeProvider inside the shell; the provider moves up to
App around the shell and the overlays so both share one sheet state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shadowdaoandClaude Opus 5.5 committed 2026-10-07 13:41:22 -07:00
1 parent 472e5e4d81
commit eb401cd979
5 files changed
+85 -12

No files matched your search

+40
View File
@@ -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<typeof useMobileChrome>; overlays?: ReturnType<typeof useMobileChrome> } = {};
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(<App whpConfig={null as any} />); });
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();
});
});
+8 -3
View File
@@ -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<AppProps> = ({ whpConfig }) => {
<PageProvider>
<GlobalSectionsProvider>
<SitesmithProvider>
<EditorShell />
<CanvasOverlays />
<SitesmithModalMount />
{/* Shared by the shell and the overlays (e.g. the section
library closes the mobile Blocks sheet after an insert). */}
<MobileChromeProvider>
<EditorShell />
<CanvasOverlays />
<SitesmithModalMount />
</MobileChromeProvider>
</SitesmithProvider>
</GlobalSectionsProvider>
</PageProvider>
+5 -8
View File
@@ -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).
<MobileChromeProvider>
// 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.
<>
<LayerFocusProvider>
<div className="editor-app">
<LoadGate />
@@ -112,6 +109,6 @@ const EditorShellBody: React.FC = () => {
/>
</div>
</LayerFocusProvider>
</MobileChromeProvider>
</>
);
};
@@ -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<typeof useProjectExtras>;
@@ -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<typeof useMobileChrome>;
const Chrome: React.FC = () => { chrome = useMobileChrome(); return null; };
function mountMobile() {
h = renderEditorHarness({
wrap: (tree) => (
<SaveGuardProvider><ProjectExtrasProvider><SiteDesignProvider><MobileChromeProvider>
{tree}<Grab /><Chrome />
</MobileChromeProvider></SiteDesignProvider></ProjectExtrasProvider></SaveGuardProvider>
),
});
h.mountChild(<SectionLibraryModal />);
}
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();
});
});
@@ -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();