From f374f2695faea54330d9d0a7a5a513ae0a1e944f Mon Sep 17 00:00:00 2001 From: fundon Date: Tue, 1 Apr 2025 12:39:13 +0000 Subject: [PATCH] fix(core): add shortcuts to open doc dropdown menu (#11358) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes: [BS-2992](https://linear.app/affine-design/issue/BS-2992/走查toolbar上的open-in-button) [Screen Recording 2025-04-01 at 16.37.57.mov (uploaded via Graphite) ](https://app.graphite.dev/media/video/8ypiIKZXudF5a0tIgIzf/cf4b1baf-aa2c-4f37-9c62-f7202d0f7c42.mov) --- .../src/components/open-doc-dropdown-menu.ts | 1 + blocksuite/affine/all/src/effects.ts | 2 + blocksuite/affine/components/package.json | 3 +- .../open-doc-dropdown-menu/dropdown-menu.ts | 121 ++++++++++++++++++ .../src/open-doc-dropdown-menu/index.ts | 7 + .../extensions/editor-config/toolbar/index.ts | 101 +++++---------- .../src/blocksuite/extensions/open-doc.ts | 8 +- tests/affine-local/e2e/links.spec.ts | 27 ++-- 8 files changed, 184 insertions(+), 86 deletions(-) create mode 100644 blocksuite/affine/all/src/components/open-doc-dropdown-menu.ts create mode 100644 blocksuite/affine/components/src/open-doc-dropdown-menu/dropdown-menu.ts create mode 100644 blocksuite/affine/components/src/open-doc-dropdown-menu/index.ts diff --git a/blocksuite/affine/all/src/components/open-doc-dropdown-menu.ts b/blocksuite/affine/all/src/components/open-doc-dropdown-menu.ts new file mode 100644 index 0000000000..2513f9c68e --- /dev/null +++ b/blocksuite/affine/all/src/components/open-doc-dropdown-menu.ts @@ -0,0 +1 @@ +export * from '@blocksuite/affine-components/open-doc-dropdown-menu'; diff --git a/blocksuite/affine/all/src/effects.ts b/blocksuite/affine/all/src/effects.ts index 44ccb7e376..1b25997c31 100644 --- a/blocksuite/affine/all/src/effects.ts +++ b/blocksuite/affine/all/src/effects.ts @@ -34,6 +34,7 @@ import { effects as componentHighlightDropdownMenuEffects } from '@blocksuite/af import { IconButton } from '@blocksuite/affine-components/icon-button'; import { effects as componentLinkPreviewEffects } from '@blocksuite/affine-components/link-preview'; import { effects as componentLinkedDocTitleEffects } from '@blocksuite/affine-components/linked-doc-title'; +import { effects as componentOpenDocDropdownMenuEffects } from '@blocksuite/affine-components/open-doc-dropdown-menu'; import { effects as componentPortalEffects } from '@blocksuite/affine-components/portal'; import { effects as componentSizeDropdownMenuEffects } from '@blocksuite/affine-components/size-dropdown-menu'; import { SmoothCorner } from '@blocksuite/affine-components/smooth-corner'; @@ -164,6 +165,7 @@ export function effects() { componentEdgelessLineWidthEffects(); componentEdgelessLineStylesEffects(); componentEdgelessShapeColorPickerEffects(); + componentOpenDocDropdownMenuEffects(); widgetScrollAnchoringEffects(); widgetFrameTitleEffects(); diff --git a/blocksuite/affine/components/package.json b/blocksuite/affine/components/package.json index b056d0b9b3..f62f1ef3d2 100644 --- a/blocksuite/affine/components/package.json +++ b/blocksuite/affine/components/package.json @@ -68,7 +68,8 @@ "./size-dropdown-menu": "./src/size-dropdown-menu/index.ts", "./edgeless-line-width-panel": "./src/edgeless-line-width-panel/index.ts", "./edgeless-line-styles-panel": "./src/edgeless-line-styles-panel/index.ts", - "./edgeless-shape-color-picker": "./src/edgeless-shape-color-picker/index.ts" + "./edgeless-shape-color-picker": "./src/edgeless-shape-color-picker/index.ts", + "./open-doc-dropdown-menu": "./src/open-doc-dropdown-menu/index.ts" }, "files": [ "src", diff --git a/blocksuite/affine/components/src/open-doc-dropdown-menu/dropdown-menu.ts b/blocksuite/affine/components/src/open-doc-dropdown-menu/dropdown-menu.ts new file mode 100644 index 0000000000..7531c6269e --- /dev/null +++ b/blocksuite/affine/components/src/open-doc-dropdown-menu/dropdown-menu.ts @@ -0,0 +1,121 @@ +import { + type OpenDocMode, + type ToolbarAction, + ToolbarContext, +} from '@blocksuite/affine-shared/services'; +import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme'; +import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit'; +import { PropTypes, requiredProperties } from '@blocksuite/std'; +import type { ReadonlySignal } from '@preact/signals-core'; +import { css, html, LitElement } from 'lit'; +import { property } from 'lit/decorators.js'; +import { ifDefined } from 'lit-html/directives/if-defined.js'; +import { repeat } from 'lit-html/directives/repeat.js'; + +import { EditorChevronDown } from '../toolbar'; + +@requiredProperties({ + actions: PropTypes.array, + context: PropTypes.instanceOf(ToolbarContext), + openDocMode$: PropTypes.object, + updateOpenDocMode: PropTypes.instanceOf(Function), +}) +export class OpenDocDropdownMenu extends SignalWatcher( + WithDisposable(LitElement) +) { + static override styles = css` + div[data-orientation] { + width: 264px; + gap: 4px; + min-width: unset; + overflow: unset; + } + + editor-menu-action { + .label { + display: flex; + flex: 1; + justify-content: space-between; + } + + .shortcut { + color: ${unsafeCSSVarV2('text/secondary')}; + } + } + `; + + @property({ attribute: false }) + accessor actions!: (ToolbarAction & { + mode: OpenDocMode; + shortcut?: string; + })[]; + + @property({ attribute: false }) + accessor context!: ToolbarContext; + + @property({ attribute: false }) + accessor openDocMode$!: ReadonlySignal; + + @property({ attribute: false }) + accessor updateOpenDocMode!: (mode: OpenDocMode) => void; + + override render() { + const { + actions, + context, + openDocMode$: { value: openDocMode }, + updateOpenDocMode, + } = this; + const currentAction = + actions.find(a => a.mode === openDocMode) ?? actions[0]; + + return html` + + ${currentAction.icon} + Open ${EditorChevronDown} + + `} + > +
+ ${repeat( + actions, + action => action.id, + ({ label, icon, run, disabled, mode, shortcut }) => html` + { + run?.(context); + updateOpenDocMode(mode); + }} + > + ${icon} +
+ ${label} + ${shortcut} +
+
+ ` + )} +
+
+ `; + } +} + +declare global { + interface HTMLElementTagNameMap { + 'affine-open-doc-dropdown-menu': OpenDocDropdownMenu; + } +} diff --git a/blocksuite/affine/components/src/open-doc-dropdown-menu/index.ts b/blocksuite/affine/components/src/open-doc-dropdown-menu/index.ts new file mode 100644 index 0000000000..f7ab73bfd1 --- /dev/null +++ b/blocksuite/affine/components/src/open-doc-dropdown-menu/index.ts @@ -0,0 +1,7 @@ +import { OpenDocDropdownMenu } from './dropdown-menu'; + +export * from './dropdown-menu'; + +export function effects() { + customElements.define('affine-open-doc-dropdown-menu', OpenDocDropdownMenu); +} diff --git a/packages/frontend/core/src/blocksuite/extensions/editor-config/toolbar/index.ts b/packages/frontend/core/src/blocksuite/extensions/editor-config/toolbar/index.ts index 77820e3de8..1634fb0dec 100644 --- a/packages/frontend/core/src/blocksuite/extensions/editor-config/toolbar/index.ts +++ b/packages/frontend/core/src/blocksuite/extensions/editor-config/toolbar/index.ts @@ -29,7 +29,6 @@ import { import { isPeekable, peek } from '@blocksuite/affine/components/peek'; import { toast } from '@blocksuite/affine/components/toast'; import { - EditorChevronDown, type MenuContext, type MenuItemGroup, } from '@blocksuite/affine/components/toolbar'; @@ -52,6 +51,7 @@ import { GenerateDocUrlProvider, isRemovedUserInfo, OpenDocExtensionIdentifier, + type OpenDocMode, type ToolbarAction, type ToolbarActionGenerator, type ToolbarActionGroupGenerator, @@ -488,39 +488,42 @@ function createOpenDocActions( | SurfaceRefBlockComponent, isSameDoc: boolean, actions = openDocActions.map( - ({ type: mode, label, icon, enabled: when }, i) => ({ + ({ type: mode, label, icon, enabled: when, shortcut }, i) => ({ mode, id: `${i}.${mode}`, label, icon, when, + shortcut, }) ) ) { return actions .filter(action => action.when) - .map(action => { - const openMode = action.mode; - const shouldOpenInCenterPeek = openMode === 'open-in-center-peek'; - const shouldOpenInActiveView = openMode === 'open-in-active-view'; + .map( + action => { + const openMode = action.mode; + const shouldOpenInCenterPeek = openMode === 'open-in-center-peek'; + const shouldOpenInActiveView = openMode === 'open-in-active-view'; - return { - ...action, - generate(ctx) { - const disabled = shouldOpenInActiveView ? isSameDoc : false; + return { + ...action, + generate(ctx) { + const disabled = shouldOpenInActiveView ? isSameDoc : false; - const when = - ctx.std.get(OpenDocExtensionIdentifier).isAllowed(openMode) && - (shouldOpenInCenterPeek ? isPeekable(target) : true); + const when = + ctx.std.get(OpenDocExtensionIdentifier).isAllowed(openMode) && + (shouldOpenInCenterPeek ? isPeekable(target) : true); - const run = shouldOpenInCenterPeek - ? (_ctx: ToolbarContext) => peek(target) - : (_ctx: ToolbarContext) => target.open({ openMode }); + const run = shouldOpenInCenterPeek + ? (_ctx: ToolbarContext) => peek(target) + : (_ctx: ToolbarContext) => target.open({ openMode }); - return { disabled, when, run }; - }, - }; - }) + return { disabled, when, run }; + }, + }; + } + ) .filter(action => { if (typeof action.when === 'function') return action.when(ctx); return action.when ?? true; @@ -713,58 +716,20 @@ function renderOpenDocMenu( })); if (!actions.length) return null; - const currentOpenMode = - settings.settingSignal.value.openDocMode ?? 'open-in-active-view'; - const currentIcon = - openDocActions.find(a => a.type === currentOpenMode)?.icon ?? - OpenInNewIcon(); - const currentAction = actions.find(a => a.icon === currentIcon) ?? actions[0]; - return html`${keyed( target, html` - currentAction.run?.(ctx)} + + settings.settingSignal.value.openDocMode ?? 'open-in-active-view' + )} + .updateOpenDocMode=${(mode: OpenDocMode) => + settings.openDocMode.set(mode)} > - ${currentAction.icon} Open - - - ${EditorChevronDown} - - `} - > -
- ${repeat( - actions, - action => action.id, - ({ label, icon, run, disabled }) => html` - { - run?.(ctx); - settings.openDocMode.set( - openDocActions.find(a => a.icon === icon)?.type ?? - 'open-in-active-view' - ); - }} - > - ${icon}${label} - - ` - )} -
-
+ ` )}`; } diff --git a/packages/frontend/core/src/blocksuite/extensions/open-doc.ts b/packages/frontend/core/src/blocksuite/extensions/open-doc.ts index 4db7bd9623..e70918280e 100644 --- a/packages/frontend/core/src/blocksuite/extensions/open-doc.ts +++ b/packages/frontend/core/src/blocksuite/extensions/open-doc.ts @@ -12,7 +12,10 @@ import { SplitViewIcon, } from '@blocksuite/icons/lit'; -type OpenDocAction = OpenDocConfigItem & { enabled: boolean }; +type OpenDocAction = OpenDocConfigItem & { + enabled: boolean; + shortcut?: string; +}; export const openDocActions: Array = [ { @@ -25,18 +28,21 @@ export const openDocActions: Array = [ type: 'open-in-new-view', label: I18n['com.affine.peek-view-controls.open-doc-in-split-view'](), icon: SplitViewIcon(), + shortcut: '⌘ ⌥ + click', enabled: BUILD_CONFIG.isElectron, }, { type: 'open-in-new-tab', label: I18n['com.affine.peek-view-controls.open-doc-in-new-tab'](), icon: OpenInNewIcon(), + shortcut: '⌘ + click', enabled: true, }, { type: 'open-in-center-peek', label: I18n['com.affine.peek-view-controls.open-doc-in-center-peek'](), icon: CenterPeekIcon(), + shortcut: '⇧ + click', enabled: true, }, ].filter( diff --git a/tests/affine-local/e2e/links.spec.ts b/tests/affine-local/e2e/links.spec.ts index 7d9e6fd98d..3b982159a0 100644 --- a/tests/affine-local/e2e/links.spec.ts +++ b/tests/affine-local/e2e/links.spec.ts @@ -1057,16 +1057,11 @@ test('should save open doc mode of internal links', async ({ page }) => { const inlineLink = page.locator('affine-reference'); await inlineLink.hover(); - const recentOpenModeBtn = toolbar.getByLabel(/^Open/).nth(0); - await expect(recentOpenModeBtn).toHaveAttribute( - 'aria-label', + const openDocBtn = toolbar.getByLabel(/^Open doc$/); + await expect(openDocBtn).toHaveAttribute( + 'data-open-doc-mode', 'Open this doc' ); - await expect( - recentOpenModeBtn.locator('span.label:has-text("Open")') - ).toBeVisible(); - - const openDocBtn = toolbar.getByLabel(/^Open doc$/); await openDocBtn.click(); const openDocMenu = toolbar.getByLabel('Open doc menu'); @@ -1089,8 +1084,8 @@ test('should save open doc mode of internal links', async ({ page }) => { await inlineLink.hover(); await expect(toolbar).toBeVisible(); - await expect(recentOpenModeBtn).toHaveAttribute( - 'aria-label', + await expect(openDocBtn).toHaveAttribute( + 'data-open-doc-mode', 'Open in center peek' ); @@ -1098,8 +1093,8 @@ test('should save open doc mode of internal links', async ({ page }) => { await cardViewBtn.click(); await expect(toolbar).toBeVisible(); - await expect(recentOpenModeBtn).toHaveAttribute( - 'aria-label', + await expect(openDocBtn).toHaveAttribute( + 'data-open-doc-mode', 'Open in center peek' ); @@ -1107,8 +1102,8 @@ test('should save open doc mode of internal links', async ({ page }) => { await embedViewBtn.click(); await expect(toolbar).toBeVisible(); - await expect(recentOpenModeBtn).toHaveAttribute( - 'aria-label', + await expect(openDocBtn).toHaveAttribute( + 'data-open-doc-mode', 'Open in center peek' ); @@ -1120,8 +1115,8 @@ test('should save open doc mode of internal links', async ({ page }) => { await page.waitForTimeout(250); await expect(toolbar).toBeVisible(); - await expect(recentOpenModeBtn).toHaveAttribute( - 'aria-label', + await expect(openDocBtn).toHaveAttribute( + 'data-open-doc-mode', 'Open in center peek' ); });