From a2e3d318ba5ed7bfe3d9fcfc2250ad7aa4ff6dca Mon Sep 17 00:00:00 2001 From: L-Sun Date: Tue, 25 Mar 2025 03:48:12 +0000 Subject: [PATCH] refactor(editor): adjust ui of surface-ref inner toolbar (#11129) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close [BS-2803](https://linear.app/affine-design/issue/BS-2803/inserted-frame-ui%E8%B0%83%E6%95%B4) Close [BS-2815](https://linear.app/affine-design/issue/BS-2815/inserted-group-ui调整) ### What Changes - Add an inner toolbar for hovered `surface-ref-block` - Simplify viewport related codes of `surface-ref-block` - Expose popover floating options from `affine-menu-button` https://github.com/user-attachments/assets/916b0a22-6271-4a6f-b338-6630e0426967 --- .../src/configs/slash-menu.ts | 9 +- .../blocks/block-surface-ref/src/index.ts | 1 - .../src/surface-ref-block.ts | 353 +++++---------- .../block-surface-ref/src/widgets/config.ts | 5 +- .../src/widgets/surface-ref-toolbar.ts | 422 ++++++++++-------- .../components/src/toolbar/menu-button.ts | 31 +- .../affine/shared/src/utils/button-popper.ts | 5 +- tests/affine-local/e2e/peek-view.spec.ts | 8 +- 8 files changed, 382 insertions(+), 452 deletions(-) diff --git a/blocksuite/affine/blocks/block-surface-ref/src/configs/slash-menu.ts b/blocksuite/affine/blocks/block-surface-ref/src/configs/slash-menu.ts index 58c2eda2a2..823034fdaa 100644 --- a/blocksuite/affine/blocks/block-surface-ref/src/configs/slash-menu.ts +++ b/blocksuite/affine/blocks/block-surface-ref/src/configs/slash-menu.ts @@ -1,6 +1,6 @@ import { EdgelessFrameManagerIdentifier } from '@blocksuite/affine-block-frame'; import { EdgelessCRUDExtension } from '@blocksuite/affine-block-surface'; -import { MindmapStyle } from '@blocksuite/affine-model'; +import { MindmapStyle, SurfaceRefBlockSchema } from '@blocksuite/affine-model'; import { type SlashMenuActionItem, type SlashMenuConfig, @@ -20,8 +20,7 @@ const surfaceRefSlashMenuConfig: SlashMenuConfig = { const crud = std.get(EdgelessCRUDExtension); const frameMgr = std.get(EdgelessFrameManagerIdentifier); - const findSpace = (bound: Bound) => { - const padding = 20; + const findSpace = (bound: Bound, padding = 20) => { const gfx = std.get(GfxControllerIdentifier); let elementInFrameBound = gfx.grid.search(bound); while (elementInFrameBound.length > 0) { @@ -78,7 +77,7 @@ const surfaceRefSlashMenuConfig: SlashMenuConfig = { }, group: `5_Edgeless Element@${index++}`, action: () => { - const bound = findSpace(Bound.fromXYWH([0, 0, 200, 200])); + const bound = findSpace(Bound.fromXYWH([0, 0, 200, 200]), 150); const { x, y, h } = bound; const rootW = 145; @@ -160,6 +159,6 @@ const surfaceRefSlashMenuConfig: SlashMenuConfig = { }; export const SurfaceRefSlashMenuConfigExtension = SlashMenuConfigExtension( - 'affine:surface-ref', + SurfaceRefBlockSchema.model.flavour, surfaceRefSlashMenuConfig ); diff --git a/blocksuite/affine/blocks/block-surface-ref/src/index.ts b/blocksuite/affine/blocks/block-surface-ref/src/index.ts index eb4c6cf7b3..8d3b074229 100644 --- a/blocksuite/affine/blocks/block-surface-ref/src/index.ts +++ b/blocksuite/affine/blocks/block-surface-ref/src/index.ts @@ -2,4 +2,3 @@ export * from './commands.js'; export * from './surface-ref-block.js'; export * from './surface-ref-block-edgeless.js'; export * from './surface-ref-spec.js'; -export * from './utils.js'; diff --git a/blocksuite/affine/blocks/block-surface-ref/src/surface-ref-block.ts b/blocksuite/affine/blocks/block-surface-ref/src/surface-ref-block.ts index 025b8302e7..4d87314561 100644 --- a/blocksuite/affine/blocks/block-surface-ref/src/surface-ref-block.ts +++ b/blocksuite/affine/blocks/block-surface-ref/src/surface-ref-block.ts @@ -1,14 +1,12 @@ +import { type FrameBlockComponent } from '@blocksuite/affine-block-frame'; import { EdgelessCRUDIdentifier, getSurfaceBlock, - type SurfaceBlockModel, - SurfaceElementModel, } from '@blocksuite/affine-block-surface'; import type { BlockCaptionEditor } from '@blocksuite/affine-components/caption'; import { Peekable } from '@blocksuite/affine-components/peek'; import { FrameBlockModel, - RootBlockModel, type SurfaceRefBlockModel, } from '@blocksuite/affine-model'; import { @@ -17,8 +15,8 @@ import { ThemeProvider, ViewportElementExtension, } from '@blocksuite/affine-shared/services'; +import { unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme'; import { - matchModels, requestConnectedFrame, SpecProvider, } from '@blocksuite/affine-shared/utils'; @@ -43,20 +41,18 @@ import { deserializeXYWH, type SerializedXYWH, } from '@blocksuite/global/gfx'; -import { DeleteIcon, EdgelessIcon, FrameIcon } from '@blocksuite/icons/lit'; +import { assertType } from '@blocksuite/global/utils'; +import { DeleteIcon } from '@blocksuite/icons/lit'; import type { BaseSelection, Store } from '@blocksuite/store'; -import { css, html, nothing, type TemplateResult } from 'lit'; +import { effect, signal } from '@preact/signals-core'; +import { css, html, nothing } from 'lit'; import { query, state } from 'lit/decorators.js'; +import { classMap } from 'lit/directives/class-map.js'; +import { guard } from 'lit/directives/guard.js'; import { styleMap } from 'lit/directives/style-map.js'; import { noContentPlaceholder } from './utils.js'; -const iconSize = { width: '20px', height: '20px' }; -const REF_LABEL_ICON = { - 'affine:frame': FrameIcon(iconSize), - DEFAULT_NOTE_HEIGHT: EdgelessIcon(iconSize), -} as Record; - const NO_CONTENT_TITLE = { 'affine:frame': 'Frame', group: 'Group', @@ -71,11 +67,28 @@ const NO_CONTENT_REASON = { @Peekable() export class SurfaceRefBlockComponent extends BlockComponent { static override styles = css` + affine-surface-ref { + position: relative; + } + + affine-surface-ref:not(:hover) + affine-surface-ref-toolbar:not([data-open-menu-display='show']) { + display: none; + } + .affine-surface-ref { position: relative; user-select: none; margin: 10px 0; break-inside: avoid; + border-radius: 8px; + border: 1px solid ${unsafeCSSVarV2('edgeless/frame/border/default')}; + background-color: ${unsafeCSSVarV2('layer/background/primary')}; + overflow: hidden; + } + + .affine-surface-ref.focused { + border-color: ${unsafeCSSVarV2('edgeless/frame/border/active')}; } @media print { @@ -150,7 +163,6 @@ export class SurfaceRefBlockComponent extends BlockComponent svg { - color: var(--affine-icon-secondary); - display: inline-block; - vertical-align: baseline; - width: 20px; - height: 20px; - vertical-align: bottom; - } - - .ref-label .suffix { - display: inline-block; - font-weight: 400; - color: var(--affine-text-disable-color); - line-height: 20px; - } `; private _previewDoc: Store | null = null; @@ -245,9 +188,7 @@ export class SurfaceRefBlockComponent extends BlockComponent(null); private get _shouldRender() { return ( @@ -312,55 +253,31 @@ export class SurfaceRefBlockComponent extends BlockComponent { if (!this.model.props.reference) return [null, this.doc.id]; + const referenceId = this.model.props.reference; - if (this.doc.getBlock(this.model.props.reference)) { - return [ - this.doc.getBlock(this.model.props.reference) - ?.model as GfxBlockElementModel, - this.doc.id, - ]; - } - - if (this._surfaceModel?.getElementById(this.model.props.reference)) { - return [ - this._surfaceModel.getElementById(this.model.props.reference), - this.doc.id, - ]; - } - - const doc = [...this.std.workspace.docs.values()] - .map(doc => doc.getStore()) - .find( - doc => - doc.getBlock(this.model.props.reference) || - getSurfaceBlock(doc)?.getElementById(this.model.props.reference) - ); - - if (doc) { - this._surfaceModel = getSurfaceBlock(doc); - } - - if (doc && doc.getBlock(this.model.props.reference)) { - return [ - doc.getBlock(this.model.props.reference) - ?.model as GfxBlockElementModel, - doc.id, - ]; - } - - if (doc) { - const surfaceBlock = getSurfaceBlock(doc); - if (surfaceBlock) { - return [ - surfaceBlock.getElementById(this.model.props.reference), - doc.id, - ]; + const find = (doc: Store): [GfxModel | null, string] => { + const block = doc.getBlock(referenceId)?.model; + if (block instanceof GfxBlockElementModel) { + return [block, doc.id]; } + const surfaceBlock = getSurfaceBlock(doc); + if (!surfaceBlock) return [null, doc.id]; + + const element = surfaceBlock.getElementById(referenceId); + if (element) return [element, doc.id]; + + return [null, doc.id]; + }; + + // find current doc first + let result = find(this.doc); + if (result[0]) return result; + + for (const doc of this.std.workspace.docs.values()) { + result = find(doc.getStore()); + if (result[0]) return result; } return [null, this.doc.id]; @@ -371,10 +288,10 @@ export class SurfaceRefBlockComponent extends BlockComponent { + this._referencedModel.surface.elementRemoved.subscribe(({ id }) => { if (this.model.props.reference === id) { init(); } @@ -422,45 +339,37 @@ export class SurfaceRefBlockComponent extends BlockComponent { + if (!this._referenceXYWH$.value) return; + const previewEditorHost = this.previewEditor; + if (!previewEditorHost) return; + const gfx = previewEditorHost.std.get(GfxControllerIdentifier); + const viewport = gfx.viewport; - override mounted() { - const disposable = this.std.view.viewUpdated.subscribe(payload => { - if (payload.type !== 'block') return; - if ( - payload.method === 'add' && - matchModels(payload.view.model, [RootBlockModel]) - ) { - disposable.unsubscribe(); - queueMicrotask(() => refreshViewport()); - const gfx = this.std.get(GfxControllerIdentifier); - gfx.viewport.sizeUpdated.subscribe(() => { - refreshViewport(); - }); - } - }); - } - } - this._previewSpec.extend([SurfaceRefViewportInitializer]); + let bound = Bound.deserialize(this._referenceXYWH$.value); + const w = Math.max(this.getBoundingClientRect().width, bound.w); + const aspectRatio = bound.w / bound.h; + const h = w / aspectRatio; + + bound = Bound.fromCenter(bound.center, w, h); + + viewport.setViewportByBound(bound); + }; + this.disposables.add(effect(refreshViewport)); const referenceId = this.model.props.reference; - const setReferenceXYWH = (xywh: typeof this._referenceXYWH) => { - this._referenceXYWH = xywh; - }; - - class FrameGroupViewWatcher extends LifeCycleWatcher { - static override readonly key = 'surface-ref-group-view-watcher'; + const referenceXYWH$ = this._referenceXYWH$; + class SurfaceRefViewportWatcher extends LifeCycleWatcher { + static override readonly key = 'surface-ref-viewport-watcher'; private readonly _disposable = new DisposableGroup(); override mounted() { const crud = this.std.get(EdgelessCRUDIdentifier); - const { _disposable } = this; - const surfaceModel = getSurfaceBlock(this.std.store); - if (!surfaceModel) return; + const gfx = this.std.get(GfxControllerIdentifier); + const { surface, viewport } = gfx; + if (!surface) return; const referenceElement = crud.getElementById(referenceId); if (!referenceElement) { @@ -469,23 +378,39 @@ export class SurfaceRefBlockComponent extends BlockComponent { - setReferenceXYWH(xywh); - refreshViewport(); + referenceXYWH$.value = xywh; }) ); + const subscription = this.std.view.viewUpdated.subscribe( + ({ id, type, method, view }) => { + if ( + id === referenceElement.id && + type === 'block' && + method === 'add' + ) { + assertType(view); + view.showBorder = false; + subscription.unsubscribe(); + } + } + ); + _disposable.add(subscription); } else if (referenceElement instanceof GfxPrimitiveElementModel) { _disposable.add( - surfaceModel.elementUpdated.subscribe(({ id, oldValues }) => { + surface.elementUpdated.subscribe(({ id, oldValues }) => { if ( id === referenceId && oldValues.xywh !== referenceElement.xywh ) { - setReferenceXYWH(referenceElement.xywh); - refreshViewport(); + referenceXYWH$.value = referenceElement.xywh; } }) ); @@ -497,69 +422,29 @@ export class SurfaceRefBlockComponent extends BlockComponent -
-
- ${REF_LABEL_ICON[flavourOrType ?? 'DEFAULT'] ?? - REF_LABEL_ICON.DEFAULT} - ${title} -
-
from edgeless mode
-
- - `; + this._previewSpec.extend([SurfaceRefViewportWatcher]); } private _renderRefContent(referencedModel: GfxModel) { const [, , w, h] = deserializeXYWH(referencedModel.xywh); - const flavourOrType = - 'flavour' in referencedModel - ? referencedModel.flavour - : referencedModel.type; const _previewSpec = this._previewSpec.value; - if (!this._viewportEditor) { - if (this._previewDoc) { - this._viewportEditor = new BlockStdScope({ - store: this._previewDoc, - extensions: _previewSpec, - }).render(); - } else { - console.error('Preview doc is not found'); - } - } - return html`
- ${this._viewportEditor} + ${guard(this._previewDoc, () => { + return this._previewDoc + ? new BlockStdScope({ + store: this._previewDoc, + extensions: _previewSpec, + }).render() + : nothing; + })}
- ${this._renderMask(referencedModel, flavourOrType)}
`; } @@ -592,7 +477,7 @@ export class SurfaceRefBlockComponent extends BlockComponent ${content} @@ -629,10 +511,10 @@ export class SurfaceRefBlockComponent extends BlockComponent): void { - if (_changedProperties.has('_referencedModel')) { - this._refreshViewport(); - } - } - @state() private accessor _focused: boolean = false; - @state() - private accessor _surfaceModel: SurfaceBlockModel | null = null; - @query('affine-surface-ref > block-caption-editor') accessor captionElement!: BlockCaptionEditor; diff --git a/blocksuite/affine/blocks/block-surface-ref/src/widgets/config.ts b/blocksuite/affine/blocks/block-surface-ref/src/widgets/config.ts index 3cd19762bd..80767a34c5 100644 --- a/blocksuite/affine/blocks/block-surface-ref/src/widgets/config.ts +++ b/blocksuite/affine/blocks/block-surface-ref/src/widgets/config.ts @@ -1,4 +1,4 @@ -import type { SurfaceBlockComponent } from '@blocksuite/affine-block-surface'; +import { type SurfaceBlockComponent } from '@blocksuite/affine-block-surface'; import { CopyIcon, DeleteIcon, @@ -19,7 +19,7 @@ export const BUILT_IN_GROUPS: MenuItemGroup[] = [ items: [ { type: 'copy', - label: 'Copy', + label: 'Copy as Image', icon: CopyIcon, action: ctx => { if (!(ctx.blockComponent.referenceModel && ctx.doc.root?.id)) { @@ -56,6 +56,7 @@ export const BUILT_IN_GROUPS: MenuItemGroup[] = [ ctx.close(); }, }, + // TODO(@L-Sun): add duplicate action after refactoring toolbar { type: 'download', label: 'Download', diff --git a/blocksuite/affine/blocks/block-surface-ref/src/widgets/surface-ref-toolbar.ts b/blocksuite/affine/blocks/block-surface-ref/src/widgets/surface-ref-toolbar.ts index 2b92146309..f14189afc8 100644 --- a/blocksuite/affine/blocks/block-surface-ref/src/widgets/surface-ref-toolbar.ts +++ b/blocksuite/affine/blocks/block-surface-ref/src/widgets/surface-ref-toolbar.ts @@ -1,33 +1,44 @@ import type { SurfaceRefBlockComponent } from '@blocksuite/affine-block-surface-ref'; -import { HoverController } from '@blocksuite/affine-components/hover'; -import { CaptionIcon, OpenIcon } from '@blocksuite/affine-components/icons'; -import { isPeekable, peek } from '@blocksuite/affine-components/peek'; +import { peek } from '@blocksuite/affine-components/peek'; +import { toast } from '@blocksuite/affine-components/toast'; import { cloneGroups, getMoreMenuConfig, type MenuItem, type MenuItemGroup, renderGroups, - renderToolbarSeparator, } from '@blocksuite/affine-components/toolbar'; -import type { SurfaceRefBlockModel } from '@blocksuite/affine-model'; -import { PAGE_HEADER_HEIGHT } from '@blocksuite/affine-shared/consts'; import { - BlockSelection, - TextSelection, - WidgetComponent, -} from '@blocksuite/block-std'; + FrameBlockModel, + GroupElementModel, + MindmapElementModel, + ShapeElementModel, + type SurfaceRefBlockModel, +} from '@blocksuite/affine-model'; +import { + copySelectedModelsCommand, + draftSelectedModelsCommand, +} from '@blocksuite/affine-shared/commands'; +import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine-shared/theme'; +import type { ButtonPopperOptions } from '@blocksuite/affine-shared/utils'; +import { WidgetComponent } from '@blocksuite/block-std'; import { ArrowDownSmallIcon, + CaptionIcon, CenterPeekIcon, + CopyIcon, EdgelessIcon, + FrameIcon, + GroupIcon, + MindmapIcon, MoreVerticalIcon, + OpenInNewIcon, } from '@blocksuite/icons/lit'; -import { offset, shift } from '@floating-ui/dom'; -import { html, nothing } from 'lit'; +import { css, html, nothing, type TemplateResult } from 'lit'; import { ifDefined } from 'lit/directives/if-defined.js'; -import { join } from 'lit/directives/join.js'; import { repeat } from 'lit/directives/repeat.js'; +import { styleMap } from 'lit/directives/style-map.js'; +import { when } from 'lit/directives/when.js'; import { BUILT_IN_GROUPS } from './config.js'; import { SurfaceRefToolbarContext } from './context.js'; @@ -38,6 +49,59 @@ export class AffineSurfaceRefToolbar extends WidgetComponent< SurfaceRefBlockModel, SurfaceRefBlockComponent > { + static override styles = css` + :host { + position: absolute; + top: 0; + left: 0; + + width: 100%; + gap: 4px; + padding: 4px; + margin: 0; + display: flex; + justify-content: flex-end; + + editor-icon-button, + editor-menu-button { + background: ${unsafeCSSVarV2('button/iconButtonSolid')}; + color: ${unsafeCSSVarV2('text/primary')}; + box-shadow: ${unsafeCSSVar('shadow1')}; + border-radius: 4px; + } + } + + .surface-ref-toolbar-title { + display: flex; + padding: 2px 4px; + margin-right: auto; + align-items: center; + gap: 4px; + border-radius: 2px; + background: ${unsafeCSSVarV2('button/iconButtonSolid')}; + + svg { + color: ${unsafeCSSVarV2('icon/primary')}; + width: 16px; + height: 16px; + } + + span { + color: ${unsafeCSSVarV2('text/primary')}; + font-size: 12px; + font-weight: 500; + line-height: 20px; + } + } + `; + + private readonly _popoverOptions: Partial = { + mainAxis: 4, + stateUpdated: ({ display }) => { + this.dataset.openMenuDisplay = display; + }, + }; + /* * Caches the more menu items. * Currently only supports configuring more menu. @@ -45,68 +109,167 @@ export class AffineSurfaceRefToolbar extends WidgetComponent< moreGroups: MenuItemGroup[] = cloneGroups(BUILT_IN_GROUPS); - private readonly _hoverController = new HoverController( - this, - ({ abortController }) => { - const surfaceRefBlock = this.block; - if (!surfaceRefBlock) { - return null; - } - const selection = this.host.selection; - - const textSelection = selection.find(TextSelection); - if ( - !!textSelection && - (!!textSelection.to || !!textSelection.from.length) - ) { - return null; - } - - const blockSelections = selection.filter(BlockSelection); - if ( - blockSelections.length > 1 || - (blockSelections.length === 1 && - blockSelections[0].blockId !== surfaceRefBlock.blockId) - ) { - return null; - } - - return { - template: SurfaceRefToolbarOptions({ - context: new SurfaceRefToolbarContext(this.block, abortController), - groups: this.moreGroups, - }), - computePosition: { - referenceElement: this.block, - placement: 'top-start', - middleware: [ - offset({ - mainAxis: 12, - crossAxis: 10, - }), - shift({ - crossAxis: true, - padding: { - top: PAGE_HEADER_HEIGHT + 12, - bottom: 12, - right: 12, - }, - }), - ], - autoUpdate: true, - }, - }; - } - ); - override connectedCallback() { super.connectedCallback(); - this.moreGroups = getMoreMenuConfig(this.std).configure(this.moreGroups); - if (!this.block) { - return; + + this.disposables.addFromEvent(this, 'dblclick', e => { + e.stopPropagation(); + }); + } + + private _renderTitle() { + if (!this.block) return nothing; + const { referenceModel } = this.block; + if (!referenceModel) return nothing; + + let title = ''; + let icon: TemplateResult<1> | null = null; + if (referenceModel instanceof GroupElementModel) { + title = referenceModel.title.toString(); + icon = GroupIcon(); + } else if (referenceModel instanceof FrameBlockModel) { + title = referenceModel.props.title.toString(); + icon = FrameIcon(); + } else if (referenceModel instanceof MindmapElementModel) { + const rootElement = referenceModel.tree.element; + if (rootElement instanceof ShapeElementModel) { + title = rootElement.text?.toString() ?? ''; + } + icon = MindmapIcon(); } - this._hoverController.setReference(this.block); + + return html`
+ ${icon} + ${title} +
`; + } + + private _renderOpenButton() { + const referenceModel = this.block?.referenceModel; + if (!referenceModel) return nothing; + + const openMenuActions: MenuItem[] = [ + { + type: 'open-in-edgeless', + label: 'Open in Edgeless', + icon: EdgelessIcon(), + action: () => this.block?.viewInEdgeless(), + disabled: this.block.model.doc.readonly, + }, + { + type: 'open-in-center-peek', + label: 'Open in center peek', + icon: CenterPeekIcon(), + action: () => this.block && peek(this.block), + }, + // TODO(@L-Sun): add split view and new tab + ]; + + return html` + ${OpenInNewIcon()} ${ArrowDownSmallIcon()} + + `} + .popperOptions=${this._popoverOptions} + > +
+ ${repeat( + openMenuActions, + button => button.label, + ({ label, icon, action, disabled }) => html` + + ${icon}${label} + + ` + )} +
+
`; + } + + private _moreButton() { + if (!this.block) return nothing; + + const moreMenuActions = renderGroups( + this.moreGroups, + new SurfaceRefToolbarContext(this.block, new AbortController()) + ); + + return html` + ${MoreVerticalIcon()} + + `} + .popperOptions=${this._popoverOptions} + > +
${moreMenuActions}
+
`; + } + + private _renderButtons() { + if (!this.block) return nothing; + const readonly = this.block.model.doc.readonly; + const buttons = [ + this._renderOpenButton(), + when( + !readonly, + () => + html` { + if (!this.block) return; + this.std.command + .chain() + .pipe(draftSelectedModelsCommand, { + selectedModels: [this.block.model], + }) + .pipe(copySelectedModelsCommand) + .run(); + toast(this.block.std.host, 'Copied to clipboard'); + }} + > + ${CopyIcon()} + ` + ), + when( + !readonly, + () => + html` { + if (!this.block) return; + this.block.captionElement.show(); + }} + > + ${CaptionIcon()} + ` + ), + this._moreButton(), + ]; + + return buttons; + } + + override render() { + if (!this.block) return nothing; + return html`${this._renderTitle()} ${this._renderButtons()}`; } } @@ -115,120 +278,3 @@ declare global { [AFFINE_SURFACE_REF_TOOLBAR]: AffineSurfaceRefToolbar; } } - -function SurfaceRefToolbarOptions({ - context, - groups, -}: { - context: SurfaceRefToolbarContext; - groups: MenuItemGroup[]; -}) { - const { blockComponent, abortController } = context; - const readonly = blockComponent.model.doc.readonly; - const hasValidReference = !!blockComponent.referenceModel; - - const openMenuActions: MenuItem[] = []; - - const iconSize = { width: '20px', height: '20px' }; - if (hasValidReference) { - openMenuActions.push({ - type: 'open-in-edgeless', - label: 'Open in edgeless', - icon: EdgelessIcon(iconSize), - action: () => blockComponent.viewInEdgeless(), - disabled: readonly, - }); - - if (isPeekable(blockComponent)) { - openMenuActions.push({ - type: 'open-in-center-peek', - label: 'Open in center peek', - icon: CenterPeekIcon(iconSize), - action: () => peek(blockComponent), - }); - } - } - - const moreMenuActions = renderGroups(groups, context); - - const buttons = [ - openMenuActions.length - ? html` - - ${OpenIcon}${ArrowDownSmallIcon({ - width: '16px', - height: '16px', - })} - - `} - > -
- ${repeat( - openMenuActions, - button => button.label, - ({ label, icon, action, disabled }) => html` - - ${icon}${label} - - ` - )} -
-
- ` - : nothing, - - readonly - ? nothing - : html` - { - abortController.abort(); - blockComponent.captionElement.show(); - }} - > - ${CaptionIcon} - - `, - - html` - - ${MoreVerticalIcon()} - - `} - > -
- ${moreMenuActions} -
-
- `, - ]; - - return html` - - ${join( - buttons.filter(button => button !== nothing), - renderToolbarSeparator - )} - - `; -} diff --git a/blocksuite/affine/components/src/toolbar/menu-button.ts b/blocksuite/affine/components/src/toolbar/menu-button.ts index dd38cde425..076d6efdf2 100644 --- a/blocksuite/affine/components/src/toolbar/menu-button.ts +++ b/blocksuite/affine/components/src/toolbar/menu-button.ts @@ -25,9 +25,10 @@ export class EditorMenuButton extends WithDisposable(LitElement) { } `; - private _popper!: ReturnType; + private _popper: ReturnType | null = null; - override firstUpdated() { + private _updatePopper() { + this._popper?.dispose(); this._popper = createButtonPopper({ reference: this._trigger, popperElement: this._content, @@ -48,16 +49,30 @@ export class EditorMenuButton extends WithDisposable(LitElement) { offsetHeight: 6 * 4, ...this.popperOptions, }); + } + + override willUpdate(changedProperties: PropertyValues) { + if (changedProperties.has('contentPadding')) { + this.style.setProperty('--content-padding', this.contentPadding ?? ''); + } + + if (this.hasUpdated && changedProperties.has('popperOptions')) { + this._updatePopper(); + } + } + + override firstUpdated() { + this._updatePopper(); this._disposables.addFromEvent(this, 'keydown', (e: KeyboardEvent) => { e.stopPropagation(); if (e.key === 'Escape') { - this._popper.hide(); + this._popper?.hide(); } }); this._disposables.addFromEvent(this._trigger, 'click', (_: MouseEvent) => { - this._popper.toggle(); + this._popper?.toggle(); }); - this._disposables.add(this._popper); + this._disposables.add(() => this._popper?.dispose()); } hide() { @@ -77,12 +92,6 @@ export class EditorMenuButton extends WithDisposable(LitElement) { this._popper?.show(force); } - override willUpdate(changedProperties: PropertyValues) { - if (changedProperties.has('contentPadding')) { - this.style.setProperty('--content-padding', this.contentPadding ?? ''); - } - } - @query('editor-menu-content') private accessor _content!: EditorMenuContent; diff --git a/blocksuite/affine/shared/src/utils/button-popper.ts b/blocksuite/affine/shared/src/utils/button-popper.ts index 82d9249260..910e8f6a34 100644 --- a/blocksuite/affine/shared/src/utils/button-popper.ts +++ b/blocksuite/affine/shared/src/utils/button-popper.ts @@ -4,6 +4,7 @@ import { autoUpdate, computePosition, offset, + type Placement, type Rect, shift, size, @@ -39,6 +40,7 @@ export type ButtonPopperOptions = { stateUpdated?: (state: { display: Display }) => void; mainAxis?: number; crossAxis?: number; + allowedPlacements?: Placement[]; rootBoundary?: Rect | (() => Rect | undefined); ignoreShift?: boolean; offsetHeight?: number; @@ -64,6 +66,7 @@ export function createButtonPopper(options: ButtonPopperOptions) { stateUpdated = () => {}, mainAxis, crossAxis, + allowedPlacements = ['top', 'bottom'], rootBoundary, ignoreShift, offsetHeight, @@ -84,7 +87,7 @@ export function createButtonPopper(options: ButtonPopperOptions) { crossAxis: crossAxis ?? 0, }), autoPlacement({ - allowedPlacements: ['top', 'bottom'], + allowedPlacements, ...overflowOptions, }), shift(overflowOptions), diff --git a/tests/affine-local/e2e/peek-view.spec.ts b/tests/affine-local/e2e/peek-view.spec.ts index 683216f1a6..e02df2eaba 100644 --- a/tests/affine-local/e2e/peek-view.spec.ts +++ b/tests/affine-local/e2e/peek-view.spec.ts @@ -132,13 +132,13 @@ test('can open peek view for embedded frames', async ({ page }) => { await page.locator('affine-surface-ref .affine-surface-ref').hover(); await page - .locator('.surface-ref-toolbar-container') - .locator('editor-icon-button[aria-label="Open doc"]') + .locator('affine-surface-ref-toolbar editor-menu-button[aria-label="Open"]') .click(); await page - .locator('.surface-ref-toolbar-container') - .locator('editor-menu-action:has-text("center peek")') + .locator( + 'affine-surface-ref-toolbar editor-menu-action[aria-label="Open in center peek"]' + ) .click(); // verify peek view is opened