feat: focus the create page item when query returns no result in at menu (#10060)
fix AF-2191
This commit is contained in:
@@ -45,6 +45,23 @@ export interface LinkedWidgetConfig {
|
|||||||
abortSignal: AbortSignal
|
abortSignal: AbortSignal
|
||||||
) => Promise<LinkedMenuGroup[]> | LinkedMenuGroup[];
|
) => Promise<LinkedMenuGroup[]> | LinkedMenuGroup[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto focused item
|
||||||
|
*
|
||||||
|
* Will be called when the menu is
|
||||||
|
* - opened
|
||||||
|
* - query changed
|
||||||
|
* - menu group or its items changed
|
||||||
|
*
|
||||||
|
* If the return value is not null, no action will be taken.
|
||||||
|
*/
|
||||||
|
autoFocusedItem?: (
|
||||||
|
menus: LinkedMenuGroup[],
|
||||||
|
query: string,
|
||||||
|
editorHost: EditorHost,
|
||||||
|
inlineEditor: AffineInlineEditor
|
||||||
|
) => LinkedMenuItem | null;
|
||||||
|
|
||||||
mobile: {
|
mobile: {
|
||||||
useScreenHeight?: boolean;
|
useScreenHeight?: boolean;
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
throttle,
|
throttle,
|
||||||
WithDisposable,
|
WithDisposable,
|
||||||
} from '@blocksuite/global/utils';
|
} from '@blocksuite/global/utils';
|
||||||
|
import { effect } from '@preact/signals-core';
|
||||||
import { css, html, LitElement, nothing } from 'lit';
|
import { css, html, LitElement, nothing } from 'lit';
|
||||||
import { property, query, queryAll, state } from 'lit/decorators.js';
|
import { property, query, queryAll, state } from 'lit/decorators.js';
|
||||||
import { styleMap } from 'lit/directives/style-map.js';
|
import { styleMap } from 'lit/directives/style-map.js';
|
||||||
@@ -47,6 +48,8 @@ export class LinkedDocPopover extends SignalWatcher(
|
|||||||
|
|
||||||
private readonly _expanded = new Map<string, boolean>();
|
private readonly _expanded = new Map<string, boolean>();
|
||||||
|
|
||||||
|
private _menusItemsEffectCleanup: () => void = () => {};
|
||||||
|
|
||||||
private readonly _updateLinkedDocGroup = async () => {
|
private readonly _updateLinkedDocGroup = async () => {
|
||||||
const query = this._query;
|
const query = this._query;
|
||||||
if (this._updateLinkedDocGroupAbortController) {
|
if (this._updateLinkedDocGroupAbortController) {
|
||||||
@@ -65,6 +68,30 @@ export class LinkedDocPopover extends SignalWatcher(
|
|||||||
this.context.inlineEditor,
|
this.context.inlineEditor,
|
||||||
this._updateLinkedDocGroupAbortController.signal
|
this._updateLinkedDocGroupAbortController.signal
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this._menusItemsEffectCleanup();
|
||||||
|
|
||||||
|
// need to rebind the effect because this._linkedDocGroup has changed.
|
||||||
|
this._menusItemsEffectCleanup = effect(() => {
|
||||||
|
this._updateAutoFocusedItem();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly _updateAutoFocusedItem = () => {
|
||||||
|
if (!this._query) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const autoFocusedItem = this.context.config.autoFocusedItem?.(
|
||||||
|
this._linkedDocGroup,
|
||||||
|
this._query,
|
||||||
|
this.context.std.host,
|
||||||
|
this.context.inlineEditor
|
||||||
|
);
|
||||||
|
if (autoFocusedItem) {
|
||||||
|
this._activatedItemIndex = this._flattenActionList.findIndex(
|
||||||
|
item => item.key === autoFocusedItem.key
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private _updateLinkedDocGroupAbortController: AbortController | null = null;
|
private _updateLinkedDocGroupAbortController: AbortController | null = null;
|
||||||
@@ -217,6 +244,11 @@ export class LinkedDocPopover extends SignalWatcher(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override disconnectedCallback() {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this._menusItemsEffectCleanup();
|
||||||
|
}
|
||||||
|
|
||||||
override render() {
|
override render() {
|
||||||
const MAX_HEIGHT = 380;
|
const MAX_HEIGHT = 380;
|
||||||
const style = this._position
|
const style = this._position
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
NewXxxEdgelessIcon,
|
NewXxxEdgelessIcon,
|
||||||
NewXxxPageIcon,
|
NewXxxPageIcon,
|
||||||
} from '@blocksuite/icons/lit';
|
} from '@blocksuite/icons/lit';
|
||||||
import { computed } from '@preact/signals-core';
|
import { computed, Signal } from '@preact/signals-core';
|
||||||
import { Service } from '@toeverything/infra';
|
import { Service } from '@toeverything/infra';
|
||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { html } from 'lit';
|
import { html } from 'lit';
|
||||||
@@ -28,6 +28,10 @@ import type { DocSearchMenuService } from '../../doc-search-menu/services';
|
|||||||
import type { EditorSettingService } from '../../editor-setting';
|
import type { EditorSettingService } from '../../editor-setting';
|
||||||
import { type JournalService, suggestJournalDate } from '../../journal';
|
import { type JournalService, suggestJournalDate } from '../../journal';
|
||||||
|
|
||||||
|
function resolveSignal<T>(data: T | Signal<T>): T {
|
||||||
|
return data instanceof Signal ? data.value : data;
|
||||||
|
}
|
||||||
|
|
||||||
export class AtMenuConfigService extends Service {
|
export class AtMenuConfigService extends Service {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly journalService: JournalService,
|
private readonly journalService: JournalService,
|
||||||
@@ -46,6 +50,7 @@ export class AtMenuConfigService extends Service {
|
|||||||
return {
|
return {
|
||||||
getMenus: this.getMenusFn(),
|
getMenus: this.getMenusFn(),
|
||||||
mobile: this.getMobileConfig(),
|
mobile: this.getMobileConfig(),
|
||||||
|
autoFocusedItem: this.autoFocusedItem,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +61,22 @@ export class AtMenuConfigService extends Service {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly autoFocusedItem = (
|
||||||
|
menus: LinkedMenuGroup[],
|
||||||
|
query: string
|
||||||
|
): LinkedMenuItem | null => {
|
||||||
|
if (query.trim().length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// if the second group (linkToDocGroup) is EMPTY,
|
||||||
|
// if the query is NOT empty && the second group (linkToDocGroup) is EMPTY,
|
||||||
|
// we will focus on the first item of the third group (create), which is the "New Doc" item.
|
||||||
|
if (resolveSignal(menus[1].items).length === 0) {
|
||||||
|
return resolveSignal(menus[2].items)[0];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
private newDocMenuGroup(
|
private newDocMenuGroup(
|
||||||
query: string,
|
query: string,
|
||||||
close: () => void,
|
close: () => void,
|
||||||
|
|||||||
@@ -473,6 +473,21 @@ test('@ popover with click "select a specific date" should show a date picker',
|
|||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('@ popover can auto focus on the "New Doc" item when query returns no items', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await waitForEmptyEditor(page);
|
||||||
|
await page.keyboard.press('@');
|
||||||
|
await page.keyboard.type('nawowenni');
|
||||||
|
await expect(page.locator('.linked-doc-popover')).toBeVisible();
|
||||||
|
const newDocMenuItem = page
|
||||||
|
.locator('.linked-doc-popover')
|
||||||
|
.locator('[data-id="create-page"]');
|
||||||
|
await expect(newDocMenuItem).toBeVisible();
|
||||||
|
await expect(newDocMenuItem).toHaveAttribute('hover', 'true');
|
||||||
|
});
|
||||||
|
|
||||||
test('linked doc should show markdown preview in the backlink section', async ({
|
test('linked doc should show markdown preview in the backlink section', async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user