From 5d539447540887d81cc767fdce5b62c9080643b5 Mon Sep 17 00:00:00 2001 From: sid597 Date: Wed, 10 Jun 2026 14:23:56 +0530 Subject: [PATCH 1/3] ENG-1801 Fix query builder column resize lag --- .../components/results-view/ResultsTable.tsx | 200 +++++++++--------- apps/roam/src/styles/styles.css | 6 + 2 files changed, 101 insertions(+), 105 deletions(-) diff --git a/apps/roam/src/components/results-view/ResultsTable.tsx b/apps/roam/src/components/results-view/ResultsTable.tsx index f5f9e1d8d..f0daf18e8 100644 --- a/apps/roam/src/components/results-view/ResultsTable.tsx +++ b/apps/roam/src/components/results-view/ResultsTable.tsx @@ -41,9 +41,7 @@ const ExtraContextRow = ({ uid }: { uid: string }) => { ); }; -const dragImage = document.createElement("img"); -dragImage.src = - "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; +const COLUMN_RESIZING_CLASS = "roamjs-query-column-resizing"; const ResultHeader = React.forwardRef< Record, @@ -174,9 +172,9 @@ const CellRender = ({ content, uid }: { content: string; uid: string }) => { type ResultRowProps = { r: Result; columns: Column[]; - onDragStart: (e: React.DragEvent) => void; - onDrag: (e: React.DragEvent) => void; - onDragEnd: (e: React.DragEvent) => void; + onResizeStart: (e: React.PointerEvent) => void; + onResize: (e: React.PointerEvent) => void; + onResizeEnd: (e: React.PointerEvent) => void; parentUid: string; ctrlClick?: (e: Result) => void; views: { column: string; mode: string; value: string }[]; @@ -189,9 +187,9 @@ const ResultRow = ({ parentUid, ctrlClick, views, - onDragStart, - onDrag, - onDragEnd, + onResizeStart, + onResize, + onResizeEnd, onRefresh, }: ResultRowProps) => { const storedRelationsEnabled = getStoredRelationsEnabled(); @@ -392,18 +390,15 @@ const ResultRow = ({ right: 0, bottom: 0, background: `rgba(16,22,26,0.15)`, + touchAction: "none", }} data-left-column-uid={columnUid} data-right-column-uid={columns[i + 1].uid} data-column={columnUid} - draggable - onDragStart={(e) => { - e.dataTransfer.setData("text/plain", ""); - e.dataTransfer.setDragImage(dragImage, 0, 0); - onDragStart(e); - }} - onDrag={onDrag} - onDragEnd={onDragEnd} + onPointerDown={onResizeStart} + onPointerMove={onResize} + onPointerUp={onResizeEnd} + onPointerCancel={onResizeEnd} /> )} @@ -420,8 +415,9 @@ type ColumnWidths = { type DragInfo = { startX: number; - leftColumnUid: string | null; - rightColumnUid: string | null; + moved: boolean; + leftHeader: HTMLElement | null; + rightHeader: HTMLElement | null; leftStartWidth: number; rightStartWidth: number; }; @@ -459,8 +455,9 @@ const ResultsTable = ({ const tableRef = useRef(null); const dragInfo = useRef({ startX: 0, - leftColumnUid: null, - rightColumnUid: null, + moved: false, + leftHeader: null, + rightHeader: null, leftStartWidth: 0, rightStartWidth: 0, }); @@ -477,28 +474,8 @@ const ResultsTable = ({ return filtered.length ? filtered : columns; }, [columns, viewsByColumn]); - const rafIdRef = useRef(null); - const throttledSetColumnWidths = useCallback((update: ColumnWidths) => { - if (rafIdRef.current !== null) { - cancelAnimationFrame(rafIdRef.current); - } - rafIdRef.current = requestAnimationFrame(() => - setColumnWidths( - (prev) => - ({ - ...prev, - ...update, - }) as ColumnWidths, - ), - ); - }, []); - useEffect(() => { - return () => { - if (rafIdRef.current !== null) { - cancelAnimationFrame(rafIdRef.current); - } - }; + return () => document.body.classList.remove(COLUMN_RESIZING_CLASS); }, []); const [columnWidths, setColumnWidths] = useState(() => { @@ -515,42 +492,45 @@ const ResultsTable = ({ return allWidths; }); - const onDragStart = useCallback((e: React.DragEvent) => { + const onResizeStart = useCallback((e: React.PointerEvent) => { + if (e.button !== 0) return; const { leftColumnUid, rightColumnUid } = e.currentTarget.dataset; if (!leftColumnUid || !rightColumnUid || !tableRef.current) return; - const leftHeader = tableRef.current?.querySelector( + const leftHeader = tableRef.current.querySelector( `thead td[data-column="${leftColumnUid}"]`, ); - const rightHeader = tableRef.current?.querySelector( + const rightHeader = tableRef.current.querySelector( `thead td[data-column="${rightColumnUid}"]`, ); if (!leftHeader || !rightHeader) return; + e.preventDefault(); + e.stopPropagation(); + e.currentTarget.setPointerCapture(e.pointerId); + document.body.classList.add(COLUMN_RESIZING_CLASS); + dragInfo.current = { startX: e.clientX, - leftColumnUid, - rightColumnUid, - leftStartWidth: (leftHeader as HTMLElement).offsetWidth, - rightStartWidth: (rightHeader as HTMLElement).offsetWidth, + moved: false, + leftHeader, + rightHeader, + leftStartWidth: leftHeader.offsetWidth, + rightStartWidth: rightHeader.offsetWidth, }; }, []); - const onDrag = useCallback((e: React.DragEvent) => { - if (e.clientX === 0) return; + const onResize = useCallback((e: React.PointerEvent) => { + if (!e.currentTarget.hasPointerCapture(e.pointerId)) return; - const { - startX, - leftColumnUid, - rightColumnUid, - leftStartWidth, - rightStartWidth, - } = dragInfo.current; + const { startX, leftHeader, rightHeader, leftStartWidth, rightStartWidth } = + dragInfo.current; - if (!leftColumnUid || !rightColumnUid) return; + if (!leftHeader || !rightHeader) return; const delta = e.clientX - startX; + if (delta !== 0) dragInfo.current.moved = true; const minWidth = 40; let newLeftWidth = leftStartWidth + delta; @@ -581,53 +561,63 @@ const ResultsTable = ({ } } - throttledSetColumnWidths({ - [leftColumnUid]: `${newLeftWidth}px`, - [rightColumnUid]: `${newRightWidth}px`, - }); + leftHeader.style.width = `${newLeftWidth}px`; + rightHeader.style.width = `${newRightWidth}px`; }, []); - const onDragEnd = useCallback(() => { - if (rafIdRef.current !== null) { - cancelAnimationFrame(rafIdRef.current); - rafIdRef.current = null; - } - - const totalWidth = tableRef.current?.offsetWidth; - if (!totalWidth || totalWidth === 0) { - return; - } - const minWidth = 40; - const minPercent = (minWidth / totalWidth) * 100; - - const finalWidths: ColumnWidths = { ...columnWidths }; - const uids = visibleColumns.map((c) => c.uid); - uids.forEach((uid) => { - const header = tableRef.current?.querySelector( - `thead td[data-column="${uid}"]`, - ); - if (header) { - const headerWidth = (header as HTMLElement).offsetWidth; - if (headerWidth > 0) { - const percent = (headerWidth / totalWidth) * 100; - finalWidths[uid] = `${Math.max(minPercent, percent)}%`; - } else { - finalWidths[uid] = columnWidths[uid] || "5%"; - } + const onResizeEnd = useCallback( + (e: React.PointerEvent) => { + if (!e.currentTarget.hasPointerCapture(e.pointerId)) return; + e.currentTarget.releasePointerCapture(e.pointerId); + document.body.classList.remove(COLUMN_RESIZING_CLASS); + const { moved } = dragInfo.current; + dragInfo.current = { + startX: 0, + moved: false, + leftHeader: null, + rightHeader: null, + leftStartWidth: 0, + rightStartWidth: 0, + }; + if (!moved) return; + + const totalWidth = tableRef.current?.offsetWidth; + if (!totalWidth || totalWidth === 0) { + return; } - }); - setColumnWidths(finalWidths); - - if (preventSavingSettings) return; - const layoutUid = getSubTree({ parentUid, key: "layout" }).uid; - if (layoutUid) { - setInputSettings({ - blockUid: layoutUid, - key: "widths", - values: Object.entries(finalWidths).map(([k, v]) => `${k} - ${v}`), + const minWidth = 40; + const minPercent = (minWidth / totalWidth) * 100; + + const finalWidths: ColumnWidths = { ...columnWidths }; + const uids = visibleColumns.map((c) => c.uid); + uids.forEach((uid) => { + const header = tableRef.current?.querySelector( + `thead td[data-column="${uid}"]`, + ); + if (header) { + const headerWidth = (header as HTMLElement).offsetWidth; + if (headerWidth > 0) { + const percent = (headerWidth / totalWidth) * 100; + finalWidths[uid] = `${Math.max(minPercent, percent)}%`; + } else { + finalWidths[uid] = columnWidths[uid] || "5%"; + } + } }); - } - }, [parentUid, columnWidths, visibleColumns, preventSavingSettings]); + setColumnWidths(finalWidths); + + if (preventSavingSettings) return; + const layoutUid = getSubTree({ parentUid, key: "layout" }).uid; + if (layoutUid) { + setInputSettings({ + blockUid: layoutUid, + key: "widths", + values: Object.entries(finalWidths).map(([k, v]) => `${k} - ${v}`), + }); + } + }, + [parentUid, columnWidths, visibleColumns, preventSavingSettings], + ); const resultHeaderSetFilters = React.useCallback( (fs: FilterData) => { @@ -752,9 +742,9 @@ const ResultsTable = ({ views={views} onRefresh={onRefresh} columns={visibleColumns} - onDragStart={onDragStart} - onDrag={onDrag} - onDragEnd={onDragEnd} + onResizeStart={onResizeStart} + onResize={onResize} + onResizeEnd={onResizeEnd} /> {extraRowUid === r.uid && ( diff --git a/apps/roam/src/styles/styles.css b/apps/roam/src/styles/styles.css index ef6ccf4f8..bee0461fd 100644 --- a/apps/roam/src/styles/styles.css +++ b/apps/roam/src/styles/styles.css @@ -36,6 +36,12 @@ background: #ffff00; } +body.roamjs-query-column-resizing, +body.roamjs-query-column-resizing * { + cursor: ew-resize !important; + user-select: none; +} + .roamjs-query-embed .rm-block-separator { display: none; } From 6ce9a03c5b5f5b523fb6a099b065b76c8788bd9d Mon Sep 17 00:00:00 2001 From: sid597 Date: Mon, 15 Jun 2026 11:32:11 +0530 Subject: [PATCH 2/3] ENG-1801 Harden query column resize cleanup --- .../components/results-view/ResultsTable.tsx | 99 ++++++++++++------- 1 file changed, 64 insertions(+), 35 deletions(-) diff --git a/apps/roam/src/components/results-view/ResultsTable.tsx b/apps/roam/src/components/results-view/ResultsTable.tsx index f0daf18e8..5062c2ec4 100644 --- a/apps/roam/src/components/results-view/ResultsTable.tsx +++ b/apps/roam/src/components/results-view/ResultsTable.tsx @@ -42,6 +42,7 @@ const ExtraContextRow = ({ uid }: { uid: string }) => { }; const COLUMN_RESIZING_CLASS = "roamjs-query-column-resizing"; +const MIN_COLUMN_WIDTH = 40; const ResultHeader = React.forwardRef< Record, @@ -175,6 +176,7 @@ type ResultRowProps = { onResizeStart: (e: React.PointerEvent) => void; onResize: (e: React.PointerEvent) => void; onResizeEnd: (e: React.PointerEvent) => void; + onResizeLostPointerCapture: (e: React.PointerEvent) => void; parentUid: string; ctrlClick?: (e: Result) => void; views: { column: string; mode: string; value: string }[]; @@ -190,6 +192,7 @@ const ResultRow = ({ onResizeStart, onResize, onResizeEnd, + onResizeLostPointerCapture, onRefresh, }: ResultRowProps) => { const storedRelationsEnabled = getStoredRelationsEnabled(); @@ -399,6 +402,7 @@ const ResultRow = ({ onPointerMove={onResize} onPointerUp={onResizeEnd} onPointerCancel={onResizeEnd} + onLostPointerCapture={onResizeLostPointerCapture} /> )} @@ -414,6 +418,7 @@ type ColumnWidths = { }; type DragInfo = { + pointerId: number | null; startX: number; moved: boolean; leftHeader: HTMLElement | null; @@ -422,6 +427,16 @@ type DragInfo = { rightStartWidth: number; }; +const getInitialDragInfo = (): DragInfo => ({ + pointerId: null, + startX: 0, + moved: false, + leftHeader: null, + rightHeader: null, + leftStartWidth: 0, + rightStartWidth: 0, +}); + const ResultsTable = ({ columns, results, @@ -453,14 +468,7 @@ const ResultsTable = ({ showInterface?: boolean; }) => { const tableRef = useRef(null); - const dragInfo = useRef({ - startX: 0, - moved: false, - leftHeader: null, - rightHeader: null, - leftStartWidth: 0, - rightStartWidth: 0, - }); + const dragInfo = useRef(getInitialDragInfo()); const viewsByColumn = useMemo( () => Object.fromEntries(views.map((v) => [v.column, v])), @@ -494,6 +502,7 @@ const ResultsTable = ({ const onResizeStart = useCallback((e: React.PointerEvent) => { if (e.button !== 0) return; + if (dragInfo.current.pointerId !== null) return; const { leftColumnUid, rightColumnUid } = e.currentTarget.dataset; if (!leftColumnUid || !rightColumnUid || !tableRef.current) return; @@ -512,6 +521,7 @@ const ResultsTable = ({ document.body.classList.add(COLUMN_RESIZING_CLASS); dragInfo.current = { + pointerId: e.pointerId, startX: e.clientX, moved: false, leftHeader, @@ -522,7 +532,9 @@ const ResultsTable = ({ }, []); const onResize = useCallback((e: React.PointerEvent) => { - if (!e.currentTarget.hasPointerCapture(e.pointerId)) return; + if (dragInfo.current.pointerId !== e.pointerId) return; + e.preventDefault(); + e.stopPropagation(); const { startX, leftHeader, rightHeader, leftStartWidth, rightStartWidth } = dragInfo.current; @@ -531,30 +543,29 @@ const ResultsTable = ({ const delta = e.clientX - startX; if (delta !== 0) dragInfo.current.moved = true; - const minWidth = 40; let newLeftWidth = leftStartWidth + delta; let newRightWidth = rightStartWidth - delta; - const leftBelow = newLeftWidth < minWidth; - const rightBelow = newRightWidth < minWidth; + const leftBelow = newLeftWidth < MIN_COLUMN_WIDTH; + const rightBelow = newRightWidth < MIN_COLUMN_WIDTH; if (leftBelow && !rightBelow) { - const adjustment = minWidth - newLeftWidth; - newLeftWidth = minWidth; + const adjustment = MIN_COLUMN_WIDTH - newLeftWidth; + newLeftWidth = MIN_COLUMN_WIDTH; newRightWidth -= adjustment; } else if (rightBelow && !leftBelow) { - const adjustment = minWidth - newRightWidth; - newRightWidth = minWidth; + const adjustment = MIN_COLUMN_WIDTH - newRightWidth; + newRightWidth = MIN_COLUMN_WIDTH; newLeftWidth -= adjustment; } else if (leftBelow && rightBelow) { - const totalMin = minWidth * 2; + const totalMin = MIN_COLUMN_WIDTH * 2; const startTotal = leftStartWidth + rightStartWidth; if (startTotal > totalMin) { const scale = totalMin / startTotal; - newLeftWidth = Math.max(minWidth, leftStartWidth * scale); - newRightWidth = Math.max(minWidth, rightStartWidth * scale); + newLeftWidth = Math.max(MIN_COLUMN_WIDTH, leftStartWidth * scale); + newRightWidth = Math.max(MIN_COLUMN_WIDTH, rightStartWidth * scale); } else { newLeftWidth = leftStartWidth; newRightWidth = rightStartWidth; @@ -565,28 +576,31 @@ const ResultsTable = ({ rightHeader.style.width = `${newRightWidth}px`; }, []); - const onResizeEnd = useCallback( - (e: React.PointerEvent) => { - if (!e.currentTarget.hasPointerCapture(e.pointerId)) return; - e.currentTarget.releasePointerCapture(e.pointerId); + const finishResize = useCallback( + ({ + pointerId, + resizeHandle, + }: { + pointerId: number; + resizeHandle?: HTMLDivElement; + }) => { + const currentDrag = dragInfo.current; + if (currentDrag.pointerId !== pointerId) return; + + dragInfo.current = getInitialDragInfo(); + + if (resizeHandle?.hasPointerCapture(pointerId)) { + resizeHandle.releasePointerCapture(pointerId); + } + document.body.classList.remove(COLUMN_RESIZING_CLASS); - const { moved } = dragInfo.current; - dragInfo.current = { - startX: 0, - moved: false, - leftHeader: null, - rightHeader: null, - leftStartWidth: 0, - rightStartWidth: 0, - }; - if (!moved) return; + if (!currentDrag.moved) return; const totalWidth = tableRef.current?.offsetWidth; if (!totalWidth || totalWidth === 0) { return; } - const minWidth = 40; - const minPercent = (minWidth / totalWidth) * 100; + const minPercent = (MIN_COLUMN_WIDTH / totalWidth) * 100; const finalWidths: ColumnWidths = { ...columnWidths }; const uids = visibleColumns.map((c) => c.uid); @@ -618,6 +632,20 @@ const ResultsTable = ({ }, [parentUid, columnWidths, visibleColumns, preventSavingSettings], ); + const onResizeEnd = useCallback( + (e: React.PointerEvent) => { + e.preventDefault(); + e.stopPropagation(); + finishResize({ pointerId: e.pointerId, resizeHandle: e.currentTarget }); + }, + [finishResize], + ); + const onResizeLostPointerCapture = useCallback( + (e: React.PointerEvent) => { + finishResize({ pointerId: e.pointerId }); + }, + [finishResize], + ); const resultHeaderSetFilters = React.useCallback( (fs: FilterData) => { @@ -745,6 +773,7 @@ const ResultsTable = ({ onResizeStart={onResizeStart} onResize={onResize} onResizeEnd={onResizeEnd} + onResizeLostPointerCapture={onResizeLostPointerCapture} /> {extraRowUid === r.uid && ( From c15902dad71d7da385a71cbcf215941256246a47 Mon Sep 17 00:00:00 2001 From: sid597 Date: Tue, 16 Jun 2026 16:57:00 +0530 Subject: [PATCH 3/3] ENG-1801 Restore query column widths after resize --- apps/roam/src/components/results-view/ResultsTable.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/roam/src/components/results-view/ResultsTable.tsx b/apps/roam/src/components/results-view/ResultsTable.tsx index 5062c2ec4..172206a74 100644 --- a/apps/roam/src/components/results-view/ResultsTable.tsx +++ b/apps/roam/src/components/results-view/ResultsTable.tsx @@ -609,13 +609,15 @@ const ResultsTable = ({ `thead td[data-column="${uid}"]`, ); if (header) { - const headerWidth = (header as HTMLElement).offsetWidth; + const headerElement = header as HTMLElement; + const headerWidth = headerElement.offsetWidth; + let finalWidth = columnWidths[uid] || "5%"; if (headerWidth > 0) { const percent = (headerWidth / totalWidth) * 100; - finalWidths[uid] = `${Math.max(minPercent, percent)}%`; - } else { - finalWidths[uid] = columnWidths[uid] || "5%"; + finalWidth = `${Math.max(minPercent, percent)}%`; } + finalWidths[uid] = finalWidth; + headerElement.style.width = finalWidth; } }); setColumnWidths(finalWidths);