feat(builder): mobile-B touch editing -- selection toolbar, tap-to-add, swipe-dismiss
Phase B makes the Craft.js editor genuinely usable by touch on top of Phase A's responsive shell, gated entirely behind useIsMobile()/<=768px: - Extract useNodeActions(nodeId) out of ContextMenu.tsx (move/duplicate/ delete/select-parent), shared by the desktop right-click menu (behavior unchanged) and the new mobile MobileSelectionToolbar. - MobileSelectionToolbar: bottom-fixed selection toolbar (Move Up/Down, Duplicate, Select Parent, Edit Styles, two-tap Delete confirm), hidden while a sheet is open. - BlocksPanel: tap-to-add on mobile (insert after selection, close sheet, select + scroll the new node into view); desktop drag/double-click unchanged. - LayersPanel rows >=44px on mobile; HeadCodeModal portaled to document.body (same fix TemplateModal already had); BottomSheet gets swipe-to-dismiss and on-screen-keyboard clearance via a new useVisualViewportInsets hook. Also fixes two pre-existing bugs surfaced only by driving a real Craft.js document with Playwright touch input (masked by tests that mock @craftjs/core): regenerateTreeIds structuredClone'd a live node's whole data object, including the component function reference in data.type, throwing DataCloneError and silently breaking Duplicate/Paste for every node type; and an earlier useNodeActions draft cached canMoveUp/canMoveDown inside a useEditor collector closed over nodeId, which goes stale for one render whenever the selection changes without an unrelated store event. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -147,6 +147,37 @@ describe('regenerateTreeIds', () => {
|
||||
|
||||
expect(input.nodes['root-1'].data.props).toEqual({ alignment: 'left' });
|
||||
});
|
||||
|
||||
// Regression test (found via real-browser Playwright touch verification,
|
||||
// Phase B): a LIVE Craft.js node's `data.type` is the actual component
|
||||
// function reference (not the `{resolvedName}` string wrapper used only
|
||||
// in SERIALIZED state) -- `query.node(id).toNodeTree()` returns nodes
|
||||
// shaped exactly like this. The previous implementation ran
|
||||
// `structuredClone(oldNode.data)` on the WHOLE data object, which throws
|
||||
// `DataCloneError` the instant `data.type` is a function, silently
|
||||
// breaking Duplicate (ContextMenu.tsx / useNodeActions.ts) and Ctrl+V
|
||||
// Paste (useKeyboardShortcuts.ts) for every real node in the app --
|
||||
// masked by every other test in this suite using a plain string `type`
|
||||
// ('div'), and by unit tests elsewhere that mock `@craftjs/core` entirely
|
||||
// (so `toNodeTree()` never actually returns a function there).
|
||||
test('does not throw when data.type is a live component function reference', () => {
|
||||
const ButtonLikeComponent = (props: { text?: string }) => props.text;
|
||||
const input = makeTree();
|
||||
input.nodes['root-1'].data.type = ButtonLikeComponent as unknown as Node['data']['type'];
|
||||
input.nodes['root-1'].data.props = { text: 'Click Me' };
|
||||
|
||||
expect(() => regenerateTreeIds(input)).not.toThrow();
|
||||
|
||||
const output = regenerateTreeIds(input);
|
||||
const outputRoot = output.nodes[output.rootNodeId];
|
||||
// The component reference itself is preserved (shared, not cloned --
|
||||
// it's a stable reference, not mutable data).
|
||||
expect(outputRoot.data.type).toBe(ButtonLikeComponent);
|
||||
// Props are still genuinely deep-cloned (the actual bug fix's point).
|
||||
expect(outputRoot.data.props).toEqual({ text: 'Click Me' });
|
||||
(outputRoot.data.props as { text: string }).text = 'changed';
|
||||
expect(input.nodes['root-1'].data.props).toEqual({ text: 'Click Me' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('flattenTreeForCraft', () => {
|
||||
|
||||
Reference in New Issue
Block a user