diff --git a/apps/roam/src/components/DiscourseNodeMenu.tsx b/apps/roam/src/components/DiscourseNodeMenu.tsx index d8140cc9f..5729e6f8b 100644 --- a/apps/roam/src/components/DiscourseNodeMenu.tsx +++ b/apps/roam/src/components/DiscourseNodeMenu.tsx @@ -7,6 +7,7 @@ import { InputGroup, getKeyCombo, IKeyCombo, + Icon, } from "@blueprintjs/core"; import React, { useCallback, @@ -30,12 +31,14 @@ import posthog from "posthog-js"; type Props = { textarea: HTMLTextAreaElement; extensionAPI: OnloadArgs["extensionAPI"]; + trigger?: JSX.Element; }; const NodeMenu = ({ onClose, textarea, extensionAPI, + trigger, }: { onClose: () => void } & Props) => { const discourseNodes = useMemo( () => getDiscourseNodes().filter((n) => n.backedBy === "user"), @@ -49,6 +52,8 @@ const NodeMenu = ({ const blockUid = useMemo(() => getUids(textarea).blockUid, [textarea]); const menuRef = useRef(null); const [activeIndex, setActiveIndex] = useState(0); + const [isOpen, setIsOpen] = useState(!trigger); + const onSelect = useCallback( (index) => { const menuItem = @@ -90,17 +95,18 @@ const NodeMenu = ({ }); onClose(); }, - [menuRef, blockUid, onClose], + [menuRef, blockUid, onClose, textarea, extensionAPI], ); + const keydownListener = useCallback( (e: KeyboardEvent) => { - if (e.key === "ArrowRight" || e.key === "ArrowDown") { + if (e.key === "ArrowDown") { const index = Number( menuRef.current?.getAttribute("data-active-index"), ); const count = menuRef.current?.childElementCount || 0; setActiveIndex((index + 1) % count); - } else if (e.key === "ArrowLeft" || e.key === "ArrowUp") { + } else if (e.key === "ArrowUp") { const index = Number( menuRef.current?.getAttribute("data-active-index"), ); @@ -113,6 +119,9 @@ const NodeMenu = ({ onSelect(index); // Remove focus from the block to ensure updateBlock works properly document.body.click(); + } else if (e.key === "Escape") { + onClose(); + document.body.click(); } else if (shortcuts.has(e.key.toUpperCase())) { onSelect(indexBySC[e.key.toUpperCase()]); // Remove focus from the block to ensure updateBlock works properly @@ -123,23 +132,44 @@ const NodeMenu = ({ e.stopPropagation(); e.preventDefault(); }, - [menuRef, setActiveIndex], + [onSelect, onClose, indexBySC], ); useEffect(() => { - textarea.addEventListener("keydown", keydownListener); - textarea.addEventListener("input", onClose); + const eventTarget = trigger ? document : textarea; + const keydownHandler = (e: Event) => { + keydownListener(e as KeyboardEvent); + }; + eventTarget.addEventListener("keydown", keydownHandler); + + if (!trigger) { + textarea.addEventListener("input", onClose); + } + return () => { - textarea.removeEventListener("keydown", keydownListener); - textarea.removeEventListener("input", onClose); + eventTarget.removeEventListener("keydown", keydownHandler); + if (!trigger) { + textarea.removeEventListener("input", onClose); + } }; - }, [keydownListener, onClose]); + }, [keydownListener, onClose, textarea, trigger]); + + const handlePopoverInteraction = useCallback( + (nextOpenState: boolean) => { + setIsOpen(nextOpenState); + if (!nextOpenState) { + onClose(); + } + }, + [onClose], + ); + return ( } + target={trigger || } position={Position.BOTTOM_LEFT} modifiers={{ flip: { enabled: false }, @@ -147,6 +177,7 @@ const NodeMenu = ({ }} autoFocus={false} enforceFocus={false} + onInteraction={trigger ? handlePopoverInteraction : undefined} content={ {discourseNodes.map((item, i) => { @@ -200,6 +231,52 @@ export const render = (props: Props) => { ); }; +export const TextSelectionNodeMenu = ({ + textarea, + extensionAPI, + onClose, +}: { + textarea: HTMLTextAreaElement; + extensionAPI: OnloadArgs["extensionAPI"]; + onClose: () => void; +}) => { + const trigger = ( +