From 2ca49731675ceaacb8bca0717f00dc1d438724be Mon Sep 17 00:00:00 2001 From: Whitewater Date: Tue, 7 Apr 2026 03:18:15 +0800 Subject: [PATCH] fix(editor): make repeated "Cancel line number" clicks work in code blocks (#14804) Fixes #13555 https://github.com/user-attachments/assets/12e55c21-080c-4c69-9780-893ccad25b45 ## Summary - make the code block More popup reactive to `wrap` and `lineNumber` prop updates - read the latest toggle state at click time so same-menu double toggles do not reuse stale closures - add e2e coverage for wrap and line number toggling twice without closing the More menu ## Bug Reason - the code block More popup was rendered as a static portal, so it stayed open without re-rendering after the first toggle - the `Cancel line number` and `Wrap` menu actions captured render-time state in their click handlers - after the first click updated the model, a second click in the same open menu reused stale state and wrote the same value again, so nothing changed visually ## Testing - yarn workspace @affine-test/blocksuite test e2e/code/crud.spec.ts ## Summary by CodeRabbit * **Bug Fixes** * Menu toggles now read and update the current wrap and line-number states reliably. * **Refactor** * Replaced inline popup rendering with a dedicated more-menu component for the code toolbar. * **Style** * Prevented text selection on menu action elements for smoother interaction. * **Tests** * Added e2e tests for wrap and line-number toggle flows. --- .../code-toolbar/components/code-toolbar.ts | 17 ++---- .../src/code-toolbar/components/more-menu.ts | 52 ++++++++++++++++++ .../blocks/code/src/code-toolbar/config.ts | 7 ++- blocksuite/affine/blocks/code/src/effects.ts | 3 ++ .../components/src/toolbar/menu-button.ts | 1 + tests/blocksuite/e2e/code/crud.spec.ts | 54 +++++++++++++++++++ 6 files changed, 119 insertions(+), 15 deletions(-) create mode 100644 blocksuite/affine/blocks/code/src/code-toolbar/components/more-menu.ts diff --git a/blocksuite/affine/blocks/code/src/code-toolbar/components/code-toolbar.ts b/blocksuite/affine/blocks/code/src/code-toolbar/components/code-toolbar.ts index 4ee4c0869..bfd92ce08 100644 --- a/blocksuite/affine/blocks/code/src/code-toolbar/components/code-toolbar.ts +++ b/blocksuite/affine/blocks/code/src/code-toolbar/components/code-toolbar.ts @@ -11,7 +11,6 @@ import { MoreVerticalIcon } from '@blocksuite/icons/lit'; import { flip, offset } from '@floating-ui/dom'; import { css, html, LitElement } from 'lit'; import { property, query, state } from 'lit/decorators.js'; -import { styleMap } from 'lit/directives/style-map.js'; import type { CodeBlockToolbarContext } from '../context.js'; @@ -82,18 +81,10 @@ export class AffineCodeToolbar extends WithDisposable(LitElement) { createLitPortal({ template: html` - -
- ${renderGroups(this.moreGroups, this.context)} -
-
+ `, // should be greater than block-selection z-index as selection and popover wil share the same stacking context(editor-host) portalStyles: { diff --git a/blocksuite/affine/blocks/code/src/code-toolbar/components/more-menu.ts b/blocksuite/affine/blocks/code/src/code-toolbar/components/more-menu.ts new file mode 100644 index 000000000..a5587ae02 --- /dev/null +++ b/blocksuite/affine/blocks/code/src/code-toolbar/components/more-menu.ts @@ -0,0 +1,52 @@ +import type { MenuItemGroup } from '@blocksuite/affine-components/toolbar'; +import { renderGroups } from '@blocksuite/affine-components/toolbar'; +import { SignalWatcher, WithDisposable } from '@blocksuite/global/lit'; +import { ShadowlessElement } from '@blocksuite/std'; +import { html } from 'lit'; +import { property } from 'lit/decorators.js'; +import { styleMap } from 'lit/directives/style-map.js'; + +import type { CodeBlockToolbarContext } from '../context.js'; + +export class AffineCodeMoreMenu extends SignalWatcher( + WithDisposable(ShadowlessElement) +) { + override firstUpdated() { + this.disposables.add( + this.context.blockComponent.model.propsUpdated.subscribe(({ key }) => { + if (key === 'wrap' || key === 'lineNumber') { + this.requestUpdate(); + } + }) + ); + } + + override render() { + return html` + +
+ ${renderGroups(this.moreGroups, this.context)} +
+
+ `; + } + + @property({ attribute: false }) + accessor context!: CodeBlockToolbarContext; + + @property({ attribute: false }) + accessor moreGroups!: MenuItemGroup[]; +} + +declare global { + interface HTMLElementTagNameMap { + 'affine-code-more-menu': AffineCodeMoreMenu; + } +} diff --git a/blocksuite/affine/blocks/code/src/code-toolbar/config.ts b/blocksuite/affine/blocks/code/src/code-toolbar/config.ts index b6e4cfa26..e6bcdd1a2 100644 --- a/blocksuite/affine/blocks/code/src/code-toolbar/config.ts +++ b/blocksuite/affine/blocks/code/src/code-toolbar/config.ts @@ -174,7 +174,8 @@ export const toggleGroup: MenuItemGroup = { return html` { - blockComponent.setWrap(!wrapped); + const currentWrap = blockComponent.model.props.wrap; + blockComponent.setWrap(!currentWrap); }} aria-label=${label} > @@ -204,8 +205,10 @@ export const toggleGroup: MenuItemGroup = { return html` { + const currentLineNumber = + blockComponent.model.props.lineNumber ?? true; blockComponent.store.updateBlock(blockComponent.model, { - lineNumber: !lineNumber, + lineNumber: !currentLineNumber, }); }} aria-label=${label} diff --git a/blocksuite/affine/blocks/code/src/effects.ts b/blocksuite/affine/blocks/code/src/effects.ts index 403131c34..31af358c7 100644 --- a/blocksuite/affine/blocks/code/src/effects.ts +++ b/blocksuite/affine/blocks/code/src/effects.ts @@ -5,12 +5,14 @@ import { } from './code-toolbar'; import { AffineCodeToolbar } from './code-toolbar/components/code-toolbar'; import { LanguageListButton } from './code-toolbar/components/lang-button'; +import { AffineCodeMoreMenu } from './code-toolbar/components/more-menu'; import { PreviewButton } from './code-toolbar/components/preview-button'; import { AffineCodeUnit } from './highlight/affine-code-unit'; export function effects() { customElements.define('language-list-button', LanguageListButton); customElements.define('affine-code-toolbar', AffineCodeToolbar); + customElements.define('affine-code-more-menu', AffineCodeMoreMenu); customElements.define(AFFINE_CODE_TOOLBAR_WIDGET, AffineCodeToolbarWidget); customElements.define('affine-code-unit', AffineCodeUnit); customElements.define('affine-code', CodeBlockComponent); @@ -21,6 +23,7 @@ declare global { interface HTMLElementTagNameMap { 'language-list-button': LanguageListButton; 'affine-code-toolbar': AffineCodeToolbar; + 'affine-code-more-menu': AffineCodeMoreMenu; 'preview-button': PreviewButton; [AFFINE_CODE_TOOLBAR_WIDGET]: AffineCodeToolbarWidget; } diff --git a/blocksuite/affine/components/src/toolbar/menu-button.ts b/blocksuite/affine/components/src/toolbar/menu-button.ts index 314ea2250..2ccdbc3b3 100644 --- a/blocksuite/affine/components/src/toolbar/menu-button.ts +++ b/blocksuite/affine/components/src/toolbar/menu-button.ts @@ -187,6 +187,7 @@ export class EditorMenuAction extends LitElement { color: var(--affine-text-primary-color); font-weight: 400; min-height: 30px; // 22 + 8 + user-select: none; } :host(:hover), diff --git a/tests/blocksuite/e2e/code/crud.spec.ts b/tests/blocksuite/e2e/code/crud.spec.ts index a73ee2e82..e0b24a25f 100644 --- a/tests/blocksuite/e2e/code/crud.spec.ts +++ b/tests/blocksuite/e2e/code/crud.spec.ts @@ -286,6 +286,32 @@ test('toggle code block wrap can work', async ({ page }, testInfo) => { ); }); +test('toggle code block wrap can work in the same more menu', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyCodeBlockState(page); + + const codeBlockController = getCodeBlock(page); + const codeBlockContainer = page.locator( + 'affine-code .affine-code-block-container' + ); + + await codeBlockController.codeBlock.hover(); + const moreMenu = await codeBlockController.openMore(); + + await moreMenu.wrapButton.click(); + + await expect(moreMenu.menu).toBeVisible(); + await expect(codeBlockContainer).toHaveClass(/wrap/); + await expect(moreMenu.cancelWrapButton).toBeVisible(); + + await moreMenu.cancelWrapButton.click(); + + await expect(moreMenu.menu).toBeVisible(); + await expect(codeBlockContainer).not.toHaveClass(/wrap/); +}); + test('add caption works', async ({ page }, testInfo) => { await enterPlaygroundRoom(page); await initEmptyCodeBlockState(page); @@ -355,6 +381,34 @@ test('toggle code block line number can work', async ({ page }) => { await expect(lineNumber).toBeVisible(); }); +test('toggle code block line number can work in the same more menu', async ({ + page, +}) => { + await enterPlaygroundRoom(page); + await initEmptyCodeBlockState(page); + await focusRichText(page); + + const lineNumber = page.locator('affine-code .line-number'); + + await expect(lineNumber).toBeVisible(); + + const codeBlockController = getCodeBlock(page); + + await codeBlockController.codeBlock.hover(); + const moreMenu = await codeBlockController.openMore(); + + await moreMenu.cancelLineNumberButton.click(); + + await expect(moreMenu.menu).toBeVisible(); + await expect(lineNumber).toBeHidden(); + await expect(moreMenu.lineNumberButton).toBeVisible(); + + await moreMenu.lineNumberButton.click(); + + await expect(moreMenu.menu).toBeVisible(); + await expect(lineNumber).toBeVisible(); +}); + test('code block toolbar widget can appear and disappear during mousemove', async ({ page, }) => {