Skip to content

Commit 09e6b5b

Browse files
feat(apollo-react): add remark-breaks to sticky note markdown rendering
Single newlines in sticky notes now render as <br> instead of being collapsed by the markdown parser, matching user expectations for line breaks in a note-taking context.
1 parent 10bfb22 commit 09e6b5b

4 files changed

Lines changed: 86 additions & 3 deletions

File tree

packages/apollo-react/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@
212212
"react-syntax-highlighter": "^16.1.0",
213213
"react-window": "^2.2.1",
214214
"rehype-katex": "^7.0.1",
215+
"remark-breaks": "^4.0.0",
215216
"remark-gfm": "^4.0.1",
216217
"remark-math": "^6.0.0",
217218
"reselect": "^5.1.1",

packages/apollo-react/src/canvas/components/StickyNoteNode/StickyNoteNode.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { NodeResizeControl, useReactFlow } from '@uipath/apollo-react/canvas/xyf
55
import { AnimatePresence } from 'motion/react';
66
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
77
import ReactMarkdown from 'react-markdown';
8+
import remarkBreaks from 'remark-breaks';
89
import remarkGfm from 'remark-gfm';
910
import { GRID_SPACING } from '../../constants';
1011
import type { ToolbarAction } from '../Toolbar';
@@ -33,6 +34,7 @@ import type { StickyNoteColor, StickyNoteData, TextSelection } from './StickyNot
3334
import { STICKY_NOTE_COLORS, withAlpha } from './StickyNoteNode.types';
3435
import { preserveNewlines } from './StickyNoteNode.utils';
3536
import { useMarkdownShortcuts } from './useMarkdownShortcuts';
37+
import { useScrollCapture } from './useScrollCapture';
3638

3739
export interface StickyNoteNodeProps extends NodeProps {
3840
data: StickyNoteData;
@@ -57,6 +59,7 @@ const StickyNoteNodeComponent = ({
5759
const [isColorPickerOpen, setIsColorPickerOpen] = useState(false);
5860
const [localContent, setLocalContent] = useState(data.content || '');
5961
const textAreaRef = useRef<HTMLTextAreaElement>(null);
62+
const { ref: markdownRef, scrollCaptureProps } = useScrollCapture();
6063
const colorButtonRef = useRef<HTMLDivElement>(null);
6164
const [activeFormats, setActiveFormats] = useState<ActiveFormats>({
6265
bold: false,
@@ -353,16 +356,22 @@ const StickyNoteNodeComponent = ({
353356
/>
354357
</>
355358
) : (
356-
<StickyNoteMarkdown>
359+
<StickyNoteMarkdown ref={markdownRef} {...scrollCaptureProps}>
357360
{localContent ? (
358-
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
361+
<ReactMarkdown
362+
remarkPlugins={[remarkGfm, remarkBreaks]}
363+
components={markdownComponents}
364+
>
359365
{preserveNewlines(localContent)}
360366
</ReactMarkdown>
361367
) : (
362368
// Render placeholder if renderPlaceholderOnSelect is enabled, node is selected, and the content is empty
363369
renderPlaceholderOnSelect &&
364370
selected && (
365-
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
371+
<ReactMarkdown
372+
remarkPlugins={[remarkGfm, remarkBreaks]}
373+
components={markdownComponents}
374+
>
366375
{placeholder}
367376
</ReactMarkdown>
368377
)
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { useCallback, useEffect, useRef, useState } from 'react';
2+
3+
const EVENT_START_POLL_INTERVAL = 150;
4+
5+
/**
6+
* Captures scroll (wheel) events on an overflowing element without hijacking
7+
* an in-progress canvas zoom gesture.
8+
*
9+
* Returns a ref to attach to the scrollable element and props to spread onto it.
10+
* Adds the `nowheel` class only when the pointer entered while no wheel gesture
11+
* was active and the element has overflow.
12+
*/
13+
export function useScrollCapture<T extends HTMLElement = HTMLDivElement>() {
14+
const ref = useRef<T>(null);
15+
const [captureScroll, setCaptureScroll] = useState(false);
16+
const wheelActiveRef = useRef(false);
17+
const wheelTimeoutRef = useRef<ReturnType<typeof setTimeout>>(null);
18+
19+
// Track global wheel activity so we can distinguish "pointer entered while idle"
20+
// from "pointer drifted over during a canvas zoom gesture".
21+
useEffect(() => {
22+
const onWheel = () => {
23+
wheelActiveRef.current = true;
24+
if (wheelTimeoutRef.current) clearTimeout(wheelTimeoutRef.current);
25+
wheelTimeoutRef.current = setTimeout(() => {
26+
wheelActiveRef.current = false;
27+
}, EVENT_START_POLL_INTERVAL);
28+
};
29+
window.addEventListener('wheel', onWheel, { passive: true });
30+
return () => window.removeEventListener('wheel', onWheel);
31+
}, []);
32+
33+
const onMouseEnter = useCallback(() => {
34+
if (wheelActiveRef.current) return;
35+
const el = ref.current;
36+
if (el && el.scrollHeight > el.clientHeight) {
37+
setCaptureScroll(true);
38+
}
39+
}, []);
40+
41+
const onMouseLeave = useCallback(() => {
42+
setCaptureScroll(false);
43+
}, []);
44+
45+
return {
46+
ref,
47+
scrollCaptureProps: {
48+
className: captureScroll ? 'nowheel' : undefined,
49+
onMouseEnter,
50+
onMouseLeave,
51+
},
52+
};
53+
}

pnpm-lock.yaml

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)