diff --git a/examples/03-ui-components/16-link-toolbar-buttons/src/App.tsx b/examples/03-ui-components/16-link-toolbar-buttons/src/App.tsx
index f2c26763b1..1714e18a0d 100644
--- a/examples/03-ui-components/16-link-toolbar-buttons/src/App.tsx
+++ b/examples/03-ui-components/16-link-toolbar-buttons/src/App.tsx
@@ -57,7 +57,7 @@ export default function App() {
text={props.text}
range={props.range}
setToolbarOpen={props.setToolbarOpen}
- setToolbarFrozen={props.setToolbarFrozen}
+ setToolbarPositionFrozen={props.setToolbarPositionFrozen}
/>
>;
floatingUIOptions?: FloatingUIOptions;
}) {
- const [open, setOpen] = useState(false);
-
const editor = useBlockNoteEditor();
const comments = useExtension(CommentsExtension);
@@ -35,9 +33,6 @@ export default function FloatingComposerController<
editor,
selector: (state) => state.pendingComment,
});
- useEffect(() => {
- setOpen(pendingComment);
- }, [pendingComment]);
const position = useEditorState({
editor,
@@ -53,7 +48,7 @@ export default function FloatingComposerController<
const floatingUIOptions = useMemo(
() => ({
useFloatingOptions: {
- open,
+ open: !!pendingComment,
// Needed as hooks like `useDismiss` call `onOpenChange` to change the
// open state.
onOpenChange: (open) => {
@@ -61,8 +56,6 @@ export default function FloatingComposerController<
comments.stopPendingComment();
editor.focus();
}
-
- setOpen(open);
},
placement: "bottom",
middleware: [offset(10), shift(), flip()],
@@ -74,7 +67,7 @@ export default function FloatingComposerController<
},
...props.floatingUIOptions,
}),
- [comments, editor, open, 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 a6c74287bf..eaf4911942 100644
--- a/packages/react/src/components/Comments/FloatingThreadController.tsx
+++ b/packages/react/src/components/Comments/FloatingThreadController.tsx
@@ -1,6 +1,6 @@
import { CommentsExtension } from "@blocknote/core/comments";
import { flip, offset, shift } from "@floating-ui/react";
-import { ComponentProps, FC, useEffect, useMemo, useState } from "react";
+import { ComponentProps, FC, useMemo } from "react";
import { useBlockNoteEditor } from "../../hooks/useBlockNoteEditor.js";
import { useExtension, useExtensionState } from "../../hooks/useExtension.js";
@@ -19,8 +19,6 @@ export default function FloatingThreadController(props: {
}) {
const editor = useBlockNoteEditor();
- const [open, setOpen] = useState(false);
-
const comments = useExtension(CommentsExtension);
const selectedThread = useExtensionState(CommentsExtension, {
editor,
@@ -32,9 +30,6 @@ export default function FloatingThreadController(props: {
}
: undefined,
});
- useEffect(() => {
- setOpen(!!selectedThread);
- }, [selectedThread]);
const threads = useThreads();
@@ -46,7 +41,7 @@ export default function FloatingThreadController(props: {
const floatingUIOptions = useMemo(
() => ({
useFloatingOptions: {
- open,
+ open: !!selectedThread,
// Needed as hooks like `useDismiss` call `onOpenChange` to change the
// open state.
onOpenChange: (open, _event, reason) => {
@@ -57,8 +52,6 @@ export default function FloatingThreadController(props: {
if (!open) {
comments.selectThread(undefined);
}
-
- setOpen(open);
},
placement: "bottom",
middleware: [offset(10), shift(), flip()],
@@ -70,7 +63,7 @@ export default function FloatingThreadController(props: {
},
...props.floatingUIOptions,
}),
- [comments, editor, open, props.floatingUIOptions],
+ [comments, editor, props.floatingUIOptions, selectedThread],
);
// nice to have improvements:
diff --git a/packages/react/src/components/LinkToolbar/DefaultButtons/EditLinkButton.tsx b/packages/react/src/components/LinkToolbar/DefaultButtons/EditLinkButton.tsx
index a533b58dc8..52cef731f4 100644
--- a/packages/react/src/components/LinkToolbar/DefaultButtons/EditLinkButton.tsx
+++ b/packages/react/src/components/LinkToolbar/DefaultButtons/EditLinkButton.tsx
@@ -6,14 +6,16 @@ import { LinkToolbarProps } from "../LinkToolbarProps.js";
export const EditLinkButton = (
props: Pick<
LinkToolbarProps,
- "url" | "text" | "range" | "setToolbarFrozen" | "setToolbarOpen"
+ "url" | "text" | "range" | "setToolbarOpen" | "setToolbarPositionFrozen"
>,
) => {
const Components = useComponentsContext()!;
const dict = useDictionary();
return (
-
+
diff --git a/packages/react/src/components/LinkToolbar/EditLinkMenuItems.tsx b/packages/react/src/components/LinkToolbar/EditLinkMenuItems.tsx
index 4577686cb0..ad454fd556 100644
--- a/packages/react/src/components/LinkToolbar/EditLinkMenuItems.tsx
+++ b/packages/react/src/components/LinkToolbar/EditLinkMenuItems.tsx
@@ -29,7 +29,7 @@ const validateUrl = (url: string) => {
export const EditLinkMenuItems = (
props: Pick<
LinkToolbarProps,
- "url" | "text" | "range" | "setToolbarOpen" | "setToolbarFrozen"
+ "url" | "text" | "range" | "setToolbarOpen" | "setToolbarPositionFrozen"
> & {
showTextField?: boolean;
},
@@ -55,7 +55,7 @@ export const EditLinkMenuItems = (
event.preventDefault();
editLink(validateUrl(currentUrl), currentText, props.range.from);
props.setToolbarOpen?.(false);
- props.setToolbarFrozen?.(false);
+ props.setToolbarPositionFrozen?.(false);
}
},
[editLink, currentUrl, currentText, props],
@@ -76,7 +76,7 @@ export const EditLinkMenuItems = (
const handleSubmit = useCallback(() => {
editLink(validateUrl(currentUrl), currentText, props.range.from);
props.setToolbarOpen?.(false);
- props.setToolbarFrozen?.(false);
+ props.setToolbarPositionFrozen?.(false);
}, [editLink, currentUrl, currentText, props]);
return (
diff --git a/packages/react/src/components/LinkToolbar/LinkToolbar.tsx b/packages/react/src/components/LinkToolbar/LinkToolbar.tsx
index 0de23e37f5..435d173df0 100644
--- a/packages/react/src/components/LinkToolbar/LinkToolbar.tsx
+++ b/packages/react/src/components/LinkToolbar/LinkToolbar.tsx
@@ -26,8 +26,8 @@ export const LinkToolbar = (props: LinkToolbarProps) => {
url={props.url}
text={props.text}
range={props.range}
- setToolbarFrozen={props.setToolbarFrozen}
setToolbarOpen={props.setToolbarOpen}
+ setToolbarPositionFrozen={props.setToolbarPositionFrozen}
/>
;
floatingUIOptions?: FloatingUIOptions;
}) => {
- const editor = useBlockNoteEditor<
- BlockSchema,
- InlineContentSchema,
- StyleSchema
- >();
+ const editor = useBlockNoteEditor();
- const [open, setOpen] = useState(false);
- const [frozen, setFrozen] = useState(false);
+ const [toolbarOpen, setToolbarOpen] = useState(false);
+ const [toolbarPositionFrozen, setToolbarPositionFrozen] = useState(false);
const linkToolbar = useExtension(LinkToolbarExtension);
- const selectionLink = useEditorState({
- editor,
- selector: () => {
- const link = linkToolbar.getLinkAtSelection();
- if (!link) {
- return undefined;
- }
- return {
- url: link.mark.attrs.href as string,
- text: link.text,
- range: link.range,
- element: linkToolbar.getLinkElementAtPos(link.range.from)!,
- };
- },
- });
- useEffect(() => {
- if (frozen) {
- return;
- }
-
- setOpen(!!selectionLink);
- if (selectionLink) {
- // Clears the link hovered by the mouse cursor, when the text cursor is
- // within a link, to avoid any potential clashes in positioning.
- setMouseHoverLink(undefined);
- }
- }, [frozen, selectionLink]);
-
- // The `mouseHoverLink` state is completely separate from the `open` state as
- // the FloatingUI `useHover` hook handles opening/closing the popover when a
- // link is hovered with the mouse cursor. Therefore, we only need to update
- // the link when a new one is hovered.
- const [mouseHoverLink, setMouseHoverLink] = useState<
- | { url: string; text: string; range: Range; element: HTMLAnchorElement }
+ // Because the toolbar opens with a delay when a link is hovered by the mouse
+ // cursor, We need separate `toolbarOpen` and `link` states.
+ const [link, setLink] = useState<
+ | {
+ cursorType: "text" | "mouse";
+ url: string;
+ text: string;
+ range: Range;
+ element: HTMLAnchorElement;
+ }
| undefined
>(undefined);
+ // Updates the link to show the toolbar for. Uses the link found at the text
+ // cursor position. If there is none, uses the link hovered by the mouse
+ // cursor. Otherwise, the toolbar remains closed.
useEffect(() => {
- const cb = (event: MouseEvent) => {
- // Ignores the link hovered by the mouse cursor, when the text cursor is
- // within a link, to avoid any potential clashes in positioning.
- if (selectionLink) {
+ const textCursorCallback = () => {
+ const textCursorLink = linkToolbar.getLinkAtSelection();
+ if (!textCursorLink) {
+ setLink(undefined);
+
+ if (!toolbarPositionFrozen) {
+ setToolbarOpen(false);
+ }
+
+ return;
+ }
+
+ setLink({
+ cursorType: "text",
+ url: textCursorLink.mark.attrs.href as string,
+ text: textCursorLink.text,
+ range: textCursorLink.range,
+ element: linkToolbar.getLinkElementAtPos(textCursorLink.range.from)!,
+ });
+
+ if (!toolbarPositionFrozen) {
+ setToolbarOpen(true);
+ }
+ };
+
+ // At no point in this callback is `setToolbarOpen` called, even though
+ // hovering a link with the mouse cursor should open the toolbar. This is
+ // because the FloatingUI `useHover` hook basically does this for us, so we
+ // only need to update `link` when a new one is hovered.
+ const mouseCursorCallback = (event: MouseEvent) => {
+ // Links selected by the text cursor take priority over those hovered by
+ // the mouse cursor.
+ if (link !== undefined && link.cursorType === "text") {
return;
}
@@ -78,40 +80,50 @@ export const LinkToolbarController = (props: {
return;
}
- const link = linkToolbar.getLinkAtElement(event.target);
- if (!link) {
+ const mouseCursorLink = linkToolbar.getLinkAtElement(event.target);
+ if (!mouseCursorLink) {
return;
}
- setMouseHoverLink({
- url: link.mark.attrs.href as string,
- text: link.text,
- range: link.range,
- element: linkToolbar.getLinkElementAtPos(link.range.from)!,
+ setLink({
+ cursorType: "mouse",
+ url: mouseCursorLink.mark.attrs.href as string,
+ text: mouseCursorLink.text,
+ range: mouseCursorLink.range,
+ element: linkToolbar.getLinkElementAtPos(mouseCursorLink.range.from)!,
});
};
- document.body.addEventListener("mouseover", cb);
+ const destroyOnChangeHandler = editor.onChange(textCursorCallback);
+ const destroyOnSelectionChangeHandler =
+ editor.onSelectionChange(textCursorCallback);
+
+ editor.domElement?.addEventListener("mouseover", mouseCursorCallback);
return () => {
- document.body.removeEventListener("mouseover", cb);
- };
- }, [frozen, linkToolbar, mouseHoverLink?.url, selectionLink]);
+ destroyOnChangeHandler();
+ destroyOnSelectionChangeHandler();
- const link = selectionLink || mouseHoverLink;
+ editor.domElement?.removeEventListener("mouseover", mouseCursorCallback);
+ };
+ }, [editor, linkToolbar, link, toolbarPositionFrozen]);
const floatingUIOptions = useMemo(
() => ({
useFloatingOptions: {
- open,
+ open: toolbarOpen,
onOpenChange: (open, _event, reason) => {
- if (frozen) {
+ if (toolbarPositionFrozen) {
return;
}
// We want to prioritize `selectionLink` over `mouseHoverLink`, so we
// ignore opening/closing from hover events.
- if (selectionLink && reason === "hover") {
+ if (
+ link !== undefined &&
+ link.cursorType === "text" &&
+ reason === "hover"
+ ) {
return;
}
@@ -119,7 +131,7 @@ export const LinkToolbarController = (props: {
editor.focus();
}
- setOpen(open);
+ setToolbarOpen(open);
},
placement: "top-start",
middleware: [offset(10), flip()],
@@ -127,7 +139,7 @@ export const LinkToolbarController = (props: {
useHoverProps: {
// `useHover` hook only enabled when a link is hovered with the
// mouse.
- enabled: !selectionLink && !!mouseHoverLink,
+ enabled: link !== undefined && link.cursorType === "mouse",
delay: {
open: 250,
close: 250,
@@ -141,14 +153,7 @@ export const LinkToolbarController = (props: {
},
...props.floatingUIOptions,
}),
- [
- editor,
- frozen,
- mouseHoverLink,
- open,
- props.floatingUIOptions,
- selectionLink,
- ],
+ [editor, link, props.floatingUIOptions, toolbarOpen, toolbarPositionFrozen],
);
const reference = useMemo(
@@ -169,8 +174,8 @@ export const LinkToolbarController = (props: {
url={link.url}
text={link.text}
range={link.range}
- setToolbarFrozen={setFrozen}
- setToolbarOpen={setOpen}
+ setToolbarOpen={setToolbarOpen}
+ setToolbarPositionFrozen={setToolbarPositionFrozen}
/>
)}
diff --git a/packages/react/src/components/LinkToolbar/LinkToolbarProps.ts b/packages/react/src/components/LinkToolbar/LinkToolbarProps.ts
index 1109ea698b..d0b3be7b8d 100644
--- a/packages/react/src/components/LinkToolbar/LinkToolbarProps.ts
+++ b/packages/react/src/components/LinkToolbar/LinkToolbarProps.ts
@@ -5,7 +5,7 @@ export type LinkToolbarProps = {
url: string;
text: string;
range: Range;
- setToolbarFrozen?: (frozen: boolean) => void;
setToolbarOpen?: (open: boolean) => void;
+ setToolbarPositionFrozen?: (frozen: boolean) => void;
children?: ReactNode;
};