-
-
Notifications
You must be signed in to change notification settings - Fork 721
fix: action buttons of left column in toggle block hover (#2526) #2671
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -47,13 +47,18 @@ function getBlockFromCoords( | |||||||||||||||||||||
| continue; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| if (adjustForColumns) { | ||||||||||||||||||||||
| const column = element.closest("[data-node-type=columnList]"); | ||||||||||||||||||||||
| const column = element.closest("[data-node-type=column]"); | ||||||||||||||||||||||
| if (column) { | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const columnRect = column.getBoundingClientRect(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return getBlockFromCoords( | ||||||||||||||||||||||
| view, | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| // TODO can we do better than this? | ||||||||||||||||||||||
| left: coords.left + 50, // bit hacky, but if we're inside a column, offset x position to right to account for the width of sidemenu itself | ||||||||||||||||||||||
| left: Math.min( | ||||||||||||||||||||||
| Math.max(columnRect.left + 10, coords.left + 20), | ||||||||||||||||||||||
| columnRect.right - 10, | ||||||||||||||||||||||
| ), | ||||||||||||||||||||||
| top: coords.top, | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| false, | ||||||||||||||||||||||
|
|
@@ -101,6 +106,21 @@ function getBlockFromMousePos( | |||||||||||||||||||||
| return undefined; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const column = referenceBlock.node.closest("[data-node-type=column]"); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (column) { | ||||||||||||||||||||||
| const columnRect = column.getBoundingClientRect(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return getBlockFromCoords( | ||||||||||||||||||||||
| view, | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| left: Math.min(columnRect.left + 20, columnRect.right - 10), | ||||||||||||||||||||||
| top: mousePos.y, | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| false, | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /** | ||||||||||||||||||||||
| * Because blocks may be nested, we need to check the right edge of the parent block: | ||||||||||||||||||||||
| * ``` | ||||||||||||||||||||||
|
|
@@ -250,7 +270,7 @@ export class SideMenuView< | |||||||||||||||||||||
| // padding. This is a little weird since this child element will | ||||||||||||||||||||||
| // be the first block, but since it's always non-nested and we | ||||||||||||||||||||||
| // only take the x coordinate, it's ok. | ||||||||||||||||||||||
| column.firstElementChild!.getBoundingClientRect().x | ||||||||||||||||||||||
| column.getBoundingClientRect().x | ||||||||||||||||||||||
| : ( | ||||||||||||||||||||||
| this.pmView.dom.firstChild as HTMLElement | ||||||||||||||||||||||
| ).getBoundingClientRect().x, | ||||||||||||||||||||||
|
|
@@ -603,6 +623,12 @@ export class SideMenuView< | |||||||||||||||||||||
|
|
||||||||||||||||||||||
| this.mousePos = { x: event.clientX, y: event.clientY }; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const target = event.target as HTMLElement | null; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (target?.closest(".bn-side-menu")) { | ||||||||||||||||||||||
| return; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
Comment on lines
+626
to
+630
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: find . -name "SideMenu.ts" -type f | head -20Repository: TypeCellOS/BlockNote Length of output: 115 🏁 Script executed: cat -n ./packages/core/src/extensions/SideMenu/SideMenu.ts | sed -n '610,640p'Repository: TypeCellOS/BlockNote Length of output: 1208 🌐 Web query:
💡 Result: In the DOM, for a MouseEvent (e.g., from a 'click' handler), event.target is an EventTarget referencing the innermost element that dispatched the event, typically an Element like an input or button. Since Element.closest(selectors) is a standard method that traverses up the DOM tree to find the nearest ancestor (or self) matching the CSS selectors, you can safely call event.target.closest(selectors) after ensuring it's an Element. This pattern is commonly used for event delegation: attach a single listener to a parent, then use event.target.closest to identify the relevant child element. Example: document.addEventListener('click', function(event) { const button = (event.target as Element).closest('button.special'); if (button) { // Handle click on special button or its children } }); TypeScript requires casting event.target to Element or HTMLElement since EventTarget lacks closest. MouseEvent generics specify the expected target type (e.g., MouseEvent). No changes or deprecations as of 2026 per MDN docs. Citations:
🌐 Web query:
💡 Result: The .closest method throws a TypeError when called on a Text node or Comment node because it is defined only on the Element interface, not on Node. Text nodes (nodeType 3) and Comment nodes (nodeType 8) are not Elements, so they do not have the closest method. To find the closest ancestor element matching a selector from a Text or Comment node, first access its parent Element using node.parentElement (or node.parentNode if you need to check the type), then call closest on that Element. Example: const textNode = someElement.firstChild; // Text node if (textNode.nodeType === Node.TEXT_NODE) { const closestDiv = textNode.parentElement.closest('div'); } This is a current DOM specification behavior as of 2026; an open WHATWG issue proposes extending closest to more node types like Text, but it remains Element-only. Citations:
Narrow The cast to Suggested fix- const target = event.target as HTMLElement | null;
+ const target = event.target;
- if (target?.closest(".bn-side-menu")) {
- return;
+ if (target instanceof Element && target.closest(".bn-side-menu")) {
+ return;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // We want the full area of the editor to check if the cursor is hovering | ||||||||||||||||||||||
| // above it though. | ||||||||||||||||||||||
| const editorOuterBoundingBox = this.pmView.dom.getBoundingClientRect(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,6 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { Extension } from "@tiptap/core"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { Fragment, Node } from "prosemirror-model"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { TextSelection } from "prosemirror-state"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { AllSelection, TextSelection } from "prosemirror-state"; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||||
| getBottomNestedBlockInfo, | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -953,6 +953,17 @@ export const KeyboardShortcutsExtension = Extension.create<{ | |||||||||||||||||||||||||||||||||||||||||||||||
| "Mod-z": () => this.options.editor.undo(), | ||||||||||||||||||||||||||||||||||||||||||||||||
| "Mod-y": () => this.options.editor.redo(), | ||||||||||||||||||||||||||||||||||||||||||||||||
| "Shift-Mod-z": () => this.options.editor.redo(), | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Forces AllSelection from pos 0 to include non-editable blocks (e.g. images) that | ||||||||||||||||||||||||||||||||||||||||||||||||
| // TextSelection would skip. | ||||||||||||||||||||||||||||||||||||||||||||||||
| "Mod-a": () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const { doc } = this.options.editor.prosemirrorState; | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.options.editor.prosemirrorView?.dispatch( | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.options.editor.prosemirrorState.tr.setSelection( | ||||||||||||||||||||||||||||||||||||||||||||||||
| new AllSelection(doc) | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+956
to
+966
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
♻️ Suggested change- "Mod-a": () => {
- const { doc } = this.options.editor.prosemirrorState;
- this.options.editor.prosemirrorView?.dispatch(
- this.options.editor.prosemirrorState.tr.setSelection(
- new AllSelection(doc)
- )
- );
- return true;
- },
+ "Mod-a": () => {
+ const view = this.options.editor.prosemirrorView;
+ if (!view) {
+ return false;
+ }
+ view.dispatch(
+ view.state.tr.setSelection(new AllSelection(view.state.doc)),
+ );
+ return true;
+ },📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -181,4 +181,18 @@ test.describe("Check Draghandle functionality", () => { | |
|
|
||
| await compareDocToSnapshot(page, "draghandlenesteddelete"); | ||
| }); | ||
|
|
||
| test("Hovering over column nested in toggle block should show draghandle", async () => { | ||
| await executeSlashCommand(page, "togglelist"); | ||
| await page.keyboard.type("Toggle heading"); | ||
| await page.keyboard.press("Enter"); | ||
| await executeSlashCommand(page, "two"); | ||
| await page.keyboard.type("Left column content"); | ||
| const leftColumn = await page.locator(".bn-block-column").first(); | ||
| await moveMouseOverElement(page, leftColumn); | ||
| await page.waitForSelector(DRAG_HANDLE_SELECTOR); | ||
| await page.click(DRAG_HANDLE_SELECTOR); | ||
| await page.waitForSelector(DRAG_HANDLE_MENU_SELECTOR); | ||
| }); | ||
|
Comment on lines
+185
to
+196
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make the regression test prove the column is inside the toggle. Right now the test selects the first column globally, so it can still pass if Suggested test hardening- const leftColumn = await page.locator(".bn-block-column").first();
+ const leftColumn = page
+ .locator('[data-node-type*="toggle" i] [data-node-type="column"]')
+ .first();
+ await expect(leftColumn).toBeVisible();
await moveMouseOverElement(page, leftColumn);🤖 Prompt for AI Agents |
||
|
|
||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Preserve nested-block resolution inside columns.
This early return skips the nested-block correction below Line 124. For indented/nested blocks inside a column, probing at
columnRect.left + 20can resolve the parent block instead of the nested block under the cursor.Suggested fix
if (column) { const columnRect = column.getBoundingClientRect(); + const referenceBlocksBoundingBox = + referenceBlock.node.getBoundingClientRect(); return getBlockFromCoords( view, { - left: Math.min(columnRect.left + 20, columnRect.right - 10), + left: Math.min( + Math.max(referenceBlocksBoundingBox.right - 10, columnRect.left + 10), + columnRect.right - 10, + ), top: mousePos.y, }, false,🤖 Prompt for AI Agents