fix: peekable in edgeless mode (#12271)

Fixes [BS-3374](https://linear.app/affine-design/issue/BS-3374/)

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

- **New Features**
  - Improved control over when the peek view is shown in "edgeless" editor mode, ensuring it only activates when interacting directly with the relevant component.
- **Bug Fixes**
  - Prevented unintended peek view activation in "edgeless" mode when clicking outside the associated component.
- **Tests**
  - Added end-to-end test verifying the peek view does not open when content is covered by a canvas element.
- **Chores**
  - Added utility function to streamline creating synced pages in edgeless mode during tests.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
doouding
2025-05-14 13:56:24 +00:00
parent 491c944ac1
commit 6959a2dab3
3 changed files with 110 additions and 2 deletions
@@ -1,5 +1,8 @@
import { DocModeProvider } from '@blocksuite/affine-shared/services';
import { isInsideEdgelessEditor } from '@blocksuite/affine-shared/utils';
import type { Constructor } from '@blocksuite/global/utils';
import { GfxControllerIdentifier } from '@blocksuite/std/gfx';
import type { BlockModel } from '@blocksuite/store';
import type { LitElement, TemplateResult } from 'lit';
import { PeekableController } from './controller.js';
@@ -47,6 +50,34 @@ export const Peekable =
const derivedClass = class extends Class {
[symbol] = new PeekableController(this as unknown as T, options.enableOn);
/**
* In edgeless mode, we need to check if the click target is not covered by
* other elements. If it is, we should not show the peek view.
*/
private _peekableInEdgeless(e: MouseEvent) {
const docModeService = this.std.getOptional(DocModeProvider);
if (
!('model' in this) ||
!docModeService ||
docModeService.getEditorMode() !== 'edgeless'
) {
return true;
}
const model = this['model'] as BlockModel;
const gfx = this.std.get(GfxControllerIdentifier);
const hitTarget = gfx.getElementByPoint(
...gfx.viewport.toModelCoordFromClientCoord([e.clientX, e.clientY])
);
if (hitTarget && hitTarget !== model) {
return false;
}
return true;
}
override connectedCallback() {
super.connectedCallback();
@@ -56,7 +87,7 @@ export const Peekable =
if (actions.includes('double-click')) {
this.disposables.addFromEvent(target, 'dblclick', e => {
if (this[symbol].peekable) {
if (this[symbol].peekable && this._peekableInEdgeless(e)) {
e.stopPropagation();
this[symbol].peek().catch(console.error);
}
@@ -68,7 +99,11 @@ export const Peekable =
!isInsideEdgelessEditor(this.std.host)
) {
this.disposables.addFromEvent(target, 'click', e => {
if (e.shiftKey && this[symbol].peekable) {
if (
e.shiftKey &&
this[symbol].peekable &&
this._peekableInEdgeless(e)
) {
e.stopPropagation();
e.stopImmediatePropagation();
this[symbol].peek().catch(console.error);
+59
View File
@@ -1,5 +1,6 @@
import { test } from '@affine-test/kit/playwright';
import {
clickEdgelessModeButton,
getSelectedXYWH,
getViewportBound,
} from '@affine-test/kit/utils/editor';
@@ -8,6 +9,7 @@ import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
createLinkedPage,
createSyncedPageInEdgeless,
type,
waitForEmptyEditor,
} from '@affine-test/kit/utils/page-logic';
@@ -104,6 +106,63 @@ test('can open peek view via db+click link card', async ({ page }) => {
).toBeVisible();
});
test('should not open peek view when content is covered by canvas element', async ({
page,
}) => {
await page.keyboard.press('Enter');
await clickEdgelessModeButton(page);
await createSyncedPageInEdgeless(page, 'Test Page');
const syncedDocBlock = await page.locator(
'affine-embed-edgeless-synced-doc-block'
);
const syncedDocRect = (await syncedDocBlock.boundingBox())!;
await expect(syncedDocBlock).toBeDefined();
const syncedDocCenter = {
x: syncedDocRect.x + syncedDocRect.width / 2,
y: syncedDocRect.y + syncedDocRect.height / 2,
};
// click to make sure peek view is working
await page.mouse.move(syncedDocCenter.x, syncedDocCenter.y, {
steps: 10,
});
await page.mouse.dblclick(syncedDocCenter.x, syncedDocCenter.y);
await page.waitForTimeout(100);
await expect(page.getByTestId('peek-view-modal')).toBeVisible();
// close peek view
await page
.locator('[data-testid="peek-view-control"][data-action-name="close"]')
.click();
await expect(page.getByTestId('peek-view-modal')).not.toBeVisible();
// create a shape covering the synced doc block
await page.locator('edgeless-toolbar-button.edgeless-shape-button').click();
await page.mouse.move(syncedDocRect.x, syncedDocRect.y);
await page.mouse.down();
await page.mouse.move(
syncedDocRect.x + syncedDocRect.width,
syncedDocRect.y + syncedDocRect.height,
{
steps: 10,
}
);
await page.mouse.up();
await page.waitForTimeout(100);
// click the same position again
await page.mouse.move(syncedDocCenter.x, syncedDocCenter.y, {
steps: 10,
});
await page.mouse.dblclick(syncedDocCenter.x, syncedDocCenter.y);
// should not open peek view because the synced doc block is covered by shape
await expect(page.getByTestId('peek-view-modal')).not.toBeVisible();
});
test('can open peek view for embedded frames', async ({ page }) => {
const frameInViewport = async () => {
const peekView = page.locator('[data-testid="peek-view-modal"]');
+14
View File
@@ -75,6 +75,20 @@ export const createLinkedPage = async (page: Page, pageName?: string) => {
.click();
};
export const createSyncedPageInEdgeless = async (
page: Page,
pageName?: string
) => {
await page.keyboard.type('@', { delay: 50 });
const cmdkPopover = page.locator('[data-testid="cmdk-quick-search"]');
await expect(cmdkPopover).toBeVisible();
await type(page, pageName || 'Untitled');
await cmdkPopover
.locator('[cmdk-item][data-value="creation:create-page"]')
.click();
};
export const createTodayPage = async (page: Page) => {
// fixme: workaround for @ popover not showing up when editor is not ready
await page.waitForTimeout(500);