fix(core): add shortcuts to open doc dropdown menu (#11358)
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 <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/8ypiIKZXudF5a0tIgIzf/cf4b1baf-aa2c-4f37-9c62-f7202d0f7c42.mov" />](https://app.graphite.dev/media/video/8ypiIKZXudF5a0tIgIzf/cf4b1baf-aa2c-4f37-9c62-f7202d0f7c42.mov)
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
export * from '@blocksuite/affine-components/open-doc-dropdown-menu';
|
||||||
@@ -34,6 +34,7 @@ import { effects as componentHighlightDropdownMenuEffects } from '@blocksuite/af
|
|||||||
import { IconButton } from '@blocksuite/affine-components/icon-button';
|
import { IconButton } from '@blocksuite/affine-components/icon-button';
|
||||||
import { effects as componentLinkPreviewEffects } from '@blocksuite/affine-components/link-preview';
|
import { effects as componentLinkPreviewEffects } from '@blocksuite/affine-components/link-preview';
|
||||||
import { effects as componentLinkedDocTitleEffects } from '@blocksuite/affine-components/linked-doc-title';
|
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 componentPortalEffects } from '@blocksuite/affine-components/portal';
|
||||||
import { effects as componentSizeDropdownMenuEffects } from '@blocksuite/affine-components/size-dropdown-menu';
|
import { effects as componentSizeDropdownMenuEffects } from '@blocksuite/affine-components/size-dropdown-menu';
|
||||||
import { SmoothCorner } from '@blocksuite/affine-components/smooth-corner';
|
import { SmoothCorner } from '@blocksuite/affine-components/smooth-corner';
|
||||||
@@ -164,6 +165,7 @@ export function effects() {
|
|||||||
componentEdgelessLineWidthEffects();
|
componentEdgelessLineWidthEffects();
|
||||||
componentEdgelessLineStylesEffects();
|
componentEdgelessLineStylesEffects();
|
||||||
componentEdgelessShapeColorPickerEffects();
|
componentEdgelessShapeColorPickerEffects();
|
||||||
|
componentOpenDocDropdownMenuEffects();
|
||||||
|
|
||||||
widgetScrollAnchoringEffects();
|
widgetScrollAnchoringEffects();
|
||||||
widgetFrameTitleEffects();
|
widgetFrameTitleEffects();
|
||||||
|
|||||||
@@ -68,7 +68,8 @@
|
|||||||
"./size-dropdown-menu": "./src/size-dropdown-menu/index.ts",
|
"./size-dropdown-menu": "./src/size-dropdown-menu/index.ts",
|
||||||
"./edgeless-line-width-panel": "./src/edgeless-line-width-panel/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-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": [
|
"files": [
|
||||||
"src",
|
"src",
|
||||||
|
|||||||
@@ -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<OpenDocMode>;
|
||||||
|
|
||||||
|
@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`
|
||||||
|
<editor-menu-button
|
||||||
|
aria-label="Open doc menu"
|
||||||
|
.contentPadding="${'8px'}"
|
||||||
|
.button=${html`
|
||||||
|
<editor-icon-button
|
||||||
|
data-open-doc-mode="${currentAction.label}"
|
||||||
|
aria-label="Open doc"
|
||||||
|
.tooltip="${'Open doc'}"
|
||||||
|
.justify="${'space-between'}"
|
||||||
|
.labelHeight="${'20px'}"
|
||||||
|
.iconContainerWidth="${'84px'}"
|
||||||
|
>
|
||||||
|
${currentAction.icon}
|
||||||
|
<span class="label">Open</span> ${EditorChevronDown}
|
||||||
|
</editor-icon-button>
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div data-orientation="vertical">
|
||||||
|
${repeat(
|
||||||
|
actions,
|
||||||
|
action => action.id,
|
||||||
|
({ label, icon, run, disabled, mode, shortcut }) => html`
|
||||||
|
<editor-menu-action
|
||||||
|
aria-label=${ifDefined(label)}
|
||||||
|
?disabled=${ifDefined(disabled)}
|
||||||
|
@click=${() => {
|
||||||
|
run?.(context);
|
||||||
|
updateOpenDocMode(mode);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
${icon}
|
||||||
|
<div class="label">
|
||||||
|
${label}
|
||||||
|
<span class="shortcut">${shortcut}</span>
|
||||||
|
</div>
|
||||||
|
</editor-menu-action>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</editor-menu-button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'affine-open-doc-dropdown-menu': OpenDocDropdownMenu;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { OpenDocDropdownMenu } from './dropdown-menu';
|
||||||
|
|
||||||
|
export * from './dropdown-menu';
|
||||||
|
|
||||||
|
export function effects() {
|
||||||
|
customElements.define('affine-open-doc-dropdown-menu', OpenDocDropdownMenu);
|
||||||
|
}
|
||||||
@@ -29,7 +29,6 @@ import {
|
|||||||
import { isPeekable, peek } from '@blocksuite/affine/components/peek';
|
import { isPeekable, peek } from '@blocksuite/affine/components/peek';
|
||||||
import { toast } from '@blocksuite/affine/components/toast';
|
import { toast } from '@blocksuite/affine/components/toast';
|
||||||
import {
|
import {
|
||||||
EditorChevronDown,
|
|
||||||
type MenuContext,
|
type MenuContext,
|
||||||
type MenuItemGroup,
|
type MenuItemGroup,
|
||||||
} from '@blocksuite/affine/components/toolbar';
|
} from '@blocksuite/affine/components/toolbar';
|
||||||
@@ -52,6 +51,7 @@ import {
|
|||||||
GenerateDocUrlProvider,
|
GenerateDocUrlProvider,
|
||||||
isRemovedUserInfo,
|
isRemovedUserInfo,
|
||||||
OpenDocExtensionIdentifier,
|
OpenDocExtensionIdentifier,
|
||||||
|
type OpenDocMode,
|
||||||
type ToolbarAction,
|
type ToolbarAction,
|
||||||
type ToolbarActionGenerator,
|
type ToolbarActionGenerator,
|
||||||
type ToolbarActionGroupGenerator,
|
type ToolbarActionGroupGenerator,
|
||||||
@@ -488,39 +488,42 @@ function createOpenDocActions(
|
|||||||
| SurfaceRefBlockComponent,
|
| SurfaceRefBlockComponent,
|
||||||
isSameDoc: boolean,
|
isSameDoc: boolean,
|
||||||
actions = openDocActions.map(
|
actions = openDocActions.map(
|
||||||
({ type: mode, label, icon, enabled: when }, i) => ({
|
({ type: mode, label, icon, enabled: when, shortcut }, i) => ({
|
||||||
mode,
|
mode,
|
||||||
id: `${i}.${mode}`,
|
id: `${i}.${mode}`,
|
||||||
label,
|
label,
|
||||||
icon,
|
icon,
|
||||||
when,
|
when,
|
||||||
|
shortcut,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return actions
|
return actions
|
||||||
.filter(action => action.when)
|
.filter(action => action.when)
|
||||||
.map<ToolbarActionGenerator>(action => {
|
.map<ToolbarActionGenerator & { mode: OpenDocMode; shortcut?: string }>(
|
||||||
const openMode = action.mode;
|
action => {
|
||||||
const shouldOpenInCenterPeek = openMode === 'open-in-center-peek';
|
const openMode = action.mode;
|
||||||
const shouldOpenInActiveView = openMode === 'open-in-active-view';
|
const shouldOpenInCenterPeek = openMode === 'open-in-center-peek';
|
||||||
|
const shouldOpenInActiveView = openMode === 'open-in-active-view';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...action,
|
...action,
|
||||||
generate(ctx) {
|
generate(ctx) {
|
||||||
const disabled = shouldOpenInActiveView ? isSameDoc : false;
|
const disabled = shouldOpenInActiveView ? isSameDoc : false;
|
||||||
|
|
||||||
const when =
|
const when =
|
||||||
ctx.std.get(OpenDocExtensionIdentifier).isAllowed(openMode) &&
|
ctx.std.get(OpenDocExtensionIdentifier).isAllowed(openMode) &&
|
||||||
(shouldOpenInCenterPeek ? isPeekable(target) : true);
|
(shouldOpenInCenterPeek ? isPeekable(target) : true);
|
||||||
|
|
||||||
const run = shouldOpenInCenterPeek
|
const run = shouldOpenInCenterPeek
|
||||||
? (_ctx: ToolbarContext) => peek(target)
|
? (_ctx: ToolbarContext) => peek(target)
|
||||||
: (_ctx: ToolbarContext) => target.open({ openMode });
|
: (_ctx: ToolbarContext) => target.open({ openMode });
|
||||||
|
|
||||||
return { disabled, when, run };
|
return { disabled, when, run };
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
})
|
}
|
||||||
|
)
|
||||||
.filter(action => {
|
.filter(action => {
|
||||||
if (typeof action.when === 'function') return action.when(ctx);
|
if (typeof action.when === 'function') return action.when(ctx);
|
||||||
return action.when ?? true;
|
return action.when ?? true;
|
||||||
@@ -713,58 +716,20 @@ function renderOpenDocMenu(
|
|||||||
}));
|
}));
|
||||||
if (!actions.length) return null;
|
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(
|
return html`${keyed(
|
||||||
target,
|
target,
|
||||||
html`
|
html`
|
||||||
<editor-icon-button
|
<affine-open-doc-dropdown-menu
|
||||||
aria-label="${currentAction.label}"
|
.actions=${actions}
|
||||||
.tooltip="${currentAction.label}"
|
.context=${ctx}
|
||||||
@click=${() => currentAction.run?.(ctx)}
|
.openDocMode$=${computed(
|
||||||
|
() =>
|
||||||
|
settings.settingSignal.value.openDocMode ?? 'open-in-active-view'
|
||||||
|
)}
|
||||||
|
.updateOpenDocMode=${(mode: OpenDocMode) =>
|
||||||
|
settings.openDocMode.set(mode)}
|
||||||
>
|
>
|
||||||
${currentAction.icon} <span class="label">Open</span>
|
</affine-open-doc-dropdown-menu>
|
||||||
</editor-icon-button>
|
|
||||||
<editor-menu-button
|
|
||||||
aria-label="Open doc menu"
|
|
||||||
.contentPadding="${'8px'}"
|
|
||||||
.button=${html`
|
|
||||||
<editor-icon-button
|
|
||||||
aria-label="Open doc"
|
|
||||||
.tooltip="${'Open doc'}"
|
|
||||||
.iconContainerPadding="${'4'}"
|
|
||||||
>
|
|
||||||
${EditorChevronDown}
|
|
||||||
</editor-icon-button>
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<div data-size="small" data-orientation="vertical">
|
|
||||||
${repeat(
|
|
||||||
actions,
|
|
||||||
action => action.id,
|
|
||||||
({ label, icon, run, disabled }) => html`
|
|
||||||
<editor-menu-action
|
|
||||||
aria-label=${ifDefined(label)}
|
|
||||||
?disabled=${ifDefined(disabled)}
|
|
||||||
@click=${() => {
|
|
||||||
run?.(ctx);
|
|
||||||
settings.openDocMode.set(
|
|
||||||
openDocActions.find(a => a.icon === icon)?.type ??
|
|
||||||
'open-in-active-view'
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
${icon}<span class="label">${label}</span>
|
|
||||||
</editor-menu-action>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</editor-menu-button>
|
|
||||||
`
|
`
|
||||||
)}`;
|
)}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ import {
|
|||||||
SplitViewIcon,
|
SplitViewIcon,
|
||||||
} from '@blocksuite/icons/lit';
|
} from '@blocksuite/icons/lit';
|
||||||
|
|
||||||
type OpenDocAction = OpenDocConfigItem & { enabled: boolean };
|
type OpenDocAction = OpenDocConfigItem & {
|
||||||
|
enabled: boolean;
|
||||||
|
shortcut?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const openDocActions: Array<OpenDocAction> = [
|
export const openDocActions: Array<OpenDocAction> = [
|
||||||
{
|
{
|
||||||
@@ -25,18 +28,21 @@ export const openDocActions: Array<OpenDocAction> = [
|
|||||||
type: 'open-in-new-view',
|
type: 'open-in-new-view',
|
||||||
label: I18n['com.affine.peek-view-controls.open-doc-in-split-view'](),
|
label: I18n['com.affine.peek-view-controls.open-doc-in-split-view'](),
|
||||||
icon: SplitViewIcon(),
|
icon: SplitViewIcon(),
|
||||||
|
shortcut: '⌘ ⌥ + click',
|
||||||
enabled: BUILD_CONFIG.isElectron,
|
enabled: BUILD_CONFIG.isElectron,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'open-in-new-tab',
|
type: 'open-in-new-tab',
|
||||||
label: I18n['com.affine.peek-view-controls.open-doc-in-new-tab'](),
|
label: I18n['com.affine.peek-view-controls.open-doc-in-new-tab'](),
|
||||||
icon: OpenInNewIcon(),
|
icon: OpenInNewIcon(),
|
||||||
|
shortcut: '⌘ + click',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'open-in-center-peek',
|
type: 'open-in-center-peek',
|
||||||
label: I18n['com.affine.peek-view-controls.open-doc-in-center-peek'](),
|
label: I18n['com.affine.peek-view-controls.open-doc-in-center-peek'](),
|
||||||
icon: CenterPeekIcon(),
|
icon: CenterPeekIcon(),
|
||||||
|
shortcut: '⇧ + click',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
].filter(
|
].filter(
|
||||||
|
|||||||
@@ -1057,16 +1057,11 @@ test('should save open doc mode of internal links', async ({ page }) => {
|
|||||||
const inlineLink = page.locator('affine-reference');
|
const inlineLink = page.locator('affine-reference');
|
||||||
await inlineLink.hover();
|
await inlineLink.hover();
|
||||||
|
|
||||||
const recentOpenModeBtn = toolbar.getByLabel(/^Open/).nth(0);
|
const openDocBtn = toolbar.getByLabel(/^Open doc$/);
|
||||||
await expect(recentOpenModeBtn).toHaveAttribute(
|
await expect(openDocBtn).toHaveAttribute(
|
||||||
'aria-label',
|
'data-open-doc-mode',
|
||||||
'Open this doc'
|
'Open this doc'
|
||||||
);
|
);
|
||||||
await expect(
|
|
||||||
recentOpenModeBtn.locator('span.label:has-text("Open")')
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
const openDocBtn = toolbar.getByLabel(/^Open doc$/);
|
|
||||||
await openDocBtn.click();
|
await openDocBtn.click();
|
||||||
|
|
||||||
const openDocMenu = toolbar.getByLabel('Open doc menu');
|
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 inlineLink.hover();
|
||||||
|
|
||||||
await expect(toolbar).toBeVisible();
|
await expect(toolbar).toBeVisible();
|
||||||
await expect(recentOpenModeBtn).toHaveAttribute(
|
await expect(openDocBtn).toHaveAttribute(
|
||||||
'aria-label',
|
'data-open-doc-mode',
|
||||||
'Open in center peek'
|
'Open in center peek'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1098,8 +1093,8 @@ test('should save open doc mode of internal links', async ({ page }) => {
|
|||||||
await cardViewBtn.click();
|
await cardViewBtn.click();
|
||||||
|
|
||||||
await expect(toolbar).toBeVisible();
|
await expect(toolbar).toBeVisible();
|
||||||
await expect(recentOpenModeBtn).toHaveAttribute(
|
await expect(openDocBtn).toHaveAttribute(
|
||||||
'aria-label',
|
'data-open-doc-mode',
|
||||||
'Open in center peek'
|
'Open in center peek'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1107,8 +1102,8 @@ test('should save open doc mode of internal links', async ({ page }) => {
|
|||||||
await embedViewBtn.click();
|
await embedViewBtn.click();
|
||||||
|
|
||||||
await expect(toolbar).toBeVisible();
|
await expect(toolbar).toBeVisible();
|
||||||
await expect(recentOpenModeBtn).toHaveAttribute(
|
await expect(openDocBtn).toHaveAttribute(
|
||||||
'aria-label',
|
'data-open-doc-mode',
|
||||||
'Open in center peek'
|
'Open in center peek'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1120,8 +1115,8 @@ test('should save open doc mode of internal links', async ({ page }) => {
|
|||||||
await page.waitForTimeout(250);
|
await page.waitForTimeout(250);
|
||||||
|
|
||||||
await expect(toolbar).toBeVisible();
|
await expect(toolbar).toBeVisible();
|
||||||
await expect(recentOpenModeBtn).toHaveAttribute(
|
await expect(openDocBtn).toHaveAttribute(
|
||||||
'aria-label',
|
'data-open-doc-mode',
|
||||||
'Open in center peek'
|
'Open in center peek'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user