diff --git a/packages/react/src/components/Comments/FloatingComposerController.tsx b/packages/react/src/components/Comments/FloatingComposerController.tsx index 0462225d2f..599bb9ca7d 100644 --- a/packages/react/src/components/Comments/FloatingComposerController.tsx +++ b/packages/react/src/components/Comments/FloatingComposerController.tsx @@ -8,6 +8,7 @@ import { } from "@blocknote/core"; import { CommentsExtension } from "@blocknote/core/comments"; import { flip, offset, shift } from "@floating-ui/react"; +import merge from "lodash.merge"; import { ComponentProps, FC, useMemo } from "react"; import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js"; @@ -46,28 +47,31 @@ export default function FloatingComposerController< }); const floatingUIOptions = useMemo( - () => ({ - useFloatingOptions: { - open: !!pendingComment, - // Needed as hooks like `useDismiss` call `onOpenChange` to change the - // open state. - onOpenChange: (open) => { - if (!open) { - comments.stopPendingComment(); - editor.focus(); - } - }, - placement: "bottom", - middleware: [offset(10), shift(), flip()], - }, - elementProps: { - style: { - zIndex: 60, - }, - }, - ...props.floatingUIOptions, - }), - [comments, editor, pendingComment, props.floatingUIOptions], + () => + merge( + { + useFloatingOptions: { + open: !!pendingComment, + // Needed as hooks like `useDismiss` call `onOpenChange` to change the + // open state. + onOpenChange: (open) => { + if (!open) { + comments.stopPendingComment(); + editor.focus(); + } + }, + placement: "bottom", + middleware: [offset(10), shift(), flip()], + }, + elementProps: { + style: { + zIndex: 60, + }, + }, + } satisfies FloatingUIOptions, + props.floatingUIOptions + ), + [comments, editor, pendingComment, props.floatingUIOptions] ); // nice to have improvements would be: diff --git a/packages/react/src/components/Comments/FloatingThreadController.tsx b/packages/react/src/components/Comments/FloatingThreadController.tsx index eaf4911942..bdf8f51c68 100644 --- a/packages/react/src/components/Comments/FloatingThreadController.tsx +++ b/packages/react/src/components/Comments/FloatingThreadController.tsx @@ -1,5 +1,6 @@ import { CommentsExtension } from "@blocknote/core/comments"; import { flip, offset, shift } from "@floating-ui/react"; +import merge from "lodash.merge"; import { ComponentProps, FC, useMemo } from "react"; import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js"; @@ -39,31 +40,34 @@ export default function FloatingThreadController(props: { ); const floatingUIOptions = useMemo( - () => ({ - useFloatingOptions: { - open: !!selectedThread, - // Needed as hooks like `useDismiss` call `onOpenChange` to change the - // open state. - onOpenChange: (open, _event, reason) => { - if (reason === "escape-key") { - editor.focus(); - } + () => + merge( + { + useFloatingOptions: { + open: !!selectedThread, + // Needed as hooks like `useDismiss` call `onOpenChange` to change the + // open state. + onOpenChange: (open, _event, reason) => { + if (reason === "escape-key") { + editor.focus(); + } - if (!open) { - comments.selectThread(undefined); - } - }, - placement: "bottom", - middleware: [offset(10), shift(), flip()], - }, - elementProps: { - style: { - zIndex: 30, - }, - }, - ...props.floatingUIOptions, - }), - [comments, editor, props.floatingUIOptions, selectedThread], + if (!open) { + comments.selectThread(undefined); + } + }, + placement: "bottom", + middleware: [offset(10), shift(), flip()], + }, + elementProps: { + style: { + zIndex: 30, + }, + }, + } satisfies FloatingUIOptions, + props.floatingUIOptions + ), + [comments, editor, props.floatingUIOptions, selectedThread] ); // nice to have improvements: diff --git a/packages/react/src/components/FilePanel/FilePanelController.tsx b/packages/react/src/components/FilePanel/FilePanelController.tsx index da330242ab..0b5bfa84ef 100644 --- a/packages/react/src/components/FilePanel/FilePanelController.tsx +++ b/packages/react/src/components/FilePanel/FilePanelController.tsx @@ -1,5 +1,6 @@ import { FilePanelExtension } from "@blocknote/core/extensions"; import { flip, offset } from "@floating-ui/react"; +import merge from "lodash.merge"; import { FC, useMemo } from "react"; import { FilePanel } from "./FilePanel.js"; @@ -19,30 +20,33 @@ export const FilePanelController = (props: { const blockId = useExtensionState(FilePanelExtension); const floatingUIOptions = useMemo( - () => ({ - useFloatingOptions: { - open: !!blockId, - // Needed as hooks like `useDismiss` call `onOpenChange` to change the - // open state. - onOpenChange: (open, _event, reason) => { - if (!open) { - filePanel.closeMenu(); - } + () => + merge( + { + useFloatingOptions: { + open: !!blockId, + // Needed as hooks like `useDismiss` call `onOpenChange` to change the + // open state. + onOpenChange: (open, _event, reason) => { + if (!open) { + filePanel.closeMenu(); + } - if (reason === "escape-key") { - editor.focus(); - } - }, - middleware: [offset(10), flip()], - }, - elementProps: { - style: { - zIndex: 90, - }, - }, - ...props.floatingUIOptions, - }), - [blockId, editor, filePanel, props.floatingUIOptions], + if (reason === "escape-key") { + editor.focus(); + } + }, + middleware: [offset(10), flip()], + }, + elementProps: { + style: { + zIndex: 90, + }, + }, + } satisfies FloatingUIOptions, + props.floatingUIOptions + ), + [blockId, editor, filePanel, props.floatingUIOptions] ); const Component = props.filePanel || FilePanel; diff --git a/packages/react/src/components/FormattingToolbar/FormattingToolbarController.tsx b/packages/react/src/components/FormattingToolbar/FormattingToolbarController.tsx index dc9fe92f12..0b90b7a237 100644 --- a/packages/react/src/components/FormattingToolbar/FormattingToolbarController.tsx +++ b/packages/react/src/components/FormattingToolbar/FormattingToolbarController.tsx @@ -8,6 +8,7 @@ import { } from "@blocknote/core"; import { FormattingToolbarExtension } from "@blocknote/core/extensions"; import { flip, offset, shift } from "@floating-ui/react"; +import merge from "lodash.merge"; import { FC, useMemo } from "react"; import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js"; @@ -78,29 +79,32 @@ export const FormattingToolbarController = (props: { }); const floatingUIOptions = useMemo( - () => ({ - useFloatingOptions: { - open: show, - // Needed as hooks like `useDismiss` call `onOpenChange` to change the - // open state. - onOpenChange: (open, _event, reason) => { - formattingToolbar.store.setState(open); + () => + merge( + { + useFloatingOptions: { + open: show, + // Needed as hooks like `useDismiss` call `onOpenChange` to change the + // open state. + onOpenChange: (open, _event, reason) => { + formattingToolbar.store.setState(open); - if (reason === "escape-key") { - editor.focus(); - } - }, - placement, - middleware: [offset(10), shift(), flip()], - }, - elementProps: { - style: { - zIndex: 40, - }, - }, - ...props.floatingUIOptions, - }), - [show, placement, props.floatingUIOptions, formattingToolbar.store, editor], + if (reason === "escape-key") { + editor.focus(); + } + }, + placement, + middleware: [offset(10), shift(), flip()], + }, + elementProps: { + style: { + zIndex: 40, + }, + }, + } satisfies FloatingUIOptions, + props.floatingUIOptions + ), + [show, placement, props.floatingUIOptions, formattingToolbar.store, editor] ); const Component = props.formattingToolbar || FormattingToolbar; diff --git a/packages/react/src/components/LinkToolbar/LinkToolbarController.tsx b/packages/react/src/components/LinkToolbar/LinkToolbarController.tsx index 9271dd7fca..795d936555 100644 --- a/packages/react/src/components/LinkToolbar/LinkToolbarController.tsx +++ b/packages/react/src/components/LinkToolbar/LinkToolbarController.tsx @@ -1,6 +1,7 @@ import { LinkToolbarExtension } from "@blocknote/core/extensions"; import { flip, offset, safePolygon } from "@floating-ui/react"; import { Range } from "@tiptap/core"; +import merge from "lodash.merge"; import { FC, useEffect, useMemo, useState } from "react"; import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js"; @@ -109,51 +110,54 @@ export const LinkToolbarController = (props: { }, [editor, linkToolbar, link, toolbarPositionFrozen]); const floatingUIOptions = useMemo( - () => ({ - useFloatingOptions: { - open: toolbarOpen, - onOpenChange: (open, _event, reason) => { - if (toolbarPositionFrozen) { - return; - } - - // We want to prioritize `selectionLink` over `mouseHoverLink`, so we - // ignore opening/closing from hover events. - if ( - link !== undefined && - link.cursorType === "text" && - reason === "hover" - ) { - return; - } - - if (reason === "escape-key") { - editor.focus(); - } - - setToolbarOpen(open); - }, - placement: "top-start", - middleware: [offset(10), flip()], - }, - useHoverProps: { - // `useHover` hook only enabled when a link is hovered with the - // mouse. - enabled: link !== undefined && link.cursorType === "mouse", - delay: { - open: 250, - close: 250, - }, - handleClose: safePolygon(), - }, - elementProps: { - style: { - zIndex: 50, - }, - }, - ...props.floatingUIOptions, - }), - [editor, link, props.floatingUIOptions, toolbarOpen, toolbarPositionFrozen], + () => + merge( + { + useFloatingOptions: { + open: toolbarOpen, + onOpenChange: (open, _event, reason) => { + if (toolbarPositionFrozen) { + return; + } + + // We want to prioritize `selectionLink` over `mouseHoverLink`, so we + // ignore opening/closing from hover events. + if ( + link !== undefined && + link.cursorType === "text" && + reason === "hover" + ) { + return; + } + + if (reason === "escape-key") { + editor.focus(); + } + + setToolbarOpen(open); + }, + placement: "top-start", + middleware: [offset(10), flip()], + }, + useHoverProps: { + // `useHover` hook only enabled when a link is hovered with the + // mouse. + enabled: link !== undefined && link.cursorType === "mouse", + delay: { + open: 250, + close: 250, + }, + handleClose: safePolygon(), + }, + elementProps: { + style: { + zIndex: 50, + }, + }, + } satisfies FloatingUIOptions, + props.floatingUIOptions + ), + [editor, link, props.floatingUIOptions, toolbarOpen, toolbarPositionFrozen] ); const reference = useMemo( diff --git a/packages/react/src/components/SideMenu/SideMenuController.tsx b/packages/react/src/components/SideMenu/SideMenuController.tsx index 3ceecc5db7..70c71040ef 100644 --- a/packages/react/src/components/SideMenu/SideMenuController.tsx +++ b/packages/react/src/components/SideMenu/SideMenuController.tsx @@ -1,4 +1,5 @@ import { SideMenuExtension } from "@blocknote/core/extensions"; +import merge from "lodash.merge"; import { FC, useMemo } from "react"; import { useExtensionState } from "../../hooks/useExtension.js"; @@ -25,22 +26,25 @@ export const SideMenuController = (props: { const { show, block } = state || {}; const floatingUIOptions = useMemo( - () => ({ - useFloatingOptions: { - open: show, - placement: "left-start", - }, - useDismissProps: { - enabled: false, - }, - elementProps: { - style: { - zIndex: 20, - }, - }, - ...props.floatingUIOptions, - }), - [props.floatingUIOptions, show], + () => + merge( + { + useFloatingOptions: { + open: show, + placement: "left-start", + }, + useDismissProps: { + enabled: false, + }, + elementProps: { + style: { + zIndex: 20, + }, + }, + } satisfies FloatingUIOptions, + props.floatingUIOptions + ), + [props.floatingUIOptions, show] ); const Component = props.sideMenu || SideMenu; diff --git a/packages/react/src/components/SuggestionMenu/GridSuggestionMenu/GridSuggestionMenuController.tsx b/packages/react/src/components/SuggestionMenu/GridSuggestionMenu/GridSuggestionMenuController.tsx index fb8ea434f7..70737c3d25 100644 --- a/packages/react/src/components/SuggestionMenu/GridSuggestionMenu/GridSuggestionMenuController.tsx +++ b/packages/react/src/components/SuggestionMenu/GridSuggestionMenu/GridSuggestionMenuController.tsx @@ -1,6 +1,7 @@ import { BlockSchema, InlineContentSchema, StyleSchema } from "@blocknote/core"; import { SuggestionMenu } from "@blocknote/core/extensions"; import { autoPlacement, offset, shift, size } from "@floating-ui/react"; +import merge from "lodash.merge"; import { FC, useEffect, useMemo } from "react"; import { useBlockNoteEditor } from "../../../hooks/useBlockNoteEditor.js"; @@ -105,48 +106,51 @@ export function GridSuggestionMenuController< }); const floatingUIOptions = useMemo( - () => ({ - useFloatingOptions: { - open: state?.show && state?.triggerCharacter === triggerCharacter, - onOpenChange: (open) => { - if (!open) { - suggestionMenu.closeMenu(); - } - }, - placement: "bottom-start", - middleware: [ - 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. - autoPlacement({ - allowedPlacements: ["bottom-start", "top-start"], - padding: 10, - }), - shift(), - size({ - apply({ elements, availableHeight }) { - elements.floating.style.maxHeight = `${Math.max(0, availableHeight)}px`; + () => + merge( + { + useFloatingOptions: { + open: state?.show && state?.triggerCharacter === triggerCharacter, + onOpenChange: (open) => { + if (!open) { + suggestionMenu.closeMenu(); + } }, - padding: 10, - }), - ], - }, - elementProps: { - // Prevents editor blurring when clicking the scroll bar. - onMouseDownCapture: (event) => event.preventDefault(), - style: { - zIndex: 70, - }, - }, - ...props.floatingUIOptions, - }), + placement: "bottom-start", + middleware: [ + 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. + autoPlacement({ + allowedPlacements: ["bottom-start", "top-start"], + padding: 10, + }), + shift(), + size({ + apply({ elements, availableHeight }) { + elements.floating.style.maxHeight = `${Math.max(0, availableHeight)}px`; + }, + padding: 10, + }), + ], + }, + elementProps: { + // Prevents editor blurring when clicking the scroll bar. + onMouseDownCapture: (event) => event.preventDefault(), + style: { + zIndex: 70, + }, + }, + } satisfies FloatingUIOptions, + props.floatingUIOptions + ), [ props.floatingUIOptions, state?.show, state?.triggerCharacter, suggestionMenu, triggerCharacter, - ], + ] ); if ( diff --git a/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx b/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx index 6985e53b7e..8a5f3a65a0 100644 --- a/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx +++ b/packages/react/src/components/SuggestionMenu/SuggestionMenuController.tsx @@ -10,6 +10,7 @@ import { shift, size, } from "@floating-ui/react"; +import merge from "lodash.merge"; import { FC, useEffect, useMemo } from "react"; import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js"; @@ -106,48 +107,51 @@ export function SuggestionMenuController< }); const floatingUIOptions = useMemo( - () => ({ - useFloatingOptions: { - open: state?.show && state?.triggerCharacter === triggerCharacter, - onOpenChange: (open) => { - if (!open) { - suggestionMenu.closeMenu(); - } - }, - placement: "bottom-start", - middleware: [ - 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. - autoPlacement({ - allowedPlacements: ["bottom-start", "top-start"], - padding: 10, - }), - shift(), - size({ - apply({ elements, availableHeight }) { - elements.floating.style.maxHeight = `${Math.max(0, availableHeight)}px`; + () => + merge( + { + useFloatingOptions: { + open: state?.show && state?.triggerCharacter === triggerCharacter, + onOpenChange: (open) => { + if (!open) { + suggestionMenu.closeMenu(); + } }, - padding: 10, - }), - ], - }, - elementProps: { - // Prevents editor blurring when clicking the scroll bar. - onMouseDownCapture: (event) => event.preventDefault(), - style: { - zIndex: 80, - }, - }, - ...props.floatingUIOptions, - }), + placement: "bottom-start", + middleware: [ + 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. + autoPlacement({ + allowedPlacements: ["bottom-start", "top-start"], + padding: 10, + }), + shift(), + size({ + apply({ elements, availableHeight }) { + elements.floating.style.maxHeight = `${Math.max(0, availableHeight)}px`; + }, + padding: 10, + }), + ], + }, + elementProps: { + // Prevents editor blurring when clicking the scroll bar. + onMouseDownCapture: (event) => event.preventDefault(), + style: { + zIndex: 80, + }, + }, + } satisfies FloatingUIOptions, + props.floatingUIOptions + ), [ props.floatingUIOptions, state?.show, state?.triggerCharacter, suggestionMenu, triggerCharacter, - ], + ] ); if ( diff --git a/packages/xl-ai/src/components/AIMenu/AIMenuController.tsx b/packages/xl-ai/src/components/AIMenu/AIMenuController.tsx index 6072d1b911..8702bbb1a6 100644 --- a/packages/xl-ai/src/components/AIMenu/AIMenuController.tsx +++ b/packages/xl-ai/src/components/AIMenu/AIMenuController.tsx @@ -6,6 +6,7 @@ import { useExtensionState, } from "@blocknote/react"; import { autoUpdate, offset, size } from "@floating-ui/react"; +import merge from "lodash.merge"; import { FC, useMemo } from "react"; import { AIExtension } from "../../AIExtension.js"; @@ -26,69 +27,72 @@ export const AIMenuController = (props: { const blockId = aiMenuState === "closed" ? undefined : aiMenuState.blockId; const floatingUIOptions = useMemo( - () => ({ - useFloatingOptions: { - open: aiMenuState !== "closed", - placement: "bottom", - middleware: [ - offset(10), - // flip(), - size({ - apply({ rects, elements }) { - Object.assign(elements.floating.style, { - width: `${rects.reference.width}px`, + () => + merge( + { + useFloatingOptions: { + open: aiMenuState !== "closed", + placement: "bottom", + middleware: [ + offset(10), + // flip(), + size({ + apply({ rects, elements }) { + Object.assign(elements.floating.style, { + width: `${rects.reference.width}px`, + }); + }, + }), + ], + onOpenChange: (open) => { + if (open || aiMenuState === "closed") { + return; + } + + if (aiMenuState.status === "user-input") { + ai.closeAIMenu(); + } else if ( + aiMenuState.status === "user-reviewing" || + aiMenuState.status === "error" + ) { + ai.rejectChanges(); + } + }, + whileElementsMounted(reference, floating, update) { + return autoUpdate(reference, floating, update, { + animationFrame: true, }); }, - }), - ], - onOpenChange: (open) => { - if (open || aiMenuState === "closed") { - return; - } - - if (aiMenuState.status === "user-input") { - ai.closeAIMenu(); - } else if ( - aiMenuState.status === "user-reviewing" || - aiMenuState.status === "error" - ) { - ai.rejectChanges(); - } - }, - whileElementsMounted(reference, floating, update) { - return autoUpdate(reference, floating, update, { - animationFrame: true, - }); - }, - }, - useDismissProps: { - enabled: - aiMenuState === "closed" || aiMenuState.status === "user-input", - // We should just be able to set `referencePress: true` instead of - // using this listener, but this doesn't seem to trigger. - // (probably because we don't assign the referenceProps to the reference element) - outsidePress: (event) => { - if (event.target instanceof Element) { - const blockElement = event.target.closest(".bn-block"); - if ( - blockElement && - blockElement.getAttribute("data-id") === blockId - ) { - ai.closeAIMenu(); - } - } + }, + useDismissProps: { + enabled: + aiMenuState === "closed" || aiMenuState.status === "user-input", + // We should just be able to set `referencePress: true` instead of + // using this listener, but this doesn't seem to trigger. + // (probably because we don't assign the referenceProps to the reference element) + outsidePress: (event) => { + if (event.target instanceof Element) { + const blockElement = event.target.closest(".bn-block"); + if ( + blockElement && + blockElement.getAttribute("data-id") === blockId + ) { + ai.closeAIMenu(); + } + } - return true; - }, - }, - elementProps: { - style: { - zIndex: 100, - }, - }, - ...props.floatingUIOptions, - }), - [ai, aiMenuState, blockId, props.floatingUIOptions], + return true; + }, + }, + elementProps: { + style: { + zIndex: 100, + }, + }, + } satisfies FloatingUIOptions, + props.floatingUIOptions + ), + [ai, aiMenuState, blockId, props.floatingUIOptions] ); const Component = props.aiMenu || AIMenu;