From c93dfed08eb4a9fc2a9ecf573fa5e8592183ccef Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Thu, 4 Dec 2025 18:03:34 +0100 Subject: [PATCH 1/4] Fixed suggestion menu positioning --- .../SuggestionMenuController.tsx | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx b/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx index 151bbea5f7..6aa7e130b6 100644 --- a/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx +++ b/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx @@ -5,7 +5,7 @@ import { } from "@blocknote/core/extensions"; import { UseFloatingOptions, - flip, + autoPlacement, offset, shift, size, @@ -127,18 +127,22 @@ export function SuggestionMenuController< offset(10), // Flips the menu placement to maximize the space available, and prevents // the menu from being cut off by the confines of the screen. - flip({ - mainAxis: true, - crossAxis: false, + autoPlacement({ + allowedPlacements: ["bottom-start", "top-start"], + padding: 10, }), shift(), size({ - apply({ availableHeight, elements }) { - Object.assign(elements.floating.style, { - maxHeight: `${availableHeight - 10}px`, - minHeight: "300px", - }); + apply(p) { + // Because the height of the suggestion menu is dynamic and based + // on the number of items, the `flip` middleware gets confused + // when the height is set on the initial render. Therefore, it's + // set right after instead. + setTimeout(() => { + p.elements.floating.style.maxHeight = `${p.availableHeight}px`; + }, 10); }, + padding: 10, }), ], }, From e394108d8f6898bd1201f0f9a317118bc04093be Mon Sep 17 00:00:00 2001 From: Matthew Lipski Date: Thu, 4 Dec 2025 18:12:15 +0100 Subject: [PATCH 2/4] Added same changes to `GridSuggestionMenuController` --- .../GridSuggestionMenuController.tsx | 21 ++++++++++++------- .../SuggestionMenuController.tsx | 6 +++--- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/packages/react/src/components/SuggestionMenu/GridSuggestionMenu/GridSuggestionMenuController.tsx b/packages/react/src/components/SuggestionMenu/GridSuggestionMenu/GridSuggestionMenuController.tsx index d6e87bf8e4..f25b62e8d2 100644 --- a/packages/react/src/components/SuggestionMenu/GridSuggestionMenu/GridSuggestionMenuController.tsx +++ b/packages/react/src/components/SuggestionMenu/GridSuggestionMenu/GridSuggestionMenuController.tsx @@ -1,6 +1,6 @@ import { BlockSchema, InlineContentSchema, StyleSchema } from "@blocknote/core"; import { SuggestionMenu } from "@blocknote/core/extensions"; -import { flip, offset, shift, size } from "@floating-ui/react"; +import { autoPlacement, offset, shift, size } from "@floating-ui/react"; import { FC, useEffect, useMemo } from "react"; import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor.js"; @@ -126,17 +126,22 @@ export function GridSuggestionMenuController< offset(10), // Flips the menu placement to maximize the space available, and prevents // the menu from being cut off by the confines of the screen. - flip({ - mainAxis: true, - crossAxis: false, + autoPlacement({ + allowedPlacements: ["bottom-start", "top-start"], + padding: 10, }), shift(), size({ - apply({ availableHeight, elements }) { - Object.assign(elements.floating.style, { - maxHeight: `${availableHeight - 10}px`, - }); + apply(p) { + // Because the height of the suggestion menu is dynamic and based + // on the number of items, the `autoPlacement` middleware gets + // confused when the height is set on the initial render. + // Therefore, it's set right after instead. + setTimeout(() => { + p.elements.floating.style.maxHeight = `${p.availableHeight}px`; + }, 10); }, + padding: 10, }), ], }, diff --git a/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx b/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx index 6aa7e130b6..5960dc010c 100644 --- a/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx +++ b/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx @@ -135,9 +135,9 @@ export function SuggestionMenuController< size({ apply(p) { // Because the height of the suggestion menu is dynamic and based - // on the number of items, the `flip` middleware gets confused - // when the height is set on the initial render. Therefore, it's - // set right after instead. + // on the number of items, the `autoPlacement` middleware gets + // confused when the height is set on the initial render. + // Therefore, it's set right after instead. setTimeout(() => { p.elements.floating.style.maxHeight = `${p.availableHeight}px`; }, 10); From 58a7755b2b8a22311e9223772aef6e076829a028 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Tue, 9 Dec 2025 12:50:14 +0100 Subject: [PATCH 3/4] fix: clamp the maxHeight --- .../GridSuggestionMenuController.tsx | 10 ++-------- .../SuggestionMenu/SuggestionMenuController.tsx | 10 ++-------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/packages/react/src/components/SuggestionMenu/GridSuggestionMenu/GridSuggestionMenuController.tsx b/packages/react/src/components/SuggestionMenu/GridSuggestionMenu/GridSuggestionMenuController.tsx index f25b62e8d2..f5d83ed8c5 100644 --- a/packages/react/src/components/SuggestionMenu/GridSuggestionMenu/GridSuggestionMenuController.tsx +++ b/packages/react/src/components/SuggestionMenu/GridSuggestionMenu/GridSuggestionMenuController.tsx @@ -132,14 +132,8 @@ export function GridSuggestionMenuController< }), shift(), size({ - apply(p) { - // Because the height of the suggestion menu is dynamic and based - // on the number of items, the `autoPlacement` middleware gets - // confused when the height is set on the initial render. - // Therefore, it's set right after instead. - setTimeout(() => { - p.elements.floating.style.maxHeight = `${p.availableHeight}px`; - }, 10); + apply({ elements, availableHeight }) { + elements.floating.style.maxHeight = `${Math.max(0, availableHeight)}px`; }, padding: 10, }), diff --git a/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx b/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx index 5960dc010c..602720cfa7 100644 --- a/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx +++ b/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx @@ -133,14 +133,8 @@ export function SuggestionMenuController< }), shift(), size({ - apply(p) { - // Because the height of the suggestion menu is dynamic and based - // on the number of items, the `autoPlacement` middleware gets - // confused when the height is set on the initial render. - // Therefore, it's set right after instead. - setTimeout(() => { - p.elements.floating.style.maxHeight = `${p.availableHeight}px`; - }, 10); + apply({ elements, availableHeight }) { + elements.floating.style.maxHeight = `${Math.max(0, availableHeight)}px`; }, padding: 10, }), From c36f639c382acbdfe04aeef814ad2e1ed28400c2 Mon Sep 17 00:00:00 2001 From: Nick the Sick Date: Tue, 9 Dec 2025 13:35:47 +0100 Subject: [PATCH 4/4] refactor: simplify the state a bit --- .../GridSuggestionMenuController.tsx | 18 +++++------------- .../SuggestionMenuController.tsx | 18 +++++------------- 2 files changed, 10 insertions(+), 26 deletions(-) diff --git a/packages/react/src/components/SuggestionMenu/GridSuggestionMenu/GridSuggestionMenuController.tsx b/packages/react/src/components/SuggestionMenu/GridSuggestionMenu/GridSuggestionMenuController.tsx index f5d83ed8c5..fb8ea434f7 100644 --- a/packages/react/src/components/SuggestionMenu/GridSuggestionMenu/GridSuggestionMenuController.tsx +++ b/packages/react/src/components/SuggestionMenu/GridSuggestionMenu/GridSuggestionMenuController.tsx @@ -9,10 +9,7 @@ import { useExtensionState, } from "../../../hooks/useExtension.js"; import { FloatingUIOptions } from "../../Popovers/FloatingUIOptions.js"; -import { - GenericPopover, - GenericPopoverReference, -} from "../../Popovers/GenericPopover.js"; +import { GenericPopover } from "../../Popovers/GenericPopover.js"; import { getDefaultReactEmojiPickerItems } from "./getDefaultReactEmojiPickerItems.js"; import { GridSuggestionMenu } from "./GridSuggestionMenu.js"; import { GridSuggestionMenuWrapper } from "./GridSuggestionMenuWrapper.js"; @@ -97,20 +94,15 @@ export function GridSuggestionMenuController< }, [suggestionMenu, triggerCharacter]); const state = useExtensionState(SuggestionMenu); - const referencePos = useExtensionState(SuggestionMenu, { - selector: (state) => state?.referencePos || new DOMRect(), - }); - - const reference = useMemo( - () => ({ + const reference = useExtensionState(SuggestionMenu, { + selector: (state) => ({ // Use first child as the editor DOM element may itself be scrollable. // For FloatingUI to auto-update the position during scrolling, the // `contextElement` must be a descendant of the scroll container. element: editor.domElement?.firstChild || undefined, - getBoundingClientRect: () => referencePos, + getBoundingClientRect: () => state?.referencePos || new DOMRect(), }), - [editor.domElement?.firstChild, referencePos], - ); + }); const floatingUIOptions = useMemo( () => ({ diff --git a/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx b/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx index 602720cfa7..6985e53b7e 100644 --- a/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx +++ b/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx @@ -15,10 +15,7 @@ import { FC, useEffect, useMemo } from "react"; import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js"; import { useExtension, useExtensionState } from "../../hooks/useExtension.js"; import { FloatingUIOptions } from "../Popovers/FloatingUIOptions.js"; -import { - GenericPopover, - GenericPopoverReference, -} from "../Popovers/GenericPopover.js"; +import { GenericPopover } from "../Popovers/GenericPopover.js"; import { SuggestionMenu } from "./SuggestionMenu.js"; import { SuggestionMenuWrapper } from "./SuggestionMenuWrapper.js"; import { getDefaultReactSlashMenuItems } from "./getDefaultReactSlashMenuItems.js"; @@ -98,20 +95,15 @@ export function SuggestionMenuController< }, [suggestionMenu, triggerCharacter]); const state = useExtensionState(SuggestionMenuExtension); - const referencePos = useExtensionState(SuggestionMenuExtension, { - selector: (state) => state?.referencePos || new DOMRect(), - }); - - const reference = useMemo( - () => ({ + const reference = useExtensionState(SuggestionMenuExtension, { + selector: (state) => ({ // Use first child as the editor DOM element may itself be scrollable. // For FloatingUI to auto-update the position during scrolling, the // `contextElement` must be a descendant of the scroll container. element: editor.domElement?.firstChild || undefined, - getBoundingClientRect: () => referencePos, + getBoundingClientRect: () => state?.referencePos || new DOMRect(), }), - [editor.domElement?.firstChild, referencePos], - ); + }); const floatingUIOptions = useMemo( () => ({