Skip to content

Commit a904e58

Browse files
YousefEDclaude
andcommitted
fix: hide side menu on scroll instead of overflow hacks
Reverts the overflow/positioning workarounds from #2043 and instead hides the side menu when the user scrolls, preventing it from overflowing outside the editor's scroll container. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 38be5fd commit a904e58

4 files changed

Lines changed: 84 additions & 12 deletions

File tree

docs/app/styles.css

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,6 @@ body {
5353
box-shadow: unset !important;
5454
}
5555

56-
.demo {
57-
overflow: none;
58-
}
59-
60-
.demo .bn-container {
61-
position: relative;
62-
}
63-
6456
.demo .bn-container:not(.bn-comment-editor),
6557
.demo .bn-editor {
6658
height: 100%;

packages/core/src/extensions/SideMenu/SideMenu.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -784,5 +784,17 @@ export const SideMenuExtension = createExtension(({ editor }) => {
784784
view!.state!.show = false;
785785
view!.emitUpdate(view!.state!);
786786
},
787+
788+
/**
789+
* Hides the side menu unless it is currently frozen (e.g. the drag
790+
* handle menu is open). Used to dismiss the menu on scroll without
791+
* interfering with open submenus.
792+
*/
793+
hideMenuIfNotFrozen() {
794+
if (!view!.menuFrozen) {
795+
view!.state!.show = false;
796+
view!.emitUpdate(view!.state!);
797+
}
798+
},
787799
} as const;
788800
});

packages/react/src/components/Popovers/GenericPopover.tsx

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
FloatingFocusManager,
44
useDismiss,
55
useFloating,
6+
UseFloatingOptions,
67
useHover,
78
useInteractions,
89
useMergeRefs,
@@ -77,15 +78,48 @@ export function getMountedBoundingClientRectCache(
7778
};
7879
}
7980

81+
/**
82+
* Merges two `whileElementsMounted` handlers into one. Both run when elements
83+
* mount, and both cleanup functions are called on unmount.
84+
*/
85+
function mergeWhileElementsMounted(
86+
a: UseFloatingOptions["whileElementsMounted"],
87+
b: UseFloatingOptions["whileElementsMounted"],
88+
): UseFloatingOptions["whileElementsMounted"] {
89+
if (!a) {
90+
return b;
91+
}
92+
if (!b) {
93+
return a;
94+
}
95+
96+
return (reference, floating, update) => {
97+
const cleanupA = a(reference, floating, update);
98+
const cleanupB = b(reference, floating, update);
99+
return () => {
100+
cleanupA?.();
101+
cleanupB?.();
102+
};
103+
};
104+
}
105+
80106
export const GenericPopover = (
81107
props: FloatingUIOptions & {
82108
reference?: GenericPopoverReference;
83109
children: ReactNode;
84110
},
85111
) => {
112+
const {
113+
whileElementsMounted: _whileElementsMounted,
114+
...restFloatingOptions
115+
} = props.useFloatingOptions ?? {};
116+
86117
const { refs, floatingStyles, context } = useFloating<HTMLDivElement>({
87-
whileElementsMounted: autoUpdate,
88-
...props.useFloatingOptions,
118+
whileElementsMounted: mergeWhileElementsMounted(
119+
autoUpdate,
120+
props.useFloatingOptions?.whileElementsMounted,
121+
),
122+
...restFloatingOptions,
89123
});
90124

91125
const { isMounted, styles } = useTransitionStyles(

packages/react/src/components/SideMenu/SideMenuController.tsx

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { SideMenuExtension } from "@blocknote/core/extensions";
2-
import { FC, useMemo } from "react";
2+
import { autoUpdate, ReferenceElement } from "@floating-ui/react";
3+
import { FC, useCallback, useMemo } from "react";
34

5+
import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js";
46
import { useExtensionState } from "../../hooks/useExtension.js";
57
import { BlockPopover } from "../Popovers/BlockPopover.js";
68
import { FloatingUIOptions } from "../Popovers/FloatingUIOptions.js";
@@ -11,6 +13,7 @@ export const SideMenuController = (props: {
1113
sideMenu?: FC<SideMenuProps>;
1214
floatingUIOptions?: Partial<FloatingUIOptions>;
1315
}) => {
16+
const editor = useBlockNoteEditor();
1417
const state = useExtensionState(SideMenuExtension, {
1518
selector: (state) => {
1619
return state !== undefined
@@ -24,12 +27,43 @@ export const SideMenuController = (props: {
2427

2528
const { show, block } = state || {};
2629

30+
// Hides the side menu on ancestor scroll so it doesn't overflow outside
31+
// the editor's scroll container.
32+
const whileElementsMounted = useCallback(
33+
(
34+
reference: ReferenceElement,
35+
floating: HTMLElement,
36+
_update: () => void,
37+
) => {
38+
let initialized = false;
39+
return autoUpdate(
40+
reference,
41+
floating,
42+
() => {
43+
if (!initialized) {
44+
initialized = true;
45+
return;
46+
}
47+
editor.getExtension(SideMenuExtension)?.hideMenuIfNotFrozen();
48+
},
49+
{
50+
ancestorScroll: true,
51+
ancestorResize: false,
52+
elementResize: false,
53+
layoutShift: false,
54+
},
55+
);
56+
},
57+
[editor],
58+
);
59+
2760
const floatingUIOptions = useMemo<FloatingUIOptions>(
2861
() => ({
2962
...props.floatingUIOptions,
3063
useFloatingOptions: {
3164
open: show,
3265
placement: "left-start",
66+
whileElementsMounted,
3367
...props.floatingUIOptions?.useFloatingOptions,
3468
},
3569
useDismissProps: {
@@ -47,7 +81,7 @@ export const SideMenuController = (props: {
4781
...props.floatingUIOptions?.elementProps,
4882
},
4983
}),
50-
[props.floatingUIOptions, show],
84+
[props.floatingUIOptions, show, whileElementsMounted],
5185
);
5286

5387
const Component = props.sideMenu || SideMenu;

0 commit comments

Comments
 (0)