From f5a4b7000fa7c8727b09dd263989a67ed28a39a3 Mon Sep 17 00:00:00 2001 From: Roman Bruckner Date: Mon, 22 Jun 2026 23:27:23 +0200 Subject: [PATCH] feat(joint-react): dev warning when resizing an auto-sized element MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An element rendered without `useModelGeometry` is auto-sized — its HTMLBox/ HTMLHost measures the React content and writes the size back to the model. A tool resize (FreeTransform / Halo) or a direct `cell.resize()` is then immediately overwritten by the measurement pipeline, so it silently has no effect. GraphStore now attaches a dev-only `change:size` listener: when a size change carries no `autoSize` flag (i.e. it is not a measurement write) and the element is currently registered with the size observer (auto-size mode), it warns once per element via `warnResizeOnAutoSizedElement`. Tree-shaken in production; detached in `clean()`. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/store/__tests__/graph-store.test.ts | 64 +++++++++++++++++++ packages/joint-react/src/store/graph-store.ts | 20 +++++- .../joint-react/src/utils/dev-warnings.ts | 23 +++++++ 3 files changed, 106 insertions(+), 1 deletion(-) diff --git a/packages/joint-react/src/store/__tests__/graph-store.test.ts b/packages/joint-react/src/store/__tests__/graph-store.test.ts index 919627b48c..09324d8cb7 100644 --- a/packages/joint-react/src/store/__tests__/graph-store.test.ts +++ b/packages/joint-react/src/store/__tests__/graph-store.test.ts @@ -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); + }); + }); }); diff --git a/packages/joint-react/src/store/graph-store.ts b/packages/joint-react/src/store/graph-store.ts index f17546f2d3..1a651080b1 100644 --- a/packages/joint-react/src/store/graph-store.ts +++ b/packages/joint-react/src/store/graph-store.ts @@ -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 = { ...shapes, @@ -105,6 +105,8 @@ export class GraphStore< private observer: GraphStoreObserver; private onIncrementalCellsChange?: OnIncrementalCellsChange; + // 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) { const { @@ -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) => { @@ -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(); } diff --git a/packages/joint-react/src/utils/dev-warnings.ts b/packages/joint-react/src/utils/dev-warnings.ts index 80c13fde13..1106e8299d 100644 --- a/packages/joint-react/src/utils/dev-warnings.ts +++ b/packages/joint-react/src/utils/dev-warnings.ts @@ -1,3 +1,4 @@ +import type { dia } from '@joint/core'; import { DEFAULT_PAPER_ID } from '../mvc/paper'; import type { CellId } from '../types/cell.types'; @@ -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).' + ); +}