mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-16 17:46:18 +08:00
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 { 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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 { 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<ToolbarActionGenerator>(action => {
|
||||
const openMode = action.mode;
|
||||
const shouldOpenInCenterPeek = openMode === 'open-in-center-peek';
|
||||
const shouldOpenInActiveView = openMode === 'open-in-active-view';
|
||||
.map<ToolbarActionGenerator & { mode: OpenDocMode; shortcut?: string }>(
|
||||
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`
|
||||
<editor-icon-button
|
||||
aria-label="${currentAction.label}"
|
||||
.tooltip="${currentAction.label}"
|
||||
@click=${() => currentAction.run?.(ctx)}
|
||||
<affine-open-doc-dropdown-menu
|
||||
.actions=${actions}
|
||||
.context=${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>
|
||||
</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>
|
||||
</affine-open-doc-dropdown-menu>
|
||||
`
|
||||
)}`;
|
||||
}
|
||||
|
||||
@@ -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<OpenDocAction> = [
|
||||
{
|
||||
@@ -25,18 +28,21 @@ export const openDocActions: Array<OpenDocAction> = [
|
||||
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(
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user