Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions packages/joint-react/src/store/__tests__/graph-store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,4 +276,68 @@ describe('GraphStore', () => {
expect(store.paperStores.size).toBe(0);
});
});

describe('dev warning: resizing auto-sized elements', () => {
const addElement = (store: GraphStore, id: string) => {
const element = new shapes.standard.Rectangle({
id,
position: { x: 0, y: 0 },
size: { width: 100, height: 40 },
});
store.graph.addCell(element);
return element;
};

const measure = (store: GraphStore, id: string) => {
const node = document.createElement('div');
document.body.append(node);
store.setMeasuredNode({ id, node });
return () => node.remove();
};

it('warns when an auto-sized (observed) element is resized externally', () => {
const store = new GraphStore({});
const element = addElement(store, 'autoA');
const cleanup = measure(store, 'autoA');
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});

element.resize(200, 80);
expect(warn).toHaveBeenCalledWith(expect.stringContaining('auto-size mode'));

// dedup — a second resize of the same element does not warn again.
warn.mockClear();
element.resize(220, 90);
expect(warn).not.toHaveBeenCalled();

warn.mockRestore();
cleanup();
store.destroy(false);
});

it('does not warn for measurement-driven (autoSize) writes', () => {
const store = new GraphStore({});
const element = addElement(store, 'autoB');
const cleanup = measure(store, 'autoB');
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});

element.set('size', { width: 200, height: 80 }, { autoSize: true });
expect(warn).not.toHaveBeenCalled();

warn.mockRestore();
cleanup();
store.destroy(false);
});

it('does not warn for a non-auto-sized element (useModelGeometry)', () => {
const store = new GraphStore({});
const element = addElement(store, 'staticC'); // never registered with the observer
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});

element.resize(200, 80);
expect(warn).not.toHaveBeenCalled();

warn.mockRestore();
store.destroy(false);
});
});
});
20 changes: 19 additions & 1 deletion packages/joint-react/src/store/graph-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
import { simpleScheduler } from '../utils/scheduler';
import { cellInputToModel } from '../utils/normalize-cell-input';
import type { CellInput } from '../types/cell.types';
import { warnDuplicatePapers } from '../utils/dev-warnings';
import { warnDuplicatePapers, warnResizeOnAutoSizedElement } from '../utils/dev-warnings';

export const DEFAULT_CELL_NAMESPACE: Record<string, unknown> = {
...shapes,
Expand Down Expand Up @@ -105,6 +105,8 @@ export class GraphStore<

private observer: GraphStoreObserver;
private onIncrementalCellsChange?: OnIncrementalCellsChange<Element, Link>;
// dev-only `change:size` listener that warns about resizing auto-sized elements.
private warnAutoSizeResize?: (cell: dia.Cell, size: dia.Size, opt?: { autoSize?: boolean }) => void;

constructor(public readonly config: GraphStoreOptions<Element, Link>) {
const {
Expand Down Expand Up @@ -226,6 +228,19 @@ export class GraphStore<
// (e.g. stencil drag's cloneView).
this.graphProjection.syncFromGraph();
}

// dev only — warn when an auto-sized element (registered with the size
// observer because it renders without `useModelGeometry`) is resized by
// something other than the measurement pipeline. Such resizes are
// immediately overwritten by the measured content size.
if (process.env.NODE_ENV !== 'production') {
this.warnAutoSizeResize = (cell, _size, opt) => {
if (opt?.autoSize) return; // our own measurement write
if (!this.observer.has(cell.id)) return; // not auto-sized → resize is honored
warnResizeOnAutoSizedElement(cell.id);
};
this.graph.on('change:size', this.warnAutoSizeResize);
}
}

public setOnIncrementalCellsChange = (callback: OnIncrementalCellsChange<Element, Link>) => {
Expand Down Expand Up @@ -288,6 +303,9 @@ export class GraphStore<
this.graphProjection.destroy();
this.internalState.clean();
this.observer.clean();
if (this.warnAutoSizeResize) {
this.graph.off('change:size', this.warnAutoSizeResize);
}
if (!isGraphExternal) {
this.graph.clear();
}
Expand Down
23 changes: 23 additions & 0 deletions packages/joint-react/src/utils/dev-warnings.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { dia } from '@joint/core';
import { DEFAULT_PAPER_ID } from '../mvc/paper';
import type { CellId } from '../types/cell.types';

Expand Down Expand Up @@ -102,3 +103,25 @@ export function warnDuplicatePapers(paperId: string, existingPaperIds: Iterable<
}
}
}

/**
* Warns once per element when an auto-sized element (rendered without
* `useModelGeometry`, so its size is measured from the React content) is
* resized externally — by a tool such as FreeTransform / Halo, or a direct
* `cell.resize()`. The measurement pipeline overwrites that size, so the
* resize has no effect. Dev-only — tree-shaken in production.
* @param cellId - The id of the resized auto-sized element.
*/
export function warnResizeOnAutoSizedElement(cellId: dia.Cell.ID): void {
if (process.env.NODE_ENV === 'production') return;
const key = `resize-auto-sized:${cellId}`;
if (WARNED.has(key)) return;
WARNED.add(key);

console.warn(
`[auto-size] Element "${cellId}" was resized while rendering in auto-size mode ` +
'(no `useModelGeometry` on its HTMLBox/HTMLHost). The measured content size ' +
'overrides the resize, so it has no effect. Pass `useModelGeometry` to honor an ' +
'explicit size (e.g. from FreeTransform / Halo).'
);
}