fix(editor): invalid caret in note-edgeless-block on focus (#14229)

### Problem
●In edgeless mode, when starting to edit, `note-block` exhibits two
types of invalid caret behavior:
(1)**Title Region Misalignment**: Clicking on the title region
incorrectly generates the caret in the first line of the note content,
rather than in the title itself.
(2)**Vanishing Caret at Line End**: When clicking in the empty space
beyond the end of a text section, the caret appears momentarily at the
line's end but disappears immediately.
●The following video demonstrates these issues:


https://github.com/user-attachments/assets/db9c2c50-709f-4d32-912c-0f01841d2024


### Solution
●**Title Click Interception**: Added a check to determine if the click
coordinates fall in the title region. If so, the caret positioning is
now handled by a dedicated logic path. Otherwise, it falls back to the
existing note-content logic as before.
●**Range Normalization**: When the generated `range.startContainer` is
not a `TextNode`, try to find a most appropriate `TextNode` and update
the `range` accordingly.

### After
●The video below shows the behavior after this fix.


https://github.com/user-attachments/assets/b2f70b64-1fc6-4049-8379-8bcf3a488a05



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

* **Bug Fixes**
* Clicking a page block title no longer creates unwanted paragraphs and
reliably focuses the title.
* Paragraph creation now occurs only when needed and focus is applied
only after successful creation.
* Click coordinates are clamped to container bounds to prevent misplaced
cursors or focus.

* **Improvements**
* Caret normalization: clicks place the caret at the last meaningful
text position for consistent single-cursor behavior.

* **Tests**
  * Added end-to-end coverage for caret placement and focus transitions.
* New ratio-based click/double-click test utilities and a helper for
double-clicking note bodies.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
congzhou09
2026-03-02 18:51:23 +08:00
committed by GitHub
parent 5464d1a9ce
commit 478138493a
8 changed files with 305 additions and 5 deletions

View File

@@ -1,6 +1,7 @@
import { test } from '@affine-test/kit/playwright';
import {
clickEdgelessModeButton,
dblclickNoteBody,
locateEditorContainer,
locateToolbar,
} from '@affine-test/kit/utils/editor';
@@ -43,7 +44,7 @@ test('should close embed editing modal when editor switching to page mode by sho
test('embed card should not overflow the edgeless note', async ({ page }) => {
const note = page.locator('affine-edgeless-note');
await note.dblclick();
await dblclickNoteBody(page);
await type(page, '/github');
await pressEnter(page);
await page

View File

@@ -29,6 +29,7 @@ import {
type,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { clickLocatorByRatio } from '@affine-test/kit/utils/utils';
import type { EdgelessRootBlockComponent } from '@blocksuite/affine/blocks/root';
import type { IVec } from '@blocksuite/affine/global/gfx';
import type { NoteBlockModel } from '@blocksuite/affine/model';
@@ -160,6 +161,59 @@ test.describe('edgeless page block', () => {
await expect(infoButton).toBeHidden();
});
test('caret on focusing', async ({ page }) => {
const note = page.locator('affine-edgeless-note');
await note.click(); // focus note
// click on title's rear
const docTitle = note.locator('edgeless-page-block-title');
await expect(docTitle).toBeVisible();
await clickLocatorByRatio(page, docTitle, { xRatio: 0.9, yRatio: 0.8 });
const hasCaretInTitle = await page.evaluate(
hasCaretIn,
'edgeless-page-block-title'
);
expect(hasCaretInTitle).toBe(true);
await clickLocatorByRatio(page, note, { xRatio: 1.1, yRatio: 0.1 }); // cancel note focus
await note.click(); // focus note again
// click on firstParagraph's rear
const firstParagraph = note.locator('affine-paragraph:first-child');
await expect(firstParagraph).toBeVisible();
await clickLocatorByRatio(page, firstParagraph, {
xRatio: 0.9,
yRatio: 0.5,
});
const hasCaretInParagraph = await page.evaluate(
hasCaretIn,
'affine-paragraph'
);
expect(hasCaretInParagraph).toBe(true);
function hasCaretIn(elemSelector: string) {
const sel = document.getSelection();
if (!sel || sel.rangeCount === 0) return false;
const startContainer = sel.getRangeAt(0).startContainer;
const selContainer =
startContainer.nodeType === Node.TEXT_NODE
? startContainer.parentElement
: startContainer;
if (!selContainer) return false;
const closestDstElem = (selContainer as HTMLElement)?.closest(
elemSelector
);
if (!closestDstElem) return false;
return true;
}
});
test('page title should be editable', async ({ page }) => {
const note = page.locator('affine-edgeless-note');
const docTitle = note.locator('edgeless-page-block-title');

View File

@@ -3,6 +3,7 @@ import {
clickEdgelessModeButton,
clickView,
createEdgelessNoteBlock,
dblclickNoteBody,
fitViewportToContent,
focusDocTitle,
getSelectedXYWH,
@@ -35,8 +36,7 @@ test.beforeEach(async ({ page }) => {
test('should not show hidden note in embed view page mode', async ({
page,
}) => {
const note = page.locator('affine-edgeless-note');
await note.dblclick();
await dblclickNoteBody(page);
await page.keyboard.type('visible content');
await createEdgelessNoteBlock(page, [100, 100]);
await page.keyboard.press('Enter');