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();
+ }
+});