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:
1 parent
472e5e4d81
commit
eb401cd979
5 files changed
+85
-12
No files matched your search
@@ -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
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user