mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-02 01:49:51 +08:00
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:
@@ -1,5 +1,8 @@
|
|||||||
|
import { DocModeProvider } from '@blocksuite/affine-shared/services';
|
||||||
import { isInsideEdgelessEditor } from '@blocksuite/affine-shared/utils';
|
import { isInsideEdgelessEditor } from '@blocksuite/affine-shared/utils';
|
||||||
import type { Constructor } from '@blocksuite/global/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 type { LitElement, TemplateResult } from 'lit';
|
||||||
|
|
||||||
import { PeekableController } from './controller.js';
|
import { PeekableController } from './controller.js';
|
||||||
@@ -47,6 +50,34 @@ export const Peekable =
|
|||||||
const derivedClass = class extends Class {
|
const derivedClass = class extends Class {
|
||||||
[symbol] = new PeekableController(this as unknown as T, options.enableOn);
|
[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() {
|
override connectedCallback() {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
|
||||||
@@ -56,7 +87,7 @@ export const Peekable =
|
|||||||
|
|
||||||
if (actions.includes('double-click')) {
|
if (actions.includes('double-click')) {
|
||||||
this.disposables.addFromEvent(target, 'dblclick', e => {
|
this.disposables.addFromEvent(target, 'dblclick', e => {
|
||||||
if (this[symbol].peekable) {
|
if (this[symbol].peekable && this._peekableInEdgeless(e)) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this[symbol].peek().catch(console.error);
|
this[symbol].peek().catch(console.error);
|
||||||
}
|
}
|
||||||
@@ -68,7 +99,11 @@ export const Peekable =
|
|||||||
!isInsideEdgelessEditor(this.std.host)
|
!isInsideEdgelessEditor(this.std.host)
|
||||||
) {
|
) {
|
||||||
this.disposables.addFromEvent(target, 'click', e => {
|
this.disposables.addFromEvent(target, 'click', e => {
|
||||||
if (e.shiftKey && this[symbol].peekable) {
|
if (
|
||||||
|
e.shiftKey &&
|
||||||
|
this[symbol].peekable &&
|
||||||
|
this._peekableInEdgeless(e)
|
||||||
|
) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
this[symbol].peek().catch(console.error);
|
this[symbol].peek().catch(console.error);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { test } from '@affine-test/kit/playwright';
|
import { test } from '@affine-test/kit/playwright';
|
||||||
import {
|
import {
|
||||||
|
clickEdgelessModeButton,
|
||||||
getSelectedXYWH,
|
getSelectedXYWH,
|
||||||
getViewportBound,
|
getViewportBound,
|
||||||
} from '@affine-test/kit/utils/editor';
|
} from '@affine-test/kit/utils/editor';
|
||||||
@@ -8,6 +9,7 @@ import { openHomePage } from '@affine-test/kit/utils/load-page';
|
|||||||
import {
|
import {
|
||||||
clickNewPageButton,
|
clickNewPageButton,
|
||||||
createLinkedPage,
|
createLinkedPage,
|
||||||
|
createSyncedPageInEdgeless,
|
||||||
type,
|
type,
|
||||||
waitForEmptyEditor,
|
waitForEmptyEditor,
|
||||||
} from '@affine-test/kit/utils/page-logic';
|
} from '@affine-test/kit/utils/page-logic';
|
||||||
@@ -104,6 +106,63 @@ test('can open peek view via db+click link card', async ({ page }) => {
|
|||||||
).toBeVisible();
|
).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 }) => {
|
test('can open peek view for embedded frames', async ({ page }) => {
|
||||||
const frameInViewport = async () => {
|
const frameInViewport = async () => {
|
||||||
const peekView = page.locator('[data-testid="peek-view-modal"]');
|
const peekView = page.locator('[data-testid="peek-view-modal"]');
|
||||||
|
|||||||
@@ -75,6 +75,20 @@ export const createLinkedPage = async (page: Page, pageName?: string) => {
|
|||||||
.click();
|
.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) => {
|
export const createTodayPage = async (page: Page) => {
|
||||||
// fixme: workaround for @ popover not showing up when editor is not ready
|
// fixme: workaround for @ popover not showing up when editor is not ready
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
|
|||||||
Reference in New Issue
Block a user