From ed802f32bd3b8e27e1b712290842c20cef64e4b6 Mon Sep 17 00:00:00 2001 From: Flrande <50035259+Flrande@users.noreply.github.com> Date: Mon, 25 Nov 2024 14:51:52 +0800 Subject: [PATCH] refactor: extract toggle button in list block (#8795) --- packages/affine/block-list/src/list-block.ts | 112 +++++++++--------- .../affine/block-list/src/list-service.ts | 8 -- packages/affine/block-list/src/styles.ts | 37 ------ packages/affine/components/package.json | 7 +- .../components/src/toggle-button/index.ts | 7 ++ .../src/toggle-button/toggle-button.ts | 82 +++++++++++++ packages/blocks/src/effects.ts | 2 + tests/list.spec.ts | 96 +++++++++------ ...ggle-in-readonly-mode-before-readonly.json | 101 ++++++++++++++++ 9 files changed, 311 insertions(+), 141 deletions(-) create mode 100644 packages/affine/components/src/toggle-button/index.ts create mode 100644 packages/affine/components/src/toggle-button/toggle-button.ts create mode 100644 tests/snapshots/list.spec.ts/can-expand-toggle-in-readonly-mode-before-readonly.json diff --git a/packages/affine/block-list/src/list-block.ts b/packages/affine/block-list/src/list-block.ts index 50affe119503..ada4025fc827 100644 --- a/packages/affine/block-list/src/list-block.ts +++ b/packages/affine/block-list/src/list-block.ts @@ -3,16 +3,13 @@ import type { BaseSelection, BlockComponent } from '@blocksuite/block-std'; import type { InlineRangeProvider } from '@blocksuite/inline'; import { CaptionedBlockComponent } from '@blocksuite/affine-components/caption'; -import { - playCheckAnimation, - toggleDown, - toggleRight, -} from '@blocksuite/affine-components/icons'; +import { playCheckAnimation } from '@blocksuite/affine-components/icons'; import { DefaultInlineManagerExtension, type RichText, } from '@blocksuite/affine-components/rich-text'; import '@blocksuite/affine-shared/commands'; +import { TOGGLE_BUTTON_PARENT_CLASS } from '@blocksuite/affine-components/toggle-button'; import { BLOCK_CHILDREN_CONTAINER_PADDING_LEFT, NOTE_SELECTOR, @@ -23,6 +20,8 @@ import { getInlineRangeProvider } from '@blocksuite/block-std'; import { effect } from '@preact/signals-core'; import { html, nothing, type TemplateResult } from 'lit'; import { query, state } from 'lit/decorators.js'; +import { classMap } from 'lit/directives/class-map.js'; +import { styleMap } from 'lit/directives/style-map.js'; import type { ListBlockService } from './list-service.js'; @@ -42,7 +41,15 @@ export class ListBlockComponent extends CaptionedBlockComponent< e.stopPropagation(); if (this.model.type === 'toggle') { - this._toggleChildren(); + if (this.doc.readonly) { + this._readonlyCollapsed = !this._readonlyCollapsed; + } else { + this.doc.captureSync(); + this.doc.updateBlock(this.model, { + collapsed: !this.model.collapsed, + }); + } + return; } else if (this.model.type === 'todo') { if (this.doc.readonly) return; @@ -97,47 +104,17 @@ export class ListBlockComponent extends CaptionedBlockComponent< }); } - private _toggleChildren() { - if (this.doc.readonly) { - this._isCollapsedWhenReadOnly = !this._isCollapsedWhenReadOnly; - return; - } - const newCollapsedState = !this.model.collapsed; - this._isCollapsedWhenReadOnly = newCollapsedState; - this.doc.captureSync(); - this.doc.updateBlock(this.model, { - collapsed: newCollapsedState, - } as Partial); - } - - private _toggleTemplate(isCollapsed: boolean) { - const noChildren = this.model.children.length === 0; - if (noChildren) return nothing; - - const toggleDownTemplate = html`
- ${toggleDown} -
`; - - const toggleRightTemplate = html`
- ${toggleRight} -
`; - - return isCollapsed ? toggleRightTemplate : toggleDownTemplate; - } - override connectedCallback() { super.connectedCallback(); this._inlineRangeProvider = getInlineRangeProvider(this); - this._isCollapsedWhenReadOnly = this.model.collapsed; + + this.disposables.add( + effect(() => { + const collapsed = this.model.collapsed$.value; + this._readonlyCollapsed = collapsed; + }) + ); this.disposables.add( effect(() => { @@ -164,28 +141,49 @@ export class ListBlockComponent extends CaptionedBlockComponent< override renderBlock(): TemplateResult<1> { const { model, _onClickIcon } = this; const collapsed = this.doc.readonly - ? this._isCollapsedWhenReadOnly - : !!model.collapsed; - const listIcon = getListIcon(model, !collapsed, _onClickIcon); + ? this._readonlyCollapsed + : model.collapsed; - const checked = - this.model.type === 'todo' && this.model.checked - ? 'affine-list--checked' - : ''; + const listIcon = getListIcon(model, !collapsed, _onClickIcon); const children = html`
${this.renderChildren(this.model)}
`; return html`
-
- ${this._toggleTemplate(collapsed)} ${listIcon} +
+ ${this.model.children.length > 0 + ? html` + { + if (this.doc.readonly) { + this._readonlyCollapsed = value; + } else { + this.doc.captureSync(); + this.doc.updateBlock(this.model, { + collapsed: value, + }); + } + }} + > + ` + : nothing} + ${listIcon} this.updateCollapsed(!this.collapsed)} + > + ${toggleDown} +
+ `; + + const toggleRightTemplate = html` +
this.updateCollapsed(!this.collapsed)} + > + ${toggleRight} +
+ `; + + return this.collapsed ? toggleRightTemplate : toggleDownTemplate; + } + + @property({ attribute: false }) + accessor collapsed!: boolean; + + @property({ attribute: false }) + accessor updateCollapsed!: (collapsed: boolean) => void; +} + +declare global { + interface HTMLElementTagNameMap { + 'blocksuite-toggle-button': ToggleButton; + } +} diff --git a/packages/blocks/src/effects.ts b/packages/blocks/src/effects.ts index e461fce55c3e..9649e629b8d2 100644 --- a/packages/blocks/src/effects.ts +++ b/packages/blocks/src/effects.ts @@ -11,6 +11,7 @@ import { effects as componentDatePickerEffects } from '@blocksuite/affine-compon import { effects as componentDragIndicatorEffects } from '@blocksuite/affine-components/drag-indicator'; import { effects as componentPortalEffects } from '@blocksuite/affine-components/portal'; import { effects as componentRichTextEffects } from '@blocksuite/affine-components/rich-text'; +import { effects as componentToggleButtonEffects } from '@blocksuite/affine-components/toggle-button'; import { effects as componentToolbarEffects } from '@blocksuite/affine-components/toolbar'; import { effects as widgetScrollAnchoringEffects } from '@blocksuite/affine-widget-scroll-anchoring/effects'; import { effects as stdEffects } from '@blocksuite/block-std/effects'; @@ -334,6 +335,7 @@ export function effects() { componentRichTextEffects(); componentToolbarEffects(); componentDragIndicatorEffects(); + componentToggleButtonEffects(); widgetScrollAnchoringEffects(); widgetMobileToolbarEffects(); diff --git a/tests/list.spec.ts b/tests/list.spec.ts index 3f26be49bbc0..3a8c3e85ea66 100644 --- a/tests/list.spec.ts +++ b/tests/list.spec.ts @@ -1,4 +1,4 @@ -import { expect, type Locator, type Page } from '@playwright/test'; +import { expect, type Locator } from '@playwright/test'; import { getFormatBar } from 'utils/query.js'; import { @@ -40,8 +40,6 @@ import { } from './utils/asserts.js'; import { test } from './utils/playwright.js'; -const getToggleIcon = (page: Page) => page.locator('.toggle-icon'); - async function isToggleIconVisible(toggleIcon: Locator) { const connected = await toggleIcon.isVisible(); if (!connected) return false; @@ -58,10 +56,6 @@ async function isToggleIconVisible(toggleIcon: Locator) { return isVisible; } -async function assertToggleIconVisible(toggleIcon: Locator, expected = true) { - expect(await isToggleIconVisible(toggleIcon)).toBe(expected); -} - test('add new bulleted list', async ({ page }) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); @@ -658,39 +652,42 @@ test.describe('toggle list', () => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await initThreeLists(page); - const toggleIcon = getToggleIcon(page); + const toggleIcon = page.locator('.toggle-icon'); const prefixes = page.locator('.affine-list-block__prefix'); - const collapsed = page.locator('.affine-list__collapsed'); + const listChildren = page + .locator('[data-block-id="4"] .affine-block-children-container') + .nth(0); const parentPrefix = prefixes.nth(1); expect(await getPageSnapshot(page, true)).toMatchSnapshot( `${testInfo.title}_init.json` ); - await expect(collapsed).toHaveCount(0); await parentPrefix.hover(); await waitNextFrame(page); - await assertToggleIconVisible(toggleIcon); + expect(await isToggleIconVisible(toggleIcon)).toBe(true); + await expect(listChildren).toBeVisible(); await toggleIcon.click(); - await expect(collapsed).toHaveCount(1); + await expect(listChildren).not.toBeVisible(); expect(await getPageSnapshot(page, true)).toMatchSnapshot( `${testInfo.title}_toggle.json` ); // Collapsed toggle icon should be show always await page.mouse.move(0, 0); - await assertToggleIconVisible(toggleIcon); + expect(await isToggleIconVisible(toggleIcon)).toBe(true); + await expect(listChildren).not.toBeVisible(); await toggleIcon.click(); - await expect(collapsed).toHaveCount(0); + await expect(listChildren).toBeVisible(); expect(await getPageSnapshot(page, true)).toMatchSnapshot( `${testInfo.title}_init.json` ); await page.mouse.move(0, 0); await waitNextFrame(page, 200); - await assertToggleIconVisible(toggleIcon, false); + expect(await isToggleIconVisible(toggleIcon)).toBe(false); }); test('indent item should expand toggle', async ({ page }, testInfo) => { @@ -702,15 +699,18 @@ test.describe('toggle list', () => { await pressEnter(page); await type(page, '012'); - const toggleIcon = getToggleIcon(page); - const collapsed = page.locator('.affine-list__collapsed'); + const toggleIcon = page.locator('.toggle-icon'); + const listChildren = page + .locator('[data-block-id="4"] .affine-block-children-container') + .nth(0); expect(await getPageSnapshot(page, true)).toMatchSnapshot( `${testInfo.title}_init.json` ); + await expect(listChildren).toBeVisible(); await toggleIcon.click(); - await expect(collapsed).toHaveCount(1); + await expect(listChildren).not.toBeVisible(); expect(await getPageSnapshot(page, true)).toMatchSnapshot( `${testInfo.title}_toggle.json` @@ -719,7 +719,7 @@ test.describe('toggle list', () => { await focusRichText(page, 3); await pressTab(page); await waitNextFrame(page, 200); - await expect(collapsed).toHaveCount(1); + await expect(listChildren).not.toBeVisible(); expect(await getPageSnapshot(page, true)).toMatchSnapshot( `${testInfo.title}_finial.json` @@ -730,47 +730,66 @@ test.describe('toggle list', () => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await initThreeLists(page); - const toggleIcon = getToggleIcon(page); + const toggleIcon = page.locator('.toggle-icon'); const prefixes = page.locator('.affine-list-block__prefix'); const parentPrefix = prefixes.nth(1); - await assertToggleIconVisible(toggleIcon, false); + expect(await isToggleIconVisible(toggleIcon)).toBe(false); await parentPrefix.hover(); await waitNextFrame(page, 200); - await assertToggleIconVisible(toggleIcon); + expect(await isToggleIconVisible(toggleIcon)).toBe(true); await page.mouse.move(0, 0); await waitNextFrame(page, 300); - await assertToggleIconVisible(toggleIcon, false); + expect(await isToggleIconVisible(toggleIcon)).toBe(false); }); }); test.describe('readonly', () => { - test('can expand toggle in readonly mode', async ({ page }) => { + test('can expand toggle in readonly mode', async ({ page }, testInfo) => { await enterPlaygroundRoom(page); await initEmptyParagraphState(page); await initThreeLists(page); - const toggleIcon = getToggleIcon(page); + const toggleIcon = page.locator('.toggle-icon'); const prefixes = page.locator('.affine-list-block__prefix'); - const collapsed = page.locator('.affine-list__collapsed'); + const listChildren = page + .locator('[data-block-id="4"] .affine-block-children-container') + .nth(0); const parentPrefix = prefixes.nth(1); - await expect(collapsed).toHaveCount(0); await parentPrefix.hover(); - await assertToggleIconVisible(toggleIcon); + await waitNextFrame(page, 200); + expect(await isToggleIconVisible(toggleIcon)).toBe(true); + await expect(listChildren).toBeVisible(); await toggleIcon.click(); - await expect(collapsed).toHaveCount(1); + await expect(listChildren).not.toBeVisible(); + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_before_readonly.json` + ); + + await waitNextFrame(page, 200); await switchReadonly(page); - await assertToggleIconVisible(toggleIcon); + await waitNextFrame(page, 200); + expect(await isToggleIconVisible(toggleIcon)).toBe(true); + + await expect(listChildren).not.toBeVisible(); await toggleIcon.click(); - await expect(collapsed).toHaveCount(0); + await expect(listChildren).toBeVisible(); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_before_readonly.json` + ); await toggleIcon.click(); - await expect(collapsed).toHaveCount(1); + await expect(listChildren).not.toBeVisible(); + + expect(await getPageSnapshot(page, true)).toMatchSnapshot( + `${testInfo.title}_before_readonly.json` + ); }); test('can not modify todo list in readonly mode', async ({ page }) => { @@ -804,19 +823,20 @@ test.describe('readonly', () => { // await switchEditorMode(page); await initThreeLists(page); - const toggleIcon = getToggleIcon(page); - const collapsed = page.locator('.affine-list__collapsed'); - - await expect(collapsed).toHaveCount(0); + const toggleIcon = page.locator('.toggle-icon'); + const listChildren = page + .locator('[data-block-id="5"] .affine-block-children-container') + .nth(0); + await expect(listChildren).toBeVisible(); await toggleIcon.click(); - await expect(collapsed).toHaveCount(1); + await expect(listChildren).not.toBeVisible(); await switchReadonly(page); // trick for render a readonly doc from scratch await switchEditorMode(page); await switchEditorMode(page); - await expect(collapsed).toHaveCount(1); + await expect(listChildren).not.toBeVisible(); }); }); diff --git a/tests/snapshots/list.spec.ts/can-expand-toggle-in-readonly-mode-before-readonly.json b/tests/snapshots/list.spec.ts/can-expand-toggle-in-readonly-mode-before-readonly.json new file mode 100644 index 000000000000..2aadcae07279 --- /dev/null +++ b/tests/snapshots/list.spec.ts/can-expand-toggle-in-readonly-mode-before-readonly.json @@ -0,0 +1,101 @@ +{ + "type": "block", + "id": "0", + "flavour": "affine:page", + "version": 2, + "props": { + "title": { + "$blocksuite:internal:text$": true, + "delta": [] + } + }, + "children": [ + { + "type": "block", + "id": "1", + "flavour": "affine:note", + "version": 1, + "props": { + "xywh": "[0,0,498,92]", + "background": "--affine-note-background-white", + "index": "a0", + "hidden": false, + "displayMode": "both", + "edgeless": { + "style": { + "borderRadius": 8, + "borderSize": 4, + "borderStyle": "none", + "shadowType": "--affine-note-shadow-box" + } + } + }, + "children": [ + { + "type": "block", + "id": "3", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "123" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + }, + { + "type": "block", + "id": "4", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "456" + } + ] + }, + "checked": false, + "collapsed": true, + "order": null + }, + "children": [ + { + "type": "block", + "id": "5", + "flavour": "affine:list", + "version": 1, + "props": { + "type": "bulleted", + "text": { + "$blocksuite:internal:text$": true, + "delta": [ + { + "insert": "789" + } + ] + }, + "checked": false, + "collapsed": false, + "order": null + }, + "children": [] + } + ] + } + ] + } + ] +} \ No newline at end of file