Skip to content

Commit c651dc6

Browse files
YousefEDclaude
andauthored
fix: hide side menu on scroll instead of overflow hacks (#2630)
* 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> * comments and check * fix: hide table handles on scroll Apply the same scroll-hide pattern to table handles. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 38be5fd commit c651dc6

File tree

6 files changed

+138
-15
lines changed

6 files changed

+138
-15
lines changed

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 && view!.state!.show) {
795+
view!.state!.show = false;
796+
view!.emitUpdate(view!.state!);
797+
}
798+
},
787799
} as const;
788800
});

packages/core/src/extensions/TableHandles/TableHandles.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -908,6 +908,20 @@ export const TableHandlesExtension = createExtension(({ editor }) => {
908908
view!.menuFrozen = false;
909909
},
910910

911+
/**
912+
* Hides the table handles unless they are currently frozen (e.g. a
913+
* handle menu is open). Used to dismiss the handles on scroll without
914+
* interfering with open submenus.
915+
*/
916+
hideHandlesIfNotFrozen() {
917+
if (!view!.menuFrozen && view!.state?.show) {
918+
view!.state.show = false;
919+
view!.state.showAddOrRemoveRowsButton = false;
920+
view!.state.showAddOrRemoveColumnsButton = false;
921+
view!.emitUpdate();
922+
}
923+
},
924+
911925
getCellsAtRowHandle(
912926
block: BlockFromConfigNoChildren<DefaultBlockSchema["table"], any, any>,
913927
relativeRowIndex: RelativeCellIndices["row"],

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: 38 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,45 @@ 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+
// autoUpdate calls this function once when the floating element is mounted
45+
// we don't want to hide the menu in that case
46+
initialized = true;
47+
return;
48+
}
49+
editor.getExtension(SideMenuExtension)?.hideMenuIfNotFrozen();
50+
},
51+
{
52+
ancestorScroll: true,
53+
ancestorResize: false,
54+
elementResize: false,
55+
layoutShift: false,
56+
},
57+
);
58+
},
59+
[editor],
60+
);
61+
2762
const floatingUIOptions = useMemo<FloatingUIOptions>(
2863
() => ({
2964
...props.floatingUIOptions,
3065
useFloatingOptions: {
3166
open: show,
3267
placement: "left-start",
68+
whileElementsMounted,
3369
...props.floatingUIOptions?.useFloatingOptions,
3470
},
3571
useDismissProps: {
@@ -47,7 +83,7 @@ export const SideMenuController = (props: {
4783
...props.floatingUIOptions?.elementProps,
4884
},
4985
}),
50-
[props.floatingUIOptions, show],
86+
[props.floatingUIOptions, show, whileElementsMounted],
5187
);
5288

5389
const Component = props.sideMenu || SideMenu;

packages/react/src/components/TableHandles/TableHandlesController.tsx

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import {
77
StyleSchema,
88
} from "@blocknote/core";
99
import { TableHandlesExtension } from "@blocknote/core/extensions";
10-
import { FC, useMemo, useState } from "react";
10+
import { FC, useCallback, useMemo, useState } from "react";
1111

12-
import { offset, size } from "@floating-ui/react";
12+
import { autoUpdate, offset, ReferenceElement, size } from "@floating-ui/react";
1313
import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js";
1414
import { useExtensionState } from "../../hooks/useExtension.js";
1515
import { FloatingUIOptions } from "../Popovers/FloatingUIOptions.js";
@@ -137,6 +137,36 @@ export const TableHandlesController = <
137137
return references;
138138
}, [editor, state]);
139139

140+
// Hides the table handles on ancestor scroll so they don't overflow
141+
// outside the editor's scroll container.
142+
const whileElementsMounted = useCallback(
143+
(
144+
reference: ReferenceElement,
145+
floating: HTMLElement,
146+
_update: () => void,
147+
) => {
148+
let initialized = false;
149+
return autoUpdate(
150+
reference,
151+
floating,
152+
() => {
153+
if (!initialized) {
154+
initialized = true;
155+
return;
156+
}
157+
editor.getExtension(TableHandlesExtension)?.hideHandlesIfNotFrozen();
158+
},
159+
{
160+
ancestorScroll: true,
161+
ancestorResize: false,
162+
elementResize: false,
163+
layoutShift: false,
164+
},
165+
);
166+
},
167+
[editor],
168+
);
169+
140170
const floatingUIOptions = useMemo<
141171
| {
142172
rowTableHandle: FloatingUIOptions;
@@ -158,6 +188,7 @@ export const TableHandlesController = <
158188
(!onlyShownElement || onlyShownElement === "rowTableHandle"),
159189
placement: "left",
160190
middleware: [offset(-10)],
191+
whileElementsMounted,
161192
},
162193
focusManagerProps: {
163194
disabled: true,
@@ -177,6 +208,7 @@ export const TableHandlesController = <
177208
onlyShownElement === "columnTableHandle"),
178209
placement: "top",
179210
middleware: [offset(-12)],
211+
whileElementsMounted,
180212
},
181213
focusManagerProps: {
182214
disabled: true,
@@ -196,6 +228,7 @@ export const TableHandlesController = <
196228
(!onlyShownElement || onlyShownElement === "tableCellHandle"),
197229
placement: "top-end",
198230
middleware: [offset({ mainAxis: -15, crossAxis: -1 })],
231+
whileElementsMounted,
199232
},
200233
focusManagerProps: {
201234
disabled: true,
@@ -214,6 +247,7 @@ export const TableHandlesController = <
214247
(!onlyShownElement ||
215248
onlyShownElement === "extendRowsButton"),
216249
placement: "bottom",
250+
whileElementsMounted,
217251
middleware: [
218252
size({
219253
apply({ rects, elements }) {
@@ -241,6 +275,7 @@ export const TableHandlesController = <
241275
(!onlyShownElement ||
242276
onlyShownElement === "extendColumnsButton"),
243277
placement: "right",
278+
whileElementsMounted,
244279
middleware: [
245280
size({
246281
apply({ rects, elements }) {
@@ -262,7 +297,7 @@ export const TableHandlesController = <
262297
},
263298
}
264299
: undefined,
265-
[onlyShownElement, state],
300+
[onlyShownElement, state, whileElementsMounted],
266301
);
267302

268303
if (!state) {

0 commit comments

Comments
 (0)