From 3384b9acd0c4c361844236e08b16ece3dbfe5b50 Mon Sep 17 00:00:00 2001 From: lucaronin Date: Fri, 1 May 2026 12:15:14 +0200 Subject: [PATCH] fix: make editor left block handle draggable --- docs/ABSTRACTIONS.md | 2 +- .../tolariaBlockNoteSideMenu.test.tsx | 16 +++++++++++--- src/components/tolariaBlockNoteSideMenu.tsx | 9 +++++++- tests/smoke/editor-block-reorder.spec.ts | 21 ++++++++++++++++--- 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/docs/ABSTRACTIONS.md b/docs/ABSTRACTIONS.md index 8d53d280..bacf4161 100644 --- a/docs/ABSTRACTIONS.md +++ b/docs/ABSTRACTIONS.md @@ -556,7 +556,7 @@ Defined in `src/components/tolariaEditorFormatting.tsx` and `src/components/tola - Tolaria's formatting-toolbar controller also keeps file/image actions mounted across the tiny hover gap between an image block and the floating toolbar, and while the toolbar itself is hovered, so image controls remain usable instead of collapsing mid-interaction. - `useImageLightbox` listens for `dblclick` on the rich-editor container and opens `ImageLightbox` only when the event target resolves to a viewable BlockNote image. The target resolver handles media wrappers, ignores image captions/resize controls, missing sources, and tiny tracking-style images, preserving BlockNote's ordinary single-click image selection path. - The `/` slash menu remains the supported path for markdown-safe block transformations such as headings, quotes, and list blocks. Tolaria filters out BlockNote's toggle-heading and toggle-list variants because those do not map cleanly to the markdown note model. -- The block-handle side menu keeps only actions that survive Tolaria's markdown round-trip. Delete and table-header toggles remain available; BlockNote's `Colors` submenu is removed because block colors are not part of Tolaria's supported markdown surface. +- The block-handle side menu keeps only actions that survive Tolaria's markdown round-trip. Delete and table-header toggles remain available; BlockNote's `Colors` submenu is removed because block colors are not part of Tolaria's supported markdown surface. Tolaria renders the drag handle before the add-block button so the leftmost block affordance starts a reorder drag on macOS. - `useNoteWikilinkDrop()` is the shared editor-drop abstraction for dragging note rows into either editor mode. It reads the existing note-retargeting drag payload, resolves the vault-relative stem, and inserts a canonical `[[wikilink]]` without hijacking unrelated plain-text drags. - `plainTextPaste.ts` is the shared plain-text paste target registry. Rich BlockNote and raw CodeMirror surfaces register focused insertion targets, while ordinary focused text controls use DOM selection replacement, so the `Cmd+Shift+V` command can preserve caret/selection behavior without each surface inventing its own clipboard reader. - `useTauriDragDropEvent()` owns the shared Tauri window drag/drop subscription and duplicate-unlisten cleanup used by native drop features. diff --git a/src/components/tolariaBlockNoteSideMenu.test.tsx b/src/components/tolariaBlockNoteSideMenu.test.tsx index 9facdb75..1d1d0aa3 100644 --- a/src/components/tolariaBlockNoteSideMenu.test.tsx +++ b/src/components/tolariaBlockNoteSideMenu.test.tsx @@ -6,14 +6,20 @@ import { TolariaSideMenu } from './tolariaBlockNoteSideMenu' let capturedDragHandleMenu: ComponentType | null = null vi.mock('@blocknote/react', () => ({ + AddBlockButton: () => , DragHandleMenu: ({ children }: PropsWithChildren) => (
{children}
), - RemoveBlockItem: ({ children }: PropsWithChildren) =>
{children}
, - SideMenu: ({ dragHandleMenu }: { dragHandleMenu?: ComponentType }) => { + DragHandleButton: ({ dragHandleMenu }: { dragHandleMenu?: ComponentType }) => { capturedDragHandleMenu = dragHandleMenu ?? null - return
+ return ( + + ) }, + RemoveBlockItem: ({ children }: PropsWithChildren) =>
{children}
, + SideMenu: ({ children }: PropsWithChildren) =>
{children}
, TableColumnHeaderItem: ({ children }: PropsWithChildren) =>
{children}
, TableRowHeaderItem: ({ children }: PropsWithChildren) =>
{children}
, useDictionary: () => ({ @@ -32,6 +38,10 @@ describe('TolariaSideMenu', () => { expect(screen.getByTestId('side-menu')).toBeInTheDocument() expect(capturedDragHandleMenu).not.toBeNull() + expect(screen.getAllByRole('button').map((button) => button.textContent)).toEqual([ + 'Drag block', + 'Add block', + ]) const DragHandleMenuComponent = capturedDragHandleMenu! render() diff --git a/src/components/tolariaBlockNoteSideMenu.tsx b/src/components/tolariaBlockNoteSideMenu.tsx index 2c132ecd..568d0796 100644 --- a/src/components/tolariaBlockNoteSideMenu.tsx +++ b/src/components/tolariaBlockNoteSideMenu.tsx @@ -1,5 +1,7 @@ import { + AddBlockButton, DragHandleMenu, + DragHandleButton, RemoveBlockItem, SideMenu, TableColumnHeaderItem, @@ -21,5 +23,10 @@ function TolariaDragHandleMenu() { } export function TolariaSideMenu(props: SideMenuProps) { - return + return ( + + + + + ) } diff --git a/tests/smoke/editor-block-reorder.spec.ts b/tests/smoke/editor-block-reorder.spec.ts index d32420b2..cc72d00a 100644 --- a/tests/smoke/editor-block-reorder.spec.ts +++ b/tests/smoke/editor-block-reorder.spec.ts @@ -19,10 +19,25 @@ async function blockOuterForText(page: Page, text: string): Promise { return textNode.locator('xpath=ancestor::*[contains(concat(" ", normalize-space(@class), " "), " bn-block-outer ")][1]') } -async function visibleDragHandle(page: Page, block: Locator): Promise { +async function visibleLeftBlockHandle(page: Page, block: Locator): Promise { await block.hover() - const handle = page.locator('.bn-side-menu [draggable="true"]').first() + + const buttons = await page.locator('.bn-side-menu button').all() + expect(buttons.length).toBeGreaterThan(0) + + let handle = buttons[0] + let leftEdge = Number.POSITIVE_INFINITY + for (const button of buttons) { + const box = await button.boundingBox() + expect(box).not.toBeNull() + if (box!.x < leftEdge) { + leftEdge = box!.x + handle = button + } + } + await expect(handle).toBeVisible({ timeout: 5_000 }) + await expect(handle).toHaveAttribute('draggable', 'true') return handle } @@ -59,7 +74,7 @@ test('dragging the left block handle reorders editor blocks', async ({ page }) = await expect.poll(async () => editor.textContent()).toMatch(/Alpha Project[\s\S]*This is a test project[\s\S]*Notes/) - const handle = await visibleDragHandle(page, notesHeading) + const handle = await visibleLeftBlockHandle(page, notesHeading) await dragHandleToBlock(page, handle, paragraph) await expect.poll(async () => editor.textContent()).toMatch(/Alpha Project[\s\S]*Notes[\s\S]*This is a test project/)