Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion packages/react/src/components/SideMenu/SideMenuController.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { SideMenuExtension } from "@blocknote/core/extensions";
import { autoUpdate, ReferenceElement } from "@floating-ui/react";
import {
autoUpdate,
offset,
OffsetOptions,
ReferenceElement,
} from "@floating-ui/react";
import { FC, useCallback, useMemo } from "react";

import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js";
Expand All @@ -9,6 +14,36 @@ import { FloatingUIOptions } from "../Popovers/FloatingUIOptions.js";
import { SideMenu } from "./SideMenu.js";
import { SideMenuProps } from "./SideMenuProps.js";

/**
* Pass to Floating UI's `offset` middleware to keep the side menu aligned
* with table blocks:
* middleware: [offset(tableWrapperOffset), ...other]
*
* Compensates for the top padding on `.tableWrapper` (the in-block container
* that reserves space for row/column handles) — without this, the side menu
* floats above the visible top of the table.
*
* Assumes `placement: "left-start"`. Other placements map `crossAxis` to a
* different visual axis.
*/
export const tableWrapperOffset: OffsetOptions = (state) => {
const { reference } = state.elements;
const refEl =
reference instanceof Element ? reference : reference.contextElement;
// The side menu's reference is the block's outer `.bn-block` element.
// For tables, the `.tableWrapper` lives one level deeper inside
// `.bn-block-content`. Match that exact path so unrelated descendants
// (e.g. a nested table inside a multi-column block) don't trigger.
const wrapper = refEl?.querySelector(
":scope > .bn-block-content > .tableWrapper",
);
if (!wrapper) {
return 0;
}
const padding = parseFloat(getComputedStyle(wrapper).paddingTop);
return padding > 0 ? { mainAxis: 0, crossAxis: padding } : 0;
};

export const SideMenuController = (props: {
sideMenu?: FC<SideMenuProps>;
floatingUIOptions?: Partial<FloatingUIOptions>;
Expand Down Expand Up @@ -67,6 +102,10 @@ export const SideMenuController = (props: {
placement: "left-start",
whileElementsMounted,
...props.floatingUIOptions?.useFloatingOptions,
middleware: [
offset(tableWrapperOffset),
...(props.floatingUIOptions?.useFloatingOptions?.middleware ?? []),
],
},
useDismissProps: {
enabled: false,
Expand Down
15 changes: 15 additions & 0 deletions tests/src/end-to-end/draghandle/draghandle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
H_TWO_BLOCK_SELECTOR,
PARAGRAPH_SELECTOR,
SLASH_MENU_SELECTOR,
TABLE_SELECTOR,
} from "../../utils/const.js";
import { insertHeading } from "../../utils/copypaste.js";
import {
Expand Down Expand Up @@ -45,6 +46,20 @@ test.describe("Check Draghandle functionality", () => {
await page.waitForSelector(DRAG_HANDLE_SELECTOR);
});

test("Draghandle should align vertically with the top of a table block", async () => {
await executeSlashCommand(page, "table");
await page.waitForSelector(TABLE_SELECTOR);

const handleY = await getDragHandleYCoord(page, TABLE_SELECTOR);
const tableY = (await page.locator(`${TABLE_SELECTOR} table`).boundingBox())!.y;

// Regression test for #2604: prior to the fix the drag handle floated
// ~8px above the visible top of the table because it anchored to the
// bn-block-content wrapper, ignoring the tableWrapper's top padding (the
// space reserved for in-block row/column handles).
expect(Math.abs(handleY - tableY)).toBeLessThanOrEqual(2);
});

test("Draghandle should display next to correct block", async () => {
await insertHeading(page, 1);
await insertHeading(page, 2);
Expand Down