♻️(frontend) change BlockNoteSideMenu to DocsSideMenu

We want to have our own side menu, so we can add more
features to it and display the ui as we want.
The new side menu is called DocsSideMenu
and it will be used in the BlockNoteEditor.
This commit is contained in:
Anthony LC
2026-08-26 11:43:11 +02:00
parent d75431f2c8
commit e1eb6c99de
11 changed files with 197 additions and 43 deletions
@@ -23,12 +23,8 @@ test.describe('Doc Editor', () => {
}) => {
await createDoc(page, 'doc-toolbar', browserName, 1);
await verifyDocName(page, 'doc-toolbar');
const editor = await writeInEditor({ page, text: 'test content' });
await page.waitForTimeout(1500);
await editor
.getByText('test content', {
exact: true,
@@ -106,6 +102,27 @@ test.describe('Doc Editor', () => {
).toBeVisible();
});
test('it checks side menu buttons are displayed', async ({
page,
browserName,
}) => {
await createDoc(page, 'doc-side-menu', browserName, 1);
const { editor } = await openSuggestionMenu({ page, suggestion: 'Table' });
await editor.locator('.tableWrapper').first().hover();
await page.locator('.bn-side-menu > button').last().click();
await expect(page.getByRole('menuitem', { name: 'Colors' })).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'Header row' }),
).toBeVisible();
await expect(
page.getByRole('menuitem', { name: 'Header column' }),
).toBeVisible();
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
});
test('markdown button converts from markdown to the editor syntax json', async ({
page,
browserName,
@@ -0,0 +1,15 @@
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_16114_11249)">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M10.666 0.666748C11.7706 0.666748 12.666 1.56218 12.666 2.66675H13.3327C13.8631 2.66675 14.3717 2.87761 14.7467 3.25269C15.1218 3.62776 15.3327 4.13632 15.3327 4.66675V6.66675C15.3327 7.19718 15.1218 7.70574 14.7467 8.08081C14.3717 8.45588 13.8631 8.66675 13.3327 8.66675H7.99935C7.82254 8.66675 7.65302 8.73704 7.528 8.86206C7.40297 8.98709 7.33268 9.1566 7.33268 9.33342V10.0001C8.06906 10.0001 8.66602 10.597 8.66602 11.3334V14.0001C8.66602 14.7365 8.06906 15.3334 7.33268 15.3334H5.99935C5.26297 15.3334 4.66602 14.7365 4.66602 14.0001V11.3334C4.66602 10.597 5.26297 10.0001 5.99935 10.0001V9.33342C5.99935 8.80298 6.21021 8.29442 6.58529 7.91935C6.96036 7.54428 7.46892 7.33342 7.99935 7.33342H13.3327C13.5095 7.33342 13.679 7.26313 13.804 7.1381C13.9291 7.01308 13.9993 6.84356 13.9993 6.66675V4.66675C13.9993 4.48994 13.9291 4.32042 13.804 4.19539C13.679 4.07037 13.5095 4.00008 13.3327 4.00008H12.666C12.666 5.10465 11.7706 6.00008 10.666 6.00008H2.66602C1.56145 6.00008 0.666016 5.10465 0.666016 4.00008V2.66675C0.666016 1.56218 1.56145 0.666748 2.66602 0.666748H10.666ZM5.99935 14.0001H7.33268V11.3334H5.99935V14.0001ZM2.66602 2.00008C2.29783 2.00008 1.99935 2.29856 1.99935 2.66675V4.00008C1.99935 4.36827 2.29783 4.66675 2.66602 4.66675H10.666C11.0342 4.66675 11.3327 4.36827 11.3327 4.00008V2.66675C11.3327 2.29856 11.0342 2.00008 10.666 2.00008H2.66602Z"
fill="currentColor"
/>
</g>
<defs>
<clipPath id="clip0_16114_11249">
<rect width="16" height="16" fill="white" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,18 @@
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M11.3327 13.3334C11.7009 13.3334 11.9993 13.6319 11.9993 14C11.9993 14.3682 11.7009 14.6667 11.3327 14.6667H4.66602C4.29783 14.6667 3.99935 14.3682 3.99935 14C3.99935 13.6319 4.29783 13.3334 4.66602 13.3334H11.3327Z"
fill="currentColor"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M7.0957 4.66801C7.1711 4.66795 7.2452 4.68769 7.31055 4.7253L10.0215 6.29366C10.0868 6.33154 10.1413 6.38577 10.179 6.45121C10.2168 6.51671 10.2363 6.59111 10.2363 6.66671C10.2363 6.7423 10.2168 6.81671 10.179 6.8822L10.1478 6.92908C10.1135 6.97386 10.0706 7.01196 10.0215 7.04041L7.3112 8.60811L7.26042 8.63285C7.20843 8.65438 7.15234 8.66606 7.0957 8.66606L7.03971 8.66215C7.00235 8.65721 6.9658 8.64732 6.93099 8.63285L6.88086 8.60811C6.83184 8.57976 6.78878 8.5421 6.75456 8.49744L6.72331 8.45056C6.68568 8.38513 6.66588 8.31054 6.66602 8.23507V5.099C6.66588 5.02351 6.68567 4.94894 6.72331 4.8835C6.76091 4.81818 6.81502 4.76378 6.88021 4.72595C6.94555 4.68811 7.02019 4.66809 7.0957 4.66801Z"
fill="currentColor"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M13.3327 1.33337C14.4373 1.33337 15.3327 2.2288 15.3327 3.33337V10C15.3327 11.1046 14.4373 12 13.3327 12H2.66602C1.56145 12 0.666016 11.1046 0.666016 10V3.33337C0.666016 2.2288 1.56145 1.33337 2.66602 1.33337H13.3327ZM2.66602 2.66671C2.29783 2.66671 1.99935 2.96518 1.99935 3.33337V10C1.99935 10.3682 2.29783 10.6667 2.66602 10.6667H13.3327C13.7009 10.6667 13.9993 10.3682 13.9993 10V3.33337C13.9993 2.96518 13.7009 2.66671 13.3327 2.66671H2.66602Z"
fill="currentColor"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,31 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect
x="4"
y="4"
width="16"
height="16"
rx="1.5"
stroke="currentColor"
stroke-width="1.5"
/>
<rect x="5.25" y="5.25" width="6" height="6" rx="1" fill="currentColor" />
<rect x="5.25" y="12.75" width="6" height="6" rx="1" fill="currentColor" />
<rect
x="12.75"
y="5.25"
width="6"
height="6"
rx="1"
stroke="currentColor"
stroke-width="1.25"
/>
<rect
x="12.75"
y="12.75"
width="6"
height="6"
rx="1"
stroke="currentColor"
stroke-width="1.25"
/>
</svg>

After

Width:  |  Height:  |  Size: 622 B

@@ -0,0 +1,7 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="4" y="4" width="16" height="16" rx="1.5" stroke="currentColor" stroke-width="1.5" />
<rect x="5.25" y="5.25" width="6" height="6" rx="1" fill="currentColor" />
<rect x="12.75" y="5.25" width="6" height="6" rx="1" fill="currentColor" />
<rect x="5.25" y="12.75" width="6" height="6" rx="1" stroke="currentColor" stroke-width="1.25" />
<rect x="12.75" y="12.75" width="6" height="6" rx="1" stroke="currentColor" stroke-width="1.25" />
</svg>

After

Width:  |  Height:  |  Size: 532 B

@@ -3,6 +3,7 @@ import { Box, BoxType } from '../Box';
export const HorizontalSeparator = (props: BoxType) => {
return (
<Box
role="separator"
$height="1px"
$width="100%"
$margin={{ vertical: 'base' }}
@@ -51,9 +51,9 @@ import { DocsBlockNoteEditor } from '../types';
import { randomColor, sanitizeColor } from '../utils';
import BlockNoteAI from './AI';
import { BlockNoteSideMenu } from './BlockNoteSideMenu';
import { BlockNoteSuggestionMenu } from './BlockNoteSuggestionMenu';
import { BlockNoteToolbar } from './BlockNoteToolBar/BlockNoteToolbar';
import { DocsSideMenu } from './DocsSideMenu/DocsSideMenu';
import { CalloutBlock, PdfBlock, UploadLoaderBlock } from './custom-blocks';
const AIMenu = BlockNoteAI?.AIMenu;
const AIMenuController = BlockNoteAI?.AIMenuController;
@@ -308,7 +308,7 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
)}
<BlockNoteSuggestionMenu aiAllowed={aiBlockNoteAllowed} />
<BlockNoteToolbar aiAllowed={aiBlockNoteAllowed} />
<BlockNoteSideMenu />
<DocsSideMenu />
{showComments && <FloatingComposerController />}
{showComments && !isCommentSideBarOpen && <FloatingThreadController />}
{threadsSidebarTarget &&
@@ -1,36 +0,0 @@
import {
BlockColorsItem,
DragHandleMenu,
RemoveBlockItem,
SideMenu,
SideMenuController,
TableColumnHeaderItem,
TableRowHeaderItem,
useDictionary,
} from '@blocknote/react';
import { PresentBlockItem } from '@/docs/doc-presenter/components/PresentBlockItem';
const DocsDragHandleMenu = () => {
const dict = useDictionary();
return (
<DragHandleMenu>
<RemoveBlockItem>{dict.drag_handle.delete_menuitem}</RemoveBlockItem>
<PresentBlockItem />
<BlockColorsItem>{dict.drag_handle.colors_menuitem}</BlockColorsItem>
<TableRowHeaderItem>
{dict.drag_handle.header_row_menuitem}
</TableRowHeaderItem>
<TableColumnHeaderItem>
{dict.drag_handle.header_column_menuitem}
</TableColumnHeaderItem>
</DragHandleMenu>
);
};
const DocsSideMenu = () => <SideMenu dragHandleMenu={DocsDragHandleMenu} />;
export const BlockNoteSideMenu = () => (
<SideMenuController sideMenu={DocsSideMenu} />
);
@@ -0,0 +1,72 @@
/**
* Overrides the default DragHandleMenu with a custom implementation for the Docs editor.
* See: https://github.com/TypeCellOS/BlockNote/blob/main/packages/react/src/components/SideMenu/DragHandleMenu/DragHandleMenu.tsx
*/
import {
BlockColorsItem,
DragHandleMenu,
RemoveBlockItem,
SideMenu,
SideMenuController,
TableColumnHeaderItem,
TableRowHeaderItem,
useComponentsContext,
useDictionary,
} from '@blocknote/react';
import { Box, HorizontalSeparator } from '@/components';
import { PresentBlockItem } from '@/docs/doc-presenter/components/PresentBlockItem';
import ColorIcon from '@/icons/color.svg';
import TableHeaderColumnIcon from '@/icons/table-header-column.svg';
import TableHeaderRowIcon from '@/icons/table-header-row.svg';
import TrashIcon from '@/icons/trash.svg';
import { TableHeaderSeparator } from './TableHeaderSeparator';
const DocsDragHandleMenu = () => {
const Components = useComponentsContext();
const dict = useDictionary();
if (Components === undefined) {
return null;
}
return (
<DragHandleMenu>
<BlockColorsItem>
<Box $align="center" $gap="xxs" $direction="row">
<ColorIcon width="16" height="16" aria-hidden="true" />
{dict.drag_handle.colors_menuitem}
</Box>
</BlockColorsItem>
<HorizontalSeparator $margin={{ vertical: '3xs' }} />
<PresentBlockItem />
<TableHeaderSeparator />
<TableRowHeaderItem>
<Box $align="center" $gap="xxs" $direction="row">
<TableHeaderRowIcon width="16" height="16" aria-hidden="true" />
{dict.drag_handle.header_row_menuitem}
</Box>
</TableRowHeaderItem>
<TableColumnHeaderItem>
<Box $align="center" $gap="xxs" $direction="row">
<TableHeaderColumnIcon width="16" height="16" aria-hidden="true" />
{dict.drag_handle.header_column_menuitem}
</Box>
</TableColumnHeaderItem>
<HorizontalSeparator $margin={{ vertical: '3xs' }} />
<RemoveBlockItem>
<Box $align="center" $gap="xxs" $direction="row">
<TrashIcon width="16" height="16" aria-hidden="true" />
{dict.drag_handle.delete_menuitem}
</Box>
</RemoveBlockItem>
</DragHandleMenu>
);
};
export const DocsSideMenu = () => (
<SideMenuController
sideMenu={() => <SideMenu dragHandleMenu={DocsDragHandleMenu} />}
/>
);
@@ -0,0 +1,24 @@
import { SideMenuExtension } from '@blocknote/core/extensions';
import { useBlockNoteEditor, useExtensionState } from '@blocknote/react';
import { HorizontalSeparator } from '@/components';
import type { DocsBlockNoteEditor } from '../../types';
export const TableHeaderSeparator = () => {
const editor: DocsBlockNoteEditor = useBlockNoteEditor();
const block = useExtensionState(SideMenuExtension, {
editor,
selector: (state) => state?.block,
});
if (
block === undefined ||
block.type !== 'table' ||
!editor.settings.tables.headers
) {
return null;
}
return <HorizontalSeparator $margin={{ vertical: '3xs' }} />;
};
@@ -6,7 +6,9 @@ import {
} from '@blocknote/react';
import { useTranslation } from 'react-i18next';
import { Box } from '@/components';
import type { DocsBlockNoteEditor } from '@/docs/doc-editor/types';
import PresentIcon from '@/icons/present.svg';
import { useResponsiveStore } from '@/stores';
import { getContentSlideIndexForBlock } from '../hooks/useSlides';
@@ -44,7 +46,10 @@ export const PresentBlockItem = () => {
openPresenter(contentSlideIndex + 1);
}}
>
{t('Present from here')}
<Box $align="center" $gap="xxs" $direction="row">
<PresentIcon width="16" height="16" aria-hidden="true" />
{t('Present from here')}
</Box>
</Components.Generic.Menu.Item>
);
};