Skip to content

Commit b2898da

Browse files
committed
fix(files): keep Markdown tables readable while scrolling
1 parent 6d73cad commit b2898da

7 files changed

Lines changed: 581 additions & 29 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/document-table.css‎

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@
22
* Canonical table chrome for the file viewer. Both surfaces that render a table for a file — the
33
* rich markdown editor (`.rich-markdown-prose table`) and the tabular previews CSV/XLSX render
44
* through `DataTable` (`.document-table`) — share this one definition so a table looks the same
5-
* whichever file it came from. Chrome is shared; *sizing* is not — prose fits the document width
6-
* while a preview sizes to its data and scrolls (see the two rules below). Editor-only concerns
7-
* (prose block margin, fixed layout for column resizing, cell paragraph reset) stay in
5+
* whichever file it came from. Both size to their content inside a scrolling viewport.
6+
* Editor-only concerns (scroll wrappers, prose block margin, cell paragraph reset) stay in
87
* rich-markdown-editor.css.
98
*/
109

@@ -21,10 +20,6 @@
2120
overflow: hidden;
2221
}
2322

24-
.rich-markdown-prose table {
25-
width: 100%;
26-
}
27-
2823
/* A preview table is data, not prose. A CSV can carry dozens of columns, so sizing the table to the
2924
frame (`width: 100%`) divides that frame between them and — with `overflow-wrap: anywhere` able to
3025
break every column down to one character — renders each header a vertical column of letters.

‎apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/bubble-menu-chrome.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ import { cn, MENU_STYLES } from '@sim/emcn'
44
export const BUBBLE_MENU_CLASS = cn(
55
MENU_STYLES.surface,
66
MENU_STYLES.padding,
7-
'scrollbar-none fade-in-0 z-[var(--z-popover)] flex max-w-[calc(100%_-_1rem)] animate-in items-center gap-0.5 overflow-x-auto duration-150 ease-out motion-reduce:animate-none'
7+
'scrollbar-none fade-in-0 absolute top-0 left-0 z-[var(--z-popover)] flex w-max max-w-[calc(100%_-_1rem)] animate-in items-center gap-0.5 overflow-x-auto duration-150 ease-out motion-reduce:animate-none'
88
)

‎apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/bubble-menu.tsx‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,11 @@ export function EditorBubbleMenu({
307307
editor.commands.focus()
308308
}
309309

310-
const { resolveAnchor, appendTo } = useBubbleMenuFloating(editor, scrollContainerRef)
310+
const { resolveAnchor, appendTo, menuRef } = useBubbleMenuFloating(
311+
editor,
312+
scrollContainerRef,
313+
bubbleMenuKey
314+
)
311315
const canFocus = useCallback(
312316
() => hasFormattableSelection(editor, editor.state.selection.from, editor.state.selection.to),
313317
[editor]
@@ -334,6 +338,7 @@ export function EditorBubbleMenu({
334338

335339
return (
336340
<BubbleMenu
341+
ref={menuRef}
337342
editor={editor}
338343
pluginKey={bubbleMenuKey}
339344
getReferencedVirtualElement={resolveAnchor}

‎apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/table-menu.tsx‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ export function TableBubbleMenu({ editor, scrollContainerRef }: TableBubbleMenuP
4545
}),
4646
})
4747

48-
const { resolveAnchor, appendTo } = useBubbleMenuFloating(editor, scrollContainerRef)
48+
const { resolveAnchor, appendTo, menuRef } = useBubbleMenuFloating(
49+
editor,
50+
scrollContainerRef,
51+
menuKey
52+
)
4953
const canFocus = useCallback(
5054
() =>
5155
shouldShowTableMenu({ editor }) &&
@@ -62,6 +66,7 @@ export function TableBubbleMenu({ editor, scrollContainerRef }: TableBubbleMenuP
6266

6367
return (
6468
<BubbleMenu
69+
ref={menuRef}
6570
editor={editor}
6671
pluginKey={menuKey}
6772
getReferencedVirtualElement={resolveAnchor}

‎apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/use-bubble-menu-floating.ts‎

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { useCallback } from 'react'
1+
import { useCallback, useEffect, useRef } from 'react'
2+
import { autoUpdate } from '@floating-ui/dom'
23
import { posToDOMRect } from '@tiptap/core'
3-
import { AllSelection, type Selection } from '@tiptap/pm/state'
4+
import { AllSelection, type PluginKey, type Selection } from '@tiptap/pm/state'
45
import type { Editor } from '@tiptap/react'
56

67
/**
@@ -35,21 +36,42 @@ function selectionVirtualElement(editor: Editor) {
3536
* Wires a BubbleMenu's Floating UI concerns — the selection anchor and the portal target — so the
3637
* text and table toolbars share one source of truth and can't drift.
3738
*
38-
* The toolbar is appended into the editor's scroll container (which must be a positioning context)
39-
* and left at TipTap's default `absolute` strategy, so it becomes an absolutely-positioned child of
40-
* the scrolled content and tracks the selection through native scrolling — no scroll listener, so no
41-
* lag or "chase" as the pane scrolls. TipTap's default `flip` still parks it above or below the
42-
* selection at the pane edges, and the container's overflow clips it once the selection scrolls out.
39+
* The toolbar stays inside the editor's scroll container. Its controls can resize after a selection
40+
* changes, and a table can scroll independently of the document, so track both through Floating UI.
4341
*/
4442
export function useBubbleMenuFloating(
4543
editor: Editor,
46-
scrollContainerRef: React.RefObject<HTMLDivElement | null>
44+
scrollContainerRef: React.RefObject<HTMLDivElement | null>,
45+
pluginKey: PluginKey
4746
) {
47+
const menuRef = useRef<HTMLDivElement>(null)
4848
const resolveAnchor = useCallback(() => selectionVirtualElement(editor), [editor])
4949
const appendTo = useCallback(
5050
() => scrollContainerRef.current ?? document.body,
5151
[scrollContainerRef]
5252
)
5353

54-
return { resolveAnchor, appendTo }
54+
useEffect(() => {
55+
const menu = menuRef.current
56+
if (!menu) return
57+
let stop: (() => void) | undefined
58+
const trackSelection = () => {
59+
stop?.()
60+
const { node } = editor.view.domAtPos(editor.state.selection.from)
61+
const reference = node instanceof Element ? node : (node.parentElement ?? editor.view.dom)
62+
stop = autoUpdate(reference, menu, () => {
63+
if (!editor.isDestroyed && menu.isConnected && menu.style.visibility !== 'hidden') {
64+
editor.commands.setMeta(pluginKey, 'updatePosition')
65+
}
66+
})
67+
}
68+
trackSelection()
69+
editor.on('selectionUpdate', trackSelection)
70+
return () => {
71+
stop?.()
72+
editor.off('selectionUpdate', trackSelection)
73+
}
74+
}, [editor, pluginKey])
75+
76+
return { resolveAnchor, appendTo, menuRef }
5577
}

‎apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.css‎

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -378,19 +378,33 @@
378378
border-radius: 4px;
379379
}
380380

381-
/* Borders, padding, typography, and header fill come from document-table.css — the chrome shared
382-
with the CSV/XLSX previews. Only the editor-specific bits live here: `table-layout: fixed` is
383-
required by prosemirror-tables' column-resizing plugin, and `overflow: visible` lets a
384-
collaborator's caret name label escape the table box instead of being clipped (fixed layout means
385-
columns can't exceed the table width, so nothing else needs clipping). */
386-
.rich-markdown-nodes table {
387-
table-layout: fixed;
381+
/* Keep wide tables inside their own viewport. Vertical padding keeps peer labels inside the clip. */
382+
.rich-markdown-nodes .tableWrapper {
383+
max-width: 100%;
384+
overflow-x: auto;
385+
}
386+
387+
/* Content sizing avoids dividing many columns into single-character strips. */
388+
.rich-markdown-nodes .tableWrapper > table {
389+
table-layout: auto;
390+
width: max-content;
388391
overflow: visible;
389392
}
390393

391-
/* The block margin is prose rhythm, so it stays with the type scale. */
392-
.rich-markdown-prose table {
393-
margin: 1rem 0;
394+
.rich-markdown-nodes .tableWrapper > table > tbody > tr > th,
395+
.rich-markdown-nodes .tableWrapper > table > tbody > tr > td {
396+
min-width: 80px;
397+
max-width: 320px;
398+
overflow-wrap: anywhere;
399+
}
400+
401+
.rich-markdown-prose .tableWrapper {
402+
margin: 0.5rem 0;
403+
padding: 0.5rem 0;
404+
}
405+
406+
.rich-markdown-prose .tableWrapper > table {
407+
margin: 0;
394408
}
395409

396410
/* A diff's links are chips, so they keep their own colour instead of the prose link style. */

0 commit comments

Comments
 (0)