diff --git a/blocksuite/affine/inlines/link/src/link-node/affine-link.ts b/blocksuite/affine/inlines/link/src/link-node/affine-link.ts index f0f7091dac..5c707e6e90 100644 --- a/blocksuite/affine/inlines/link/src/link-node/affine-link.ts +++ b/blocksuite/affine/inlines/link/src/link-node/affine-link.ts @@ -59,6 +59,7 @@ export class AffineLink extends WithDisposable(ShadowlessElement) { refNodeSlotsProvider.docLinkClicked.next({ ...referenceInfo, + openMode: e?.button === 1 ? 'open-in-new-tab' : undefined, host: this.std.host, }); }; @@ -149,6 +150,7 @@ export class AffineLink extends WithDisposable(ShadowlessElement) { target="_blank" style=${styleMap(style)} @click=${this.openLink} + @auxclick=${this.openLink} @mouseup=${this._onMouseUp} >`; diff --git a/blocksuite/affine/inlines/reference/src/reference-node/reference-node.ts b/blocksuite/affine/inlines/reference/src/reference-node/reference-node.ts index 009d008da7..fd7c0eff36 100644 --- a/blocksuite/affine/inlines/reference/src/reference-node/reference-node.ts +++ b/blocksuite/affine/inlines/reference/src/reference-node/reference-node.ts @@ -154,6 +154,8 @@ export class AffineReference extends WithDisposable(ShadowlessElement) { this.std.getOptional(RefNodeSlotsProvider)?.docLinkClicked.next({ ...this.referenceInfo, ...event, + openMode: + event?.event?.button === 1 ? 'open-in-new-tab' : event?.openMode, host: this.std.host, }); }; @@ -285,6 +287,7 @@ export class AffineReference extends WithDisposable(ShadowlessElement) { class="affine-reference" style=${styleMap(style)} @click=${(event: MouseEvent) => this.open({ event })} + @auxclick=${(event: MouseEvent) => this.open({ event })} >${content}`; } diff --git a/blocksuite/framework/global/src/env/index.ts b/blocksuite/framework/global/src/env/index.ts index afbe24ecf9..6dffcbaf39 100644 --- a/blocksuite/framework/global/src/env/index.ts +++ b/blocksuite/framework/global/src/env/index.ts @@ -26,4 +26,6 @@ export const IS_IPAD = export const IS_WINDOWS = /Win/.test(platform) || /win32/.test(platform); +export const IS_LINUX = /Linux/.test(platform); + export const IS_MOBILE = IS_IOS || IS_IPAD || IS_ANDROID; diff --git a/packages/frontend/core/src/blocksuite/block-suite-editor/blocksuite-editor.tsx b/packages/frontend/core/src/blocksuite/block-suite-editor/blocksuite-editor.tsx index 0537f15057..3c1a20b9d7 100644 --- a/packages/frontend/core/src/blocksuite/block-suite-editor/blocksuite-editor.tsx +++ b/packages/frontend/core/src/blocksuite/block-suite-editor/blocksuite-editor.tsx @@ -14,6 +14,7 @@ import track from '@affine/track'; import { appendParagraphCommand } from '@blocksuite/affine/blocks/paragraph'; import type { DocTitle } from '@blocksuite/affine/fragments/doc-title'; import { DisposableGroup } from '@blocksuite/affine/global/disposable'; +import { IS_LINUX } from '@blocksuite/affine/global/env'; import type { DocMode, RootBlockModel } from '@blocksuite/affine/model'; import { customImageProxyMiddleware, @@ -178,7 +179,14 @@ const BlockSuiteEditorImpl = ({ const editorContainer = rootRef.current; if (editorContainer) { const handleMiddleClick = (e: MouseEvent) => { - if (!enableMiddleClickPaste && e.button === 1) { + if ( + e.target instanceof HTMLElement && + (e.target.closest('affine-reference') || + e.target.closest('affine-link')) + ) { + return; + } + if (!enableMiddleClickPaste && IS_LINUX && e.button === 1) { e.preventDefault(); } }; diff --git a/tests/affine-local/e2e/links.spec.ts b/tests/affine-local/e2e/links.spec.ts index 6edab86270..397c865009 100644 --- a/tests/affine-local/e2e/links.spec.ts +++ b/tests/affine-local/e2e/links.spec.ts @@ -1,9 +1,15 @@ import { toolbarButtons } from '@affine-test/kit/bs/linked-toolbar'; import { waitNextFrame } from '@affine-test/kit/bs/misc'; import { test } from '@affine-test/kit/playwright'; -import { clickEdgelessModeButton } from '@affine-test/kit/utils/editor'; +import { + clickEdgelessModeButton, + locateToolbar, +} from '@affine-test/kit/utils/editor'; import { pasteByKeyboard, + pressArrowUp, + pressBackspace, + pressEnter, selectAllByKeyboard, writeTextToClipboard, } from '@affine-test/kit/utils/keyboard'; @@ -13,6 +19,7 @@ import { createLinkedPage, createTodayPage, getBlockSuiteEditorTitle, + type, waitForEmptyEditor, } from '@affine-test/kit/utils/page-logic'; import { @@ -1164,3 +1171,75 @@ test('should add HTTP protocol into link automatically', async ({ page }) => { url = await linkPreview.locator('a').getAttribute('href'); expect(url).toBe(link); }); + +test('should open link in new tab when middle clicking on link', async ({ + page, + context, +}) => { + await pressEnter(page); + + // external link + { + await type(page, 'external-link'); + await selectAllByKeyboard(page); + const toolbar = locateToolbar(page); + await toolbar.getByTestId('link').click(); + const input = page.locator('.affine-link-popover-input'); + + const externalUrl = new URL('https://github.com/').toString(); + await input.fill(externalUrl); + await pressEnter(page); + + const newTabPromise = context.waitForEvent('page'); + + await page.locator('affine-link').click({ button: 'middle' }); + + const newTab = await newTabPromise; + await expect(newTab).toHaveURL(externalUrl); + await newTab.close(); + } + + await selectAllByKeyboard(page); + await pressBackspace(page); + + // internal link + { + await type(page, 'internal-link'); + const url = page.url(); + await selectAllByKeyboard(page); + const toolbar = locateToolbar(page); + await toolbar.getByTestId('link').click(); + const input = page.locator('.affine-link-popover-input'); + await input.fill(url); + await pressEnter(page); + + const newTabPromise = context.waitForEvent('page'); + + await page.locator('affine-link').click({ button: 'middle' }); + + const newTab = await newTabPromise; + // there is a refreshKey in the url + expect(newTab.url()).toContain(url); + await newTab.close(); + } + + await selectAllByKeyboard(page); + await pressBackspace(page); + + // reference doc + { + await pressArrowUp(page); + await type(page, 'ThisPage'); + await pressEnter(page); + await type(page, '@ThisPage'); + await pressEnter(page); + + const newTabPromise = context.waitForEvent('page'); + + await page.locator('affine-reference').click({ button: 'middle' }); + + const newTab = await newTabPromise; + expect(newTab.url()).toContain(page.url()); + await newTab.close(); + } +});