Show component-indicator selection badge in the canvas
.component-indicator existed in editor.css but was never rendered anywhere. Add RenderNode.tsx as a Craft.js <Editor onRender> override and wire it in App.tsx: for the currently-selected node (excluding ROOT) it portals a floating badge showing the node's displayName plus a "select parent" chevron wired to actions.selectNode(parentId). Every other node's render passes through untouched (a Fragment, no extra DOM), and the badge portals to document.body positioned via getBoundingClientRect rather than wrapping nodes in extra DOM, so it can't perturb canvas layout and never appears in toHtml export. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import { describe, test, expect, vi, beforeEach } from 'vitest';
|
||||
import React from 'react';
|
||||
import { createRoot, Root } from 'react-dom/client';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
/* Same DOM-harness pattern as Footer.editguard.test.tsx: mock @craftjs/core
|
||||
so RenderNode (the <Editor onRender> override) can be driven without a
|
||||
real Editor tree. document.body doubles as the portal target, same as
|
||||
the component itself uses. */
|
||||
let mockNode: {
|
||||
id: string;
|
||||
selected: boolean;
|
||||
dom: HTMLElement | null;
|
||||
displayName: string;
|
||||
parent: string | null;
|
||||
};
|
||||
const selectNodeSpy = vi.fn();
|
||||
|
||||
vi.mock('@craftjs/core', () => ({
|
||||
useEditor: () => ({ actions: { selectNode: selectNodeSpy } }),
|
||||
useNode: (collect?: (node: any) => any) => {
|
||||
const node = {
|
||||
events: { selected: mockNode.selected },
|
||||
dom: mockNode.dom,
|
||||
data: { custom: {}, displayName: mockNode.displayName, parent: mockNode.parent },
|
||||
};
|
||||
return { id: mockNode.id, ...(collect ? collect(node) : {}) };
|
||||
},
|
||||
}));
|
||||
|
||||
import { RenderNode } from './RenderNode';
|
||||
|
||||
let container: HTMLDivElement;
|
||||
let root: Root;
|
||||
let nodeDom: HTMLElement;
|
||||
|
||||
function render(ui: React.ReactElement) {
|
||||
container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
act(() => {
|
||||
root = createRoot(container);
|
||||
root.render(ui);
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
nodeDom = document.createElement('div');
|
||||
document.body.appendChild(nodeDom);
|
||||
mockNode = { id: 'node-1', selected: false, dom: nodeDom, displayName: 'Heading', parent: 'ROOT' };
|
||||
selectNodeSpy.mockClear();
|
||||
});
|
||||
|
||||
const rendered = <span data-testid="inner">hello</span>;
|
||||
|
||||
describe('RenderNode (Editor onRender override)', () => {
|
||||
test('passes render through untouched when not selected', () => {
|
||||
render(<RenderNode render={rendered} />);
|
||||
expect(container.querySelector('[data-testid="inner"]')).not.toBeNull();
|
||||
expect(document.querySelector('.component-indicator')).toBeNull();
|
||||
container.remove();
|
||||
nodeDom.remove();
|
||||
});
|
||||
|
||||
test('shows the badge with the displayName when selected', () => {
|
||||
mockNode.selected = true;
|
||||
render(<RenderNode render={rendered} />);
|
||||
const badge = document.querySelector('.component-indicator');
|
||||
expect(badge).not.toBeNull();
|
||||
expect(badge?.textContent).toContain('Heading');
|
||||
container.remove();
|
||||
nodeDom.remove();
|
||||
document.querySelector('.component-indicator')?.remove();
|
||||
});
|
||||
|
||||
test('never shows a badge for ROOT even if "selected"', () => {
|
||||
mockNode.selected = true;
|
||||
mockNode.id = 'ROOT';
|
||||
render(<RenderNode render={rendered} />);
|
||||
expect(document.querySelector('.component-indicator')).toBeNull();
|
||||
container.remove();
|
||||
nodeDom.remove();
|
||||
});
|
||||
|
||||
test('chevron click selects the parent node', () => {
|
||||
mockNode.selected = true;
|
||||
mockNode.parent = 'parent-42';
|
||||
render(<RenderNode render={rendered} />);
|
||||
const chevron = document.querySelector('.component-indicator-parent-btn') as HTMLElement;
|
||||
expect(chevron).not.toBeNull();
|
||||
act(() => {
|
||||
chevron.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
|
||||
});
|
||||
expect(selectNodeSpy).toHaveBeenCalledWith('parent-42');
|
||||
container.remove();
|
||||
nodeDom.remove();
|
||||
document.querySelector('.component-indicator')?.remove();
|
||||
});
|
||||
|
||||
test('no chevron when there is no parent', () => {
|
||||
mockNode.selected = true;
|
||||
mockNode.parent = null;
|
||||
render(<RenderNode render={rendered} />);
|
||||
expect(document.querySelector('.component-indicator-parent-btn')).toBeNull();
|
||||
container.remove();
|
||||
nodeDom.remove();
|
||||
document.querySelector('.component-indicator')?.remove();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user