fix(editor): middle click open new tab (#12902)

Close
[BS-3251](https://linear.app/affine-design/issue/BS-3251/正文的inline链接,chrome中,中键开新窗口的行为丢失了)

#### PR Dependency Tree


* **PR #12902** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

- **New Features**
- Middle-clicking on links and references now opens them in a new
browser tab.
- Linux platform detection has been added for improved
environment-specific behavior.

- **Bug Fixes**
- Middle-click paste prevention is now limited to Linux environments
when the relevant setting is disabled and excludes clicks on links and
references.

- **Tests**
- Added end-to-end tests to verify that middle-clicking links opens them
in a new tab for external links, internal links, and reference
documents.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
L-Sun 2025-06-23 20:33:51 +08:00 committed by GitHub
parent a8c18cd631
commit 1686b92adb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 96 additions and 2 deletions

View File

@ -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}
><v-text .str=${this.delta.insert}></v-text
></a>`;

View File

@ -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}<v-text .str=${ZERO_WIDTH_FOR_EMBED_NODE}></v-text
></span>`;
}

View File

@ -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;

View File

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

View File

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