From beef1a3ab48ce032dc72affb0e01a02bd9e2ecb7 Mon Sep 17 00:00:00 2001 From: David Rios Date: Tue, 31 Mar 2026 10:29:53 -0400 Subject: [PATCH 1/2] feat(apollo-react): add remark-breaks to sticky note markdown rendering Single newlines in sticky notes now render as
instead of being collapsed by the markdown parser, matching user expectations for line breaks in a note-taking context. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/apollo-react/package.json | 1 + .../StickyNoteNode/StickyNoteNode.tsx | 5 +++-- pnpm-lock.yaml | 20 +++++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/apollo-react/package.json b/packages/apollo-react/package.json index ed36af802..4b80383f2 100644 --- a/packages/apollo-react/package.json +++ b/packages/apollo-react/package.json @@ -212,6 +212,7 @@ "react-syntax-highlighter": "^16.1.0", "react-window": "^2.2.1", "rehype-katex": "^7.0.1", + "remark-breaks": "^4.0.0", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", "reselect": "^5.1.1", diff --git a/packages/apollo-react/src/canvas/components/StickyNoteNode/StickyNoteNode.tsx b/packages/apollo-react/src/canvas/components/StickyNoteNode/StickyNoteNode.tsx index 65b0eac38..ed45259d9 100644 --- a/packages/apollo-react/src/canvas/components/StickyNoteNode/StickyNoteNode.tsx +++ b/packages/apollo-react/src/canvas/components/StickyNoteNode/StickyNoteNode.tsx @@ -5,6 +5,7 @@ import { NodeResizeControl, useReactFlow } from '@uipath/apollo-react/canvas/xyf import { AnimatePresence } from 'motion/react'; import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import ReactMarkdown from 'react-markdown'; +import remarkBreaks from 'remark-breaks'; import remarkGfm from 'remark-gfm'; import { GRID_SPACING } from '../../constants'; import type { ToolbarAction } from '../Toolbar'; @@ -355,14 +356,14 @@ const StickyNoteNodeComponent = ({ ) : ( {localContent ? ( - + {preserveNewlines(localContent)} ) : ( // Render placeholder if renderPlaceholderOnSelect is enabled, node is selected, and the content is empty renderPlaceholderOnSelect && selected && ( - + {placeholder} ) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bb68aba6a..5923caa42 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -665,6 +665,9 @@ importers: rehype-katex: specifier: ^7.0.1 version: 7.0.1 + remark-breaks: + specifier: ^4.0.0 + version: 4.0.0 remark-gfm: specifier: ^4.0.1 version: 4.0.1 @@ -9345,6 +9348,9 @@ packages: mdast-util-mdxjs-esm@2.0.1: resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + mdast-util-newline-to-break@2.0.0: + resolution: {integrity: sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==} + mdast-util-phrasing@4.1.0: resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} @@ -10990,6 +10996,9 @@ packages: rehype-recma@1.0.0: resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==} + remark-breaks@4.0.0: + resolution: {integrity: sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==} + remark-frontmatter@5.0.0: resolution: {integrity: sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==} @@ -21723,6 +21732,11 @@ snapshots: transitivePeerDependencies: - supports-color + mdast-util-newline-to-break@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-find-and-replace: 3.0.2 + mdast-util-phrasing@4.1.0: dependencies: '@types/mdast': 4.0.4 @@ -23691,6 +23705,12 @@ snapshots: transitivePeerDependencies: - supports-color + remark-breaks@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-newline-to-break: 2.0.0 + unified: 11.0.5 + remark-frontmatter@5.0.0: dependencies: '@types/mdast': 4.0.4 From b6e098164cd8c15dd368131f2d87d9c55adb6d3c Mon Sep 17 00:00:00 2001 From: David Rios Date: Tue, 31 Mar 2026 10:49:43 -0400 Subject: [PATCH 2/2] feat(apollo-react): hijack canvas scroll events when sticky note has overflow --- .../StickyNoteNode/StickyNoteNode.tsx | 4 +- .../StickyNoteNode/useScrollCapture.ts | 53 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 packages/apollo-react/src/canvas/components/StickyNoteNode/useScrollCapture.ts diff --git a/packages/apollo-react/src/canvas/components/StickyNoteNode/StickyNoteNode.tsx b/packages/apollo-react/src/canvas/components/StickyNoteNode/StickyNoteNode.tsx index ed45259d9..cb4cf5b30 100644 --- a/packages/apollo-react/src/canvas/components/StickyNoteNode/StickyNoteNode.tsx +++ b/packages/apollo-react/src/canvas/components/StickyNoteNode/StickyNoteNode.tsx @@ -34,6 +34,7 @@ import type { StickyNoteColor, StickyNoteData, TextSelection } from './StickyNot import { STICKY_NOTE_COLORS, withAlpha } from './StickyNoteNode.types'; import { preserveNewlines } from './StickyNoteNode.utils'; import { useMarkdownShortcuts } from './useMarkdownShortcuts'; +import { useScrollCapture } from './useScrollCapture'; export interface StickyNoteNodeProps extends NodeProps { data: StickyNoteData; @@ -58,6 +59,7 @@ const StickyNoteNodeComponent = ({ const [isColorPickerOpen, setIsColorPickerOpen] = useState(false); const [localContent, setLocalContent] = useState(data.content || ''); const textAreaRef = useRef(null); + const { ref: markdownRef, scrollCaptureProps } = useScrollCapture(); const colorButtonRef = useRef(null); const [activeFormats, setActiveFormats] = useState({ bold: false, @@ -354,7 +356,7 @@ const StickyNoteNodeComponent = ({ /> ) : ( - + {localContent ? ( {preserveNewlines(localContent)} diff --git a/packages/apollo-react/src/canvas/components/StickyNoteNode/useScrollCapture.ts b/packages/apollo-react/src/canvas/components/StickyNoteNode/useScrollCapture.ts new file mode 100644 index 000000000..5fadf0ef8 --- /dev/null +++ b/packages/apollo-react/src/canvas/components/StickyNoteNode/useScrollCapture.ts @@ -0,0 +1,53 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; + +const EVENT_START_POLL_INTERVAL = 150; + +/** + * Captures scroll (wheel) events on an overflowing element without hijacking + * an in-progress canvas zoom gesture. + * + * Returns a ref to attach to the scrollable element and props to spread onto it. + * Adds the `nowheel` class only when the pointer entered while no wheel gesture + * was active and the element has overflow. + */ +export function useScrollCapture() { + const ref = useRef(null); + const [captureScroll, setCaptureScroll] = useState(false); + const wheelActiveRef = useRef(false); + const wheelTimeoutRef = useRef>(null); + + // Track global wheel activity so we can distinguish "pointer entered while idle" + // from "pointer drifted over during a canvas zoom gesture". + useEffect(() => { + const onWheel = () => { + wheelActiveRef.current = true; + if (wheelTimeoutRef.current) clearTimeout(wheelTimeoutRef.current); + wheelTimeoutRef.current = setTimeout(() => { + wheelActiveRef.current = false; + }, EVENT_START_POLL_INTERVAL); + }; + window.addEventListener('wheel', onWheel, { passive: true }); + return () => window.removeEventListener('wheel', onWheel); + }, []); + + const onMouseEnter = useCallback(() => { + if (wheelActiveRef.current) return; + const el = ref.current; + if (el && el.scrollHeight > el.clientHeight) { + setCaptureScroll(true); + } + }, []); + + const onMouseLeave = useCallback(() => { + setCaptureScroll(false); + }, []); + + return { + ref, + scrollCaptureProps: { + className: captureScroll ? 'nowheel' : undefined, + onMouseEnter, + onMouseLeave, + }, + }; +}