diff --git a/CHANGELOG.md b/CHANGELOG.md index d0e573a3..b5c09447 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Format loosely follows [Keep a Changelog](https://keepachangelog.com). ### Fixed +- Clicking elsewhere in a note now closes the formatting menu and clears its old text highlight. [#256](https://github.com/bholmesdev/hubble.md/pull/256) - Pinned notes remain visible after restarting or updating the desktop app. [#254](https://github.com/bholmesdev/hubble.md/pull/254) - Pressing Enter in a list item containing an image now creates a new list item instead of dropping out of the list. Thanks [@MonisMS](https://github.com/MonisMS)! [#241](https://github.com/bholmesdev/hubble.md/pull/241) diff --git a/packages/editor/src/FakeSelectionExtension.ts b/packages/editor/src/FakeSelectionExtension.ts index 18ae0ed8..54bc765a 100644 --- a/packages/editor/src/FakeSelectionExtension.ts +++ b/packages/editor/src/FakeSelectionExtension.ts @@ -31,6 +31,14 @@ export const FakeSelectionExtension = Extension.create({ editor.view.dispatch(editor.state.tr); return true; }, + clearFrozenSelection: + () => + ({ editor }) => { + const storage = this.storage as { frozen: FrozenSelection }; + storage.frozen = null; + editor.view.dispatch(editor.state.tr); + return true; + }, restoreSelection: ({ focus }) => ({ editor, chain }) => { @@ -84,6 +92,8 @@ declare module "@tiptap/core" { fakeSelection: { /** Snapshot the current editor selection to display while blurred */ freezeSelection: () => ReturnType; + /** Clear the frozen selection without changing the editor selection */ + clearFrozenSelection: () => ReturnType; /** Restore the frozen selection into the native DOM selection and focus the editor */ restoreSelection: ({ focus }: { focus: boolean }) => ReturnType; }; diff --git a/packages/ui/src/editor/FormatCommandMenu.tsx b/packages/ui/src/editor/FormatCommandMenu.tsx index 62dfff9f..7863ce9f 100644 --- a/packages/ui/src/editor/FormatCommandMenu.tsx +++ b/packages/ui/src/editor/FormatCommandMenu.tsx @@ -187,10 +187,13 @@ export function FormatCommandMenu({ ) ? selectedKind : visibleCommands[0]?.kind; - const closeMenu = () => { + const hideMenu = () => { setOpen(false); setQuery(""); setPosition(null); + }; + const closeMenu = () => { + hideMenu(); // Drop the frozen highlight and restore the real selection so a chosen // command formats the range the user was looking at. editor?.commands.restoreSelection({ focus: false }); @@ -214,6 +217,13 @@ export function FormatCommandMenu({ if (!editor) return; const handleKeyDown = (event: KeyboardEvent) => { + if (open && event.key === "Escape") { + event.preventDefault(); + event.stopImmediatePropagation(); + closeMenu(); + editor.commands.focus(undefined, { scrollIntoView: false }); + return; + } if (!keymatch(event, getCommand("app.format-menu").defaultBinding)) return; if (!editor.isFocused && !open) return; @@ -226,12 +236,24 @@ export function FormatCommandMenu({ } openMenu(); }; + const handlePointerDown = (event: PointerEvent) => { + if (!open) return; + const target = event.target; + if (!(target instanceof Node)) return; + if (menuRef.current?.contains(target)) return; + hideMenu(); + editor.commands.clearFrozenSelection(); + }; window.addEventListener("keydown", handleKeyDown, true); - return () => window.removeEventListener("keydown", handleKeyDown, true); + window.addEventListener("pointerdown", handlePointerDown, true); + return () => { + window.removeEventListener("keydown", handleKeyDown, true); + window.removeEventListener("pointerdown", handlePointerDown, true); + }; }, // biome-ignore lint/correctness/useExhaustiveDependencies: React Compiler stabilizes render-local callbacks. - [closeMenu, editor, open, openMenu], + [closeMenu, editor, hideMenu, open, openMenu], ); useEffect( @@ -291,13 +313,6 @@ export function FormatCommandMenu({ onValueChange={setQuery} placeholder="Format..." className="h-8 w-full border-0 border-b border-border bg-background px-2 text-[11px] leading-4 text-foreground outline-hidden placeholder:text-muted-foreground" - onKeyDown={(event) => { - if (event.key === "Escape") { - event.preventDefault(); - closeMenu(); - editor.commands.focus(undefined, { scrollIntoView: false }); - } - }} /> {visibleCommands.length === 0 ? (