♻️(frontend) adapt side menu with Blocknote 0.54.0

Blocknote changed the way the side menu is positioned.
Because we are modifying the height of the heading blocks
we need to adjust the offset of the side menu to match
the new height of the heading blocks.
To do so we have to override the default offset of
the side menu with a custom offset that takes into
account the new height of the heading blocks.
This commit is contained in:
Anthony LC
2026-09-03 13:30:49 +02:00
parent 7c106e33a8
commit ceae767825
6 changed files with 196 additions and 105 deletions
+1
View File
@@ -39,6 +39,7 @@
"@dnd-kit/modifiers": "9.0.0",
"@emoji-mart/data": "1.2.1",
"@emoji-mart/react": "1.1.1",
"@floating-ui/react": "^0.27.19",
"@fontsource-variable/inter": "5.3.0",
"@fontsource-variable/material-symbols-outlined": "5.3.3",
"@fontsource/material-icons": "5.3.0",
@@ -0,0 +1,66 @@
/**
* 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,
TableColumnHeaderItem,
TableRowHeaderItem,
useComponentsContext,
useDictionary,
} from '@blocknote/react';
import { Box } 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 { LinkToBlockItem } from './LinkToBlockItem';
import { SideMenuSeparator } from './SideMenuSeparator';
export 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>
<SideMenuSeparator typeHide="codeBlock" />
<LinkToBlockItem />
<PresentBlockItem />
<SideMenuSeparator typeDisplay="table" />
<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>
<SideMenuSeparator />
<RemoveBlockItem>
<Box $align="center" $gap="xxs" $direction="row">
<TrashIcon width="16" height="16" aria-hidden="true" />
{dict.drag_handle.delete_menuitem}
</Box>
</RemoveBlockItem>
</DragHandleMenu>
);
};
@@ -1,74 +1,105 @@
/**
* 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
* Overrides the default SideMenu with a custom implementation for the Docs editor.
* See: https://github.com/TypeCellOS/BlockNote/blob/main/packages/react/src/components/SideMenu/SideMenu.tsx
*/
import type { Block } from '@blocknote/core';
import { SideMenuExtension } from '@blocknote/core/extensions';
import {
BlockColorsItem,
DragHandleMenu,
RemoveBlockItem,
SideMenu,
SideMenuController,
TableColumnHeaderItem,
TableRowHeaderItem,
useComponentsContext,
useDictionary,
useBlockNoteEditor,
useExtensionState,
} from '@blocknote/react';
import { offset } from '@floating-ui/react';
import { useMemo } from '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 type {
DocsBlockNoteEditor,
DocsBlockSchema,
DocsInlineContentSchema,
DocsStyleSchema,
} from '../../types';
import { LinkToBlockItem } from './LinkToBlockItem';
import { TableHeaderSeparator } from './TableHeaderSeparator';
import { DocsDragHandleMenu } from './DocsDragHandleMenu';
const DocsDragHandleMenu = () => {
const Components = useComponentsContext();
const dict = useDictionary();
if (Components === undefined) {
return null;
/**
* Blocknote v0.54.0
* See: https://github.com/TypeCellOS/BlockNote/blob/main/packages/react/src/components/SideMenu/SideMenuController.tsx#L21
*
* Blocknote changed the way the side menu is positioned.
* Because we are modifying the height of the heading blocks we need to adjust the offset of the side menu to match the
* new height of the heading blocks.
*
* @todo Try to see with Blocknote if we can have a better way to handle this, maybe a prop to set the offset of the side menu.
*/
function getBlockOffset(
editor: DocsBlockNoteEditor,
block: Block<DocsBlockSchema, DocsInlineContentSchema, DocsStyleSchema>,
): number {
if (block.type === 'heading') {
switch (block.props.level) {
case 1:
return 13;
case 2:
return 7;
case 3:
return 4;
default:
return 0;
}
}
// File blocks without a URL all render the same "Add file" button,
// regardless of their type.
if (
editor.schema.blockSpecs[block.type]?.implementation.meta
?.fileBlockAccept &&
(!('url' in block.props) || !block.props.url)
) {
return 12;
}
if (block.type === 'file') {
return 4;
}
if (block.type === 'audio' || block.type === 'table') {
return 15;
}
return 0;
}
export const DocsSideMenu = () => {
const editor = useBlockNoteEditor<
DocsBlockSchema,
DocsInlineContentSchema,
DocsStyleSchema
>();
const block = useExtensionState(SideMenuExtension, {
selector: (state) =>
state?.block as Block<
DocsBlockSchema,
DocsInlineContentSchema,
DocsStyleSchema
> | null,
});
const floatingUIOptions = useMemo(
() => ({
useFloatingOptions: {
middleware: [
offset({ crossAxis: block ? getBlockOffset(editor, block) : 0 }),
],
},
}),
[editor, block],
);
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' }} />
<LinkToBlockItem />
<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>
<SideMenuController
floatingUIOptions={floatingUIOptions}
sideMenu={() => <SideMenu dragHandleMenu={DocsDragHandleMenu} />}
/>
);
};
export const DocsSideMenu = () => (
<SideMenuController
sideMenu={() => <SideMenu dragHandleMenu={DocsDragHandleMenu} />}
/>
);
@@ -0,0 +1,38 @@
import { SideMenuExtension } from '@blocknote/core/extensions';
import { useBlockNoteEditor, useExtensionState } from '@blocknote/react';
import { type BoxType, HorizontalSeparator } from '@/components';
import type { DocsBlockNoteEditor } from '../../types';
type SideMenuSeparatorProps = {
typeDisplay?: 'table';
typeHide?: 'codeBlock';
} & BoxType;
export const SideMenuSeparator = ({
typeDisplay,
typeHide,
...props
}: SideMenuSeparatorProps) => {
const editor: DocsBlockNoteEditor = useBlockNoteEditor();
const block = useExtensionState(SideMenuExtension, {
editor,
selector: (state) => state?.block,
});
if (
typeDisplay &&
(block === undefined ||
block.type !== typeDisplay ||
!editor.settings.tables.headers)
) {
return null;
}
if (typeHide && (block === undefined || block.type === typeHide)) {
return null;
}
return <HorizontalSeparator $margin={{ vertical: '3xs' }} {...props} />;
};
@@ -1,24 +0,0 @@
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' }} />;
};
@@ -192,9 +192,6 @@ export const DocsEditorStyle = createGlobalStyle`
var(--c--contextuals--border--semantic--brand--tertiary);
background: var(--c--contextuals--border--semantic--brand--secondary);
}
.bn-side-menu[data-block-type='divider'] {
height: 28px;
}
/**
* Checklist items
@@ -251,15 +248,6 @@ export const DocsEditorStyle = createGlobalStyle`
--level: 0.875rem;
}
}
.bn-side-menu[data-block-type='heading'][data-level='1'] {
height: 54px;
}
.bn-side-menu[data-block-type='heading'][data-level='2'] {
height: 43px;
}
.bn-side-menu[data-block-type='heading'][data-level='3'] {
height: 35px;
}
& .bn-default-styles h1 {
font-size: 1.875rem;
}
@@ -309,15 +297,6 @@ export const DocsEditorStyle = createGlobalStyle`
& .bn-editor {
padding-right: 10px;
}
.bn-side-menu[data-block-type='heading'][data-level='1'] {
height: 46px;
}
.bn-side-menu[data-block-type='heading'][data-level='2'] {
height: 40px;
}
.bn-side-menu[data-block-type='heading'][data-level='3'] {
height: 40px;
}
[data-content-type='heading'] {
--level: 1.6rem;
&[data-level='2'] {