fix: prevent IME preedit overflow in mind map node editor (#14520)

## Summary

Update the edgeless shape text editor to resize mind map node text
bounds while IME composition is in progress.

## Changes

- listen to `compositionupdate` on the inline editor container
- trigger `_updateElementWH()` on `compositionupdate` and
`compositionend`
- keep text box dimensions in sync before composition is committed

## Testing

- Not run locally: `pnpm` is not available in this environment, so
package build/tests could not be executed here.

Fixes #11515


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Editor mounting tolerates missing/null elements and validates input to
avoid errors.
* Text creation/update consistently targets the refreshed element to
prevent mismatches.
* Inline editor listens for IME composition events and schedules
layout/size recalculation (with proper cleanup) so sizing stays in sync.

* **Tests**
* Added an integration test verifying layout/size updates during IME
composition events.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: DarkSky <25152247+darkskygit@users.noreply.github.com>
Co-authored-by: DarkSky <darksky2048@gmail.com>
This commit is contained in:
Cats Juice
2026-04-09 11:25:55 +08:00
committed by GitHub
co-authored by DarkSky DarkSky
parent 7138fea9db
commit 77c0b2ef47
4 changed files with 325 additions and 30 deletions
@@ -3,11 +3,8 @@ import {
EdgelessCRUDIdentifier, EdgelessCRUDIdentifier,
TextUtils, TextUtils,
} from '@blocksuite/affine-block-surface'; } from '@blocksuite/affine-block-surface';
import { import type { ShapeElementModel } from '@blocksuite/affine-model';
MindmapElementModel, import { MindmapElementModel, TextResizing } from '@blocksuite/affine-model';
ShapeElementModel,
TextResizing,
} from '@blocksuite/affine-model';
import type { RichText } from '@blocksuite/affine-rich-text'; import type { RichText } from '@blocksuite/affine-rich-text';
import { ThemeProvider } from '@blocksuite/affine-shared/services'; import { ThemeProvider } from '@blocksuite/affine-shared/services';
import { getSelectedRect } from '@blocksuite/affine-shared/utils'; import { getSelectedRect } from '@blocksuite/affine-shared/utils';
@@ -21,15 +18,24 @@ import {
stdContext, stdContext,
} from '@blocksuite/std'; } from '@blocksuite/std';
import { GfxControllerIdentifier } from '@blocksuite/std/gfx'; import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
import { RANGE_SYNC_EXCLUDE_ATTR } from '@blocksuite/std/inline'; import { InlineEditor, RANGE_SYNC_EXCLUDE_ATTR } from '@blocksuite/std/inline';
import { consume } from '@lit/context'; import { consume } from '@lit/context';
import { html, nothing } from 'lit'; import { html, nothing } from 'lit';
import { property, query } from 'lit/decorators.js'; import { property, query } from 'lit/decorators.js';
import { styleMap } from 'lit/directives/style-map.js'; import { styleMap } from 'lit/directives/style-map.js';
import * as Y from 'yjs'; import * as Y from 'yjs';
function isShapeElement(element: unknown): element is ShapeElementModel {
return (
!!element &&
typeof element === 'object' &&
'type' in element &&
element.type === 'shape'
);
}
export function mountShapeTextEditor( export function mountShapeTextEditor(
shapeElement: ShapeElementModel, shapeElement: { id: string } | null | undefined,
edgeless: BlockComponent edgeless: BlockComponent
) { ) {
const mountElm = edgeless.querySelector('.edgeless-mount-point'); const mountElm = edgeless.querySelector('.edgeless-mount-point');
@@ -43,24 +49,27 @@ export function mountShapeTextEditor(
const gfx = edgeless.std.get(GfxControllerIdentifier); const gfx = edgeless.std.get(GfxControllerIdentifier);
const crud = edgeless.std.get(EdgelessCRUDIdentifier); const crud = edgeless.std.get(EdgelessCRUDIdentifier);
if (!shapeElement?.id) {
console.error('Cannot mount text editor on an invalid shape element');
return;
}
const updatedElement = crud.getElementById(shapeElement.id); const updatedElement = crud.getElementById(shapeElement.id);
if (!(updatedElement instanceof ShapeElementModel)) { if (!isShapeElement(updatedElement)) {
console.error('Cannot mount text editor on a non-shape element'); console.error('Cannot mount text editor on a non-shape element');
return; return;
} }
gfx.tool.setTool(DefaultTool); gfx.tool.setTool(DefaultTool);
gfx.selection.set({ gfx.selection.set({
elements: [shapeElement.id], elements: [updatedElement.id],
editing: true, editing: true,
}); });
if (!shapeElement.text) { if (!updatedElement.text) {
const text = new Y.Text(); const text = new Y.Text();
edgeless.std crud.updateElement(updatedElement.id, { text });
.get(EdgelessCRUDIdentifier)
.updateElement(shapeElement.id, { text });
} }
const shapeEditor = new EdgelessShapeTextEditor(); const shapeEditor = new EdgelessShapeTextEditor();
@@ -70,6 +79,8 @@ export function mountShapeTextEditor(
} }
export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) { export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) {
private _compositionUpdateRaf: number | null = null;
private _keeping = false; private _keeping = false;
private _lastXYWH = ''; private _lastXYWH = '';
@@ -148,6 +159,11 @@ export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) {
} }
private _unmount() { private _unmount() {
if (this._compositionUpdateRaf !== null) {
cancelAnimationFrame(this._compositionUpdateRaf);
this._compositionUpdateRaf = null;
}
this._resizeObserver?.disconnect(); this._resizeObserver?.disconnect();
this._resizeObserver = null; this._resizeObserver = null;
@@ -171,10 +187,96 @@ export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) {
}); });
} }
private _scheduleElementWHUpdate(flush = false) {
if (flush) {
if (this._compositionUpdateRaf !== null) {
cancelAnimationFrame(this._compositionUpdateRaf);
this._compositionUpdateRaf = null;
}
this._updateElementWH();
return;
}
if (this._compositionUpdateRaf !== null) {
return;
}
this._compositionUpdateRaf = requestAnimationFrame(() => {
this._compositionUpdateRaf = null;
this._updateElementWH();
});
}
private _getInlineEditorContentRect() {
if (!this.inlineEditorContainer) {
return null;
}
const textNodes = InlineEditor.getTextNodesFromElement(
this.inlineEditorContainer
);
const firstText = textNodes[0];
const lastText = textNodes.at(-1);
if (!firstText || !lastText) {
return null;
}
const range = this.ownerDocument.createRange();
range.setStart(firstText, 0);
range.setEnd(lastText, lastText.length);
const rect = range.getBoundingClientRect();
return rect.width > 0 || rect.height > 0 ? rect : null;
}
private _updateElementWH() { private _updateElementWH() {
const bcr = this.richText.getBoundingClientRect(); const bcr = this.richText.getBoundingClientRect();
const containerHeight = this.richText.offsetHeight; const [verticalPadding, horizontalPadding] = this.element.padding;
const containerWidth = this.richText.offsetWidth; const contentRect = this._getInlineEditorContentRect();
const autoWidth =
this.element.textResizing === TextResizing.AUTO_WIDTH_AND_HEIGHT;
const constrainedAutoWidth = autoWidth && !!this.element.maxWidth;
const maxAutoWidth =
constrainedAutoWidth && typeof this.element.maxWidth === 'number'
? this.element.maxWidth
: Number.POSITIVE_INFINITY;
const nativeRangeRect = this.inlineEditor
?.getNativeRange()
?.getBoundingClientRect();
const nativeRangeHeight =
nativeRangeRect != null
? Math.max(0, nativeRangeRect.bottom - bcr.top + verticalPadding)
: 0;
const nativeRangeWidth =
nativeRangeRect != null
? Math.max(0, nativeRangeRect.right - bcr.left + horizontalPadding)
: 0;
const editorContentHeight =
this.inlineEditorContainer?.scrollHeight != null
? this.inlineEditorContainer.scrollHeight + verticalPadding * 2
: 0;
const editorContentWidth =
this.inlineEditorContainer?.scrollWidth != null
? this.inlineEditorContainer.scrollWidth + horizontalPadding * 2
: 0;
const contentRectHeight =
contentRect != null ? contentRect.height + verticalPadding * 2 : 0;
const contentRectWidth =
contentRect != null ? contentRect.width + horizontalPadding * 2 : 0;
const containerHeight = Math.max(
this.richText.offsetHeight,
contentRectHeight,
constrainedAutoWidth ? nativeRangeHeight : 0,
autoWidth ? 0 : editorContentHeight
);
const containerWidth = Math.max(
Math.min(this.richText.offsetWidth, maxAutoWidth),
Math.min(contentRectWidth, maxAutoWidth),
Math.min(nativeRangeWidth, maxAutoWidth),
autoWidth ? 0 : editorContentWidth
);
const textResizing = this.element.textResizing; const textResizing = this.element.textResizing;
if ( if (
@@ -213,7 +315,7 @@ export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) {
if (this.isMindMapNode) { if (this.isMindMapNode) {
const mindmap = this.element.group as MindmapElementModel; const mindmap = this.element.group as MindmapElementModel;
mindmap.layout(); mindmap.layout(mindmap.tree, { applyStyle: false });
} }
this.richText.style.minHeight = `${containerHeight}px`; this.richText.style.minHeight = `${containerHeight}px`;
@@ -259,6 +361,7 @@ export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) {
this.updateComplete this.updateComplete
.then(() => { .then(() => {
if (!this.inlineEditor) return; if (!this.inlineEditor) return;
if (this.element.group instanceof MindmapElementModel) { if (this.element.group instanceof MindmapElementModel) {
this.inlineEditor.selectAll(); this.inlineEditor.selectAll();
} else { } else {
@@ -280,6 +383,21 @@ export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) {
this._unmount(); this._unmount();
} }
); );
this.disposables.addFromEvent(
this.inlineEditorContainer,
'compositionupdate',
() => {
this._scheduleElementWHUpdate();
}
);
this.disposables.addFromEvent(
this.inlineEditorContainer,
'compositionend',
() => {
this._scheduleElementWHUpdate(true);
}
);
}) })
.catch(console.error); .catch(console.error);
@@ -325,6 +443,12 @@ export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) {
); );
const [x, y] = this.gfx.viewport.toViewCoord(leftTopX, leftTopY); const [x, y] = this.gfx.viewport.toViewCoord(leftTopX, leftTopY);
const autoWidth = textResizing === TextResizing.AUTO_WIDTH_AND_HEIGHT; const autoWidth = textResizing === TextResizing.AUTO_WIDTH_AND_HEIGHT;
const constrainedAutoWidth = autoWidth && !!this.element.maxWidth;
const editorWidth = constrainedAutoWidth
? 'max-content'
: textResizing === TextResizing.AUTO_HEIGHT
? rect.width + 'px'
: 'fit-content';
const color = this.std const color = this.std
.get(ThemeProvider) .get(ThemeProvider)
.generateColorProperty(this.element.color, '#000000'); .generateColorProperty(this.element.color, '#000000');
@@ -333,10 +457,7 @@ export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) {
position: 'absolute', position: 'absolute',
left: x + 'px', left: x + 'px',
top: y + 'px', top: y + 'px',
width: width: editorWidth,
textResizing === TextResizing.AUTO_HEIGHT
? rect.width + 'px'
: 'fit-content',
// override rich-text style (height: 100%) // override rich-text style (height: 100%)
height: 'initial', height: 'initial',
minHeight: minHeight:
@@ -377,9 +498,21 @@ export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) {
return html` <style> return html` <style>
edgeless-shape-text-editor v-text [data-v-text] { edgeless-shape-text-editor v-text [data-v-text] {
overflow-wrap: ${autoWidth ? 'normal' : 'anywhere'}; overflow-wrap: ${constrainedAutoWidth
word-break: ${autoWidth ? 'normal' : 'break-word'} !important; ? 'anywhere'
white-space: ${autoWidth ? 'pre' : 'pre-wrap'} !important; : autoWidth
? 'normal'
: 'anywhere'};
word-break: ${constrainedAutoWidth
? 'break-word'
: autoWidth
? 'normal'
: 'break-word'} !important;
white-space: ${constrainedAutoWidth
? 'pre-wrap'
: autoWidth
? 'pre'
: 'pre-wrap'} !important;
} }
edgeless-shape-text-editor .inline-editor { edgeless-shape-text-editor .inline-editor {
@@ -389,7 +522,7 @@ export class EdgelessShapeTextEditor extends WithDisposable(ShadowlessElement) {
<rich-text <rich-text
.yText=${this.element.text} .yText=${this.element.text}
.enableFormat=${false} .enableFormat=${false}
.enableAutoScrollHorizontally=${false} .enableAutoScrollHorizontally=${autoWidth && !this.isMindMapNode}
style=${inlineEditorStyle} style=${inlineEditorStyle}
></rich-text>`; ></rich-text>`;
} }
@@ -311,7 +311,6 @@ export class MindmapElementModel extends GfxGroupLikeElementModel<MindmapElement
id = this.surface.addElement({ id = this.surface.addElement({
type, type,
xywh: '[0,0,100,30]', xywh: '[0,0,100,30]',
maxWidth: false,
...props, ...props,
...style.node, ...style.node,
}); });
@@ -345,7 +344,6 @@ export class MindmapElementModel extends GfxGroupLikeElementModel<MindmapElement
id = this.surface.addElement({ id = this.surface.addElement({
type, type,
xywh: '[0,0,113,41]', xywh: '[0,0,113,41]',
maxWidth: false,
...props, ...props,
...rootStyle, ...rootStyle,
}); });
@@ -834,7 +832,12 @@ export class MindmapElementModel extends GfxGroupLikeElementModel<MindmapElement
} }
const stashed = new Set<GfxPrimitiveElementModel>(); const stashed = new Set<GfxPrimitiveElementModel>();
const stashedNodeIds = new Set<string>();
const traverse = (node: MindmapNode) => { const traverse = (node: MindmapNode) => {
if (this._stashedNode.has(node.id)) return;
this._stashedNode.add(node.id);
stashedNodeIds.add(node.id);
node.element.stash('xywh'); node.element.stash('xywh');
stashed.add(node.element); stashed.add(node.element);
@@ -846,7 +849,9 @@ export class MindmapElementModel extends GfxGroupLikeElementModel<MindmapElement
traverse(mindNode); traverse(mindNode);
return () => { return () => {
this._stashedNode.delete(mindNode.id); stashedNodeIds.forEach(id => {
this._stashedNode.delete(id);
});
stashed.forEach(el => { stashed.forEach(el => {
el.pop('xywh'); el.pop('xywh');
}); });
@@ -35,6 +35,7 @@ export type NodeStyle = {
strokeColor: Color; strokeColor: Color;
textResizing: TextResizing; textResizing: TextResizing;
maxWidth: false | number;
fontSize: number; fontSize: number;
fontFamily: string; fontFamily: string;
@@ -62,6 +63,8 @@ export type ConnectorStyle = {
mode: ConnectorMode; mode: ConnectorMode;
}; };
export const MINDMAP_NODE_MAX_WIDTH = 512;
export abstract class MindmapStyleGetter { export abstract class MindmapStyleGetter {
abstract readonly root: NodeStyle; abstract readonly root: NodeStyle;
@@ -90,6 +93,7 @@ export class StyleOne extends MindmapStyleGetter {
radius: 8, radius: 8,
textResizing: TextResizing.AUTO_WIDTH_AND_HEIGHT, textResizing: TextResizing.AUTO_WIDTH_AND_HEIGHT,
maxWidth: MINDMAP_NODE_MAX_WIDTH,
strokeWidth: 4, strokeWidth: 4,
strokeColor: '#53b2ef', strokeColor: '#53b2ef',
@@ -161,6 +165,7 @@ export class StyleOne extends MindmapStyleGetter {
radius: 8, radius: 8,
textResizing: TextResizing.AUTO_WIDTH_AND_HEIGHT, textResizing: TextResizing.AUTO_WIDTH_AND_HEIGHT,
maxWidth: MINDMAP_NODE_MAX_WIDTH,
strokeWidth: 3, strokeWidth: 3,
strokeColor: color, strokeColor: color,
@@ -198,6 +203,7 @@ export class StyleTwo extends MindmapStyleGetter {
radius: 3, radius: 3,
textResizing: TextResizing.AUTO_WIDTH_AND_HEIGHT, textResizing: TextResizing.AUTO_WIDTH_AND_HEIGHT,
maxWidth: MINDMAP_NODE_MAX_WIDTH,
strokeWidth: 3, strokeWidth: 3,
strokeColor: DefaultTheme.black, strokeColor: DefaultTheme.black,
@@ -271,6 +277,7 @@ export class StyleTwo extends MindmapStyleGetter {
radius: 3, radius: 3,
textResizing: TextResizing.AUTO_WIDTH_AND_HEIGHT, textResizing: TextResizing.AUTO_WIDTH_AND_HEIGHT,
maxWidth: MINDMAP_NODE_MAX_WIDTH,
strokeWidth: 3, strokeWidth: 3,
strokeColor: DefaultTheme.black, strokeColor: DefaultTheme.black,
@@ -308,6 +315,7 @@ export class StyleThree extends MindmapStyleGetter {
radius: 10, radius: 10,
textResizing: TextResizing.AUTO_WIDTH_AND_HEIGHT, textResizing: TextResizing.AUTO_WIDTH_AND_HEIGHT,
maxWidth: MINDMAP_NODE_MAX_WIDTH,
strokeWidth: 0, strokeWidth: 0,
strokeColor: 'transparent', strokeColor: 'transparent',
@@ -343,6 +351,7 @@ export class StyleThree extends MindmapStyleGetter {
radius: 10, radius: 10,
textResizing: TextResizing.AUTO_WIDTH_AND_HEIGHT, textResizing: TextResizing.AUTO_WIDTH_AND_HEIGHT,
maxWidth: MINDMAP_NODE_MAX_WIDTH,
strokeWidth: 2, strokeWidth: 2,
strokeColor, strokeColor,
@@ -420,6 +429,7 @@ export class StyleFour extends MindmapStyleGetter {
radius: 0, radius: 0,
textResizing: TextResizing.AUTO_WIDTH_AND_HEIGHT, textResizing: TextResizing.AUTO_WIDTH_AND_HEIGHT,
maxWidth: MINDMAP_NODE_MAX_WIDTH,
strokeWidth: 0, strokeWidth: 0,
strokeColor: 'transparent', strokeColor: 'transparent',
@@ -1,8 +1,13 @@
import type { MindMapView } from '@blocksuite/affine/gfx/mindmap'; import type { MindMapView } from '@blocksuite/affine/gfx/mindmap';
import { LayoutType, type MindmapElementModel } from '@blocksuite/affine-model'; import { mountShapeTextEditor } from '@blocksuite/affine/gfx/shape';
import { Bound } from '@blocksuite/global/gfx'; import {
LayoutType,
type MindmapElementModel,
type ShapeElementModel,
} from '@blocksuite/affine-model';
import { Bound, deserializeXYWH } from '@blocksuite/global/gfx';
import type { GfxController } from '@blocksuite/std/gfx'; import type { GfxController } from '@blocksuite/std/gfx';
import { beforeEach, describe, expect, test } from 'vitest'; import { beforeEach, describe, expect, test, vi } from 'vitest';
import { click, pointermove, wait } from '../utils/common.js'; import { click, pointermove, wait } from '../utils/common.js';
import { getDocRootBlock } from '../utils/edgeless.js'; import { getDocRootBlock } from '../utils/edgeless.js';
@@ -36,6 +41,148 @@ describe('mindmap', () => {
return cleanup; return cleanup;
}); });
test('should update mindmap node size during IME composition', async () => {
const mindmapId = gfx.surface!.addElement({
type: 'mindmap',
children: { text: 'root', children: [{ text: 'leaf1' }] },
});
const mindmap = () => gfx.getElementById(mindmapId) as MindmapElementModel;
const root = getDocRootBlock(window.doc, window.editor, 'edgeless');
doc.captureSync();
await wait();
const rootNode = mindmap().tree.element as ShapeElementModel;
const updateSpy = vi.spyOn(gfx.surface!, 'updateElement');
mountShapeTextEditor(rootNode, root);
await wait();
const shapeEditor = root.querySelector('edgeless-shape-text-editor') as
| (HTMLElement & {
inlineEditorContainer?: HTMLElement;
richText?: HTMLElement;
})
| null;
expect(shapeEditor).not.toBeNull();
expect(shapeEditor?.richText).toBeTruthy();
expect(shapeEditor?.inlineEditorContainer).toBeTruthy();
expect(rootNode.maxWidth).toBe(512);
const initialWidth = mindmap().tree.element.w;
const initialHeight = mindmap().tree.element.h;
const exaggeratedScrollWidth = initialWidth + 1000;
const exaggeratedScrollHeight = initialHeight + 1000;
updateSpy.mockClear();
const preedit = shapeEditor!.inlineEditorContainer!.querySelector(
'[data-v-text="true"]'
);
expect(preedit).toBeTruthy();
shapeEditor!.inlineEditorContainer?.dispatchEvent(
new CompositionEvent('compositionstart', {
data: '',
bubbles: true,
})
);
preedit!.textContent =
'测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试';
Object.defineProperty(shapeEditor!.inlineEditorContainer!, 'scrollWidth', {
configurable: true,
get: () => exaggeratedScrollWidth,
});
Object.defineProperty(shapeEditor!.inlineEditorContainer!, 'scrollHeight', {
configurable: true,
get: () => exaggeratedScrollHeight,
});
const compositionUpdate = new CompositionEvent('compositionupdate', {
data: '测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试',
bubbles: true,
});
shapeEditor!.inlineEditorContainer?.dispatchEvent(compositionUpdate);
await wait();
await wait();
const compositionWidths = updateSpy.mock.calls
.filter(
(call): call is [string, { xywh: string }] =>
call[0] === rootNode.id &&
typeof call[1] === 'object' &&
call[1] !== null &&
'xywh' in call[1] &&
typeof call[1].xywh === 'string'
)
.map(([, payload]) => deserializeXYWH(payload.xywh)[2]);
const compositionHeights = updateSpy.mock.calls
.filter(
(call): call is [string, { xywh: string }] =>
call[0] === rootNode.id &&
typeof call[1] === 'object' &&
call[1] !== null &&
'xywh' in call[1] &&
typeof call[1].xywh === 'string'
)
.map(([, payload]) => deserializeXYWH(payload.xywh)[3]);
expect(compositionWidths.length).toBeGreaterThan(0);
expect(Math.max(...compositionWidths)).toBeGreaterThan(initialWidth);
expect(Math.max(...compositionWidths)).toBeLessThanOrEqual(512);
expect(Math.max(...compositionWidths)).toBeLessThan(exaggeratedScrollWidth);
expect(Math.max(...compositionHeights)).toBeGreaterThan(initialHeight);
expect(Math.max(...compositionHeights)).toBeLessThan(
exaggeratedScrollHeight
);
const compositionEnd = new CompositionEvent('compositionend', {
data: '测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试',
bubbles: true,
});
shapeEditor!.inlineEditorContainer?.dispatchEvent(compositionEnd);
await wait();
});
test('should wrap long mindmap editor content by max width instead of viewport', async () => {
const text = 'abcdefghijklmnopqrstuvwxyz'.repeat(20);
const mindmapId = gfx.surface!.addElement({
type: 'mindmap',
children: { text, children: [{ text: 'leaf1' }] },
});
const mindmap = () => gfx.getElementById(mindmapId) as MindmapElementModel;
const root = getDocRootBlock(window.doc, window.editor, 'edgeless');
doc.captureSync();
await wait();
const rootNode = mindmap().tree.element as ShapeElementModel;
gfx.viewport.setViewport(1, [
-gfx.viewport.width / 2 + 48,
gfx.viewport.height / 2,
]);
await wait();
mountShapeTextEditor(rootNode, root);
await wait();
const shapeEditor = root.querySelector('edgeless-shape-text-editor') as
| (HTMLElement & {
richText?: HTMLElement;
})
| null;
expect(shapeEditor?.richText).toBeTruthy();
const richText = shapeEditor!.richText!;
const viewLeft = gfx.viewport.toViewCoord(rootNode.x, rootNode.y)[0];
expect(viewLeft).toBeGreaterThan(gfx.viewport.width - 64);
expect(rootNode.maxWidth).toBe(512);
expect(rootNode.w).toBeLessThanOrEqual(512);
expect(richText.clientWidth).toBeGreaterThan(400);
expect(richText.clientWidth).toBeLessThanOrEqual(512);
expect(richText.scrollWidth).toBeLessThanOrEqual(richText.clientWidth + 1);
});
test('delete the root node should remove all children', async () => { test('delete the root node should remove all children', async () => {
const tree = { const tree = {
text: 'root', text: 'root',