Skip to content

Commit 5ed7c03

Browse files
authored
ENG-479: Resizing qb columns make them disappear sometimes (#270)
* working * now correct behaviour * request animation frame and address coderabbit review * address review * restore accidental deleted styles * address coderabbit review * address coderabbit
1 parent cd9d14b commit 5ed7c03

1 file changed

Lines changed: 199 additions & 89 deletions

File tree

apps/roam/src/components/results-view/ResultsTable.tsx

Lines changed: 199 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,6 @@ const ResultHeader = React.forwardRef<
9999
() => activeSort.findIndex((s) => s.key === c.key),
100100
[c.key, activeSort],
101101
);
102-
const refCallback = useCallback(
103-
(r: HTMLTableDataCellElement) => {
104-
if (ref && "current" in ref && ref.current) ref.current[c.uid] = r;
105-
},
106-
[ref, c.uid],
107-
);
108102
return (
109103
<td
110104
style={{
@@ -113,7 +107,6 @@ const ResultHeader = React.forwardRef<
113107
width: columnWidth,
114108
}}
115109
data-column={c.uid}
116-
ref={refCallback}
117110
key={c.uid}
118111
onClick={() => {
119112
if (sortIndex >= 0) {
@@ -194,28 +187,29 @@ export const CellEmbed = ({
194187
);
195188
};
196189

197-
type OnWidthUpdate = (args: {
198-
column: string;
199-
width: string;
200-
save?: boolean;
201-
}) => void;
190+
type ResultRowProps = {
191+
r: Result;
192+
columns: Column[];
193+
onDragStart: (e: React.DragEvent<HTMLDivElement>) => void;
194+
onDrag: (e: React.DragEvent<HTMLDivElement>) => void;
195+
onDragEnd: (e: React.DragEvent<HTMLDivElement>) => void;
196+
parentUid: string;
197+
ctrlClick?: (e: Result) => void;
198+
views: { column: string; mode: string; value: string }[];
199+
onRefresh: () => void;
200+
};
201+
202202
const ResultRow = ({
203203
r,
204+
columns,
204205
parentUid,
205206
ctrlClick,
206207
views,
208+
onDragStart,
209+
onDrag,
210+
onDragEnd,
207211
onRefresh,
208-
columns,
209-
onWidthUpdate,
210-
}: {
211-
r: Result;
212-
parentUid: string;
213-
ctrlClick?: (e: Result) => void;
214-
views: { column: string; mode: string; value: string }[];
215-
columns: Column[];
216-
onRefresh: () => void;
217-
onWidthUpdate: OnWidthUpdate;
218-
}) => {
212+
}: ResultRowProps) => {
219213
const cell = (key: string) => {
220214
const value = toCellValue({
221215
value: r[`${key}-display`] || r[key] || "",
@@ -264,32 +258,6 @@ const ResultRow = ({
264258
[views],
265259
);
266260
const trRef = useRef<HTMLTableRowElement>(null);
267-
const dragHandler = useCallback(
268-
(e: React.DragEvent<HTMLDivElement>) => {
269-
const delta = e.clientX - e.currentTarget.getBoundingClientRect().left;
270-
const cellWidth =
271-
e.currentTarget.parentElement?.getBoundingClientRect().width;
272-
if (typeof cellWidth === "undefined") return;
273-
if (cellWidth + delta <= 0) return;
274-
const rowWidth =
275-
e.currentTarget.parentElement?.parentElement?.getBoundingClientRect()
276-
.width;
277-
if (typeof rowWidth === "undefined") return;
278-
if (cellWidth + delta >= rowWidth) return;
279-
const column = e.currentTarget.getAttribute("data-column");
280-
const save = e.type === "dragend";
281-
if (trRef.current) {
282-
trRef.current.style.cursor = save ? "" : "ew-resize";
283-
}
284-
if (column)
285-
onWidthUpdate({
286-
column,
287-
width: `${((cellWidth + delta) / rowWidth) * 100}%`,
288-
save,
289-
});
290-
},
291-
[onWidthUpdate],
292-
);
293261
return (
294262
<>
295263
<tr ref={trRef} data-uid={r.uid}>
@@ -350,21 +318,25 @@ const ResultRow = ({
350318
{i < columns.length - 1 && (
351319
<div
352320
style={{
353-
width: 1,
321+
width: 2,
354322
cursor: "ew-resize",
355323
position: "absolute",
356324
top: 0,
357325
right: 0,
358326
bottom: 0,
359327
background: `rgba(16,22,26,0.15)`,
360328
}}
329+
data-left-column-uid={columnUid}
330+
data-right-column-uid={columns[i + 1].uid}
361331
data-column={columnUid}
362332
draggable
363-
onDragStart={(e) =>
364-
e.dataTransfer.setDragImage(dragImage, 0, 0)
365-
}
366-
onDrag={dragHandler}
367-
onDragEnd={dragHandler}
333+
onDragStart={(e) => {
334+
e.dataTransfer.setData("text/plain", "");
335+
e.dataTransfer.setDragImage(dragImage, 0, 0);
336+
onDragStart(e);
337+
}}
338+
onDrag={onDrag}
339+
onDragEnd={onDragEnd}
368340
/>
369341
)}
370342
</td>
@@ -375,6 +347,18 @@ const ResultRow = ({
375347
);
376348
};
377349

350+
type ColumnWidths = {
351+
[key: string]: string;
352+
};
353+
354+
type DragInfo = {
355+
startX: number;
356+
leftColumnUid: string | null;
357+
rightColumnUid: string | null;
358+
leftStartWidth: number;
359+
rightStartWidth: number;
360+
};
361+
378362
const ResultsTable = ({
379363
columns,
380364
results,
@@ -417,42 +401,166 @@ const ResultsTable = ({
417401
allResults: Result[];
418402
showInterface?: boolean;
419403
}) => {
420-
const columnWidths = useMemo(() => {
404+
const tableRef = useRef<HTMLTableElement | null>(null);
405+
const dragInfo = useRef<DragInfo>({
406+
startX: 0,
407+
leftColumnUid: null,
408+
rightColumnUid: null,
409+
leftStartWidth: 0,
410+
rightStartWidth: 0,
411+
});
412+
413+
const rafIdRef = useRef<number | null>(null);
414+
const throttledSetColumnWidths = useCallback((update: ColumnWidths) => {
415+
if (rafIdRef.current !== null) {
416+
cancelAnimationFrame(rafIdRef.current);
417+
}
418+
rafIdRef.current = requestAnimationFrame(() =>
419+
setColumnWidths(
420+
(prev) =>
421+
({
422+
...prev,
423+
...update,
424+
}) as ColumnWidths,
425+
),
426+
);
427+
}, []);
428+
429+
useEffect(() => {
430+
return () => {
431+
if (rafIdRef.current !== null) {
432+
cancelAnimationFrame(rafIdRef.current);
433+
}
434+
};
435+
}, []);
436+
437+
const [columnWidths, setColumnWidths] = useState(() => {
421438
const widths =
422439
typeof layout.widths === "string" ? [layout.widths] : layout.widths || [];
423-
return Object.fromEntries(
424-
widths
425-
.map((w) => {
426-
const match = /^(.*) - ([^-]+)$/.exec(w);
427-
return match;
428-
})
429-
.filter((m): m is RegExpExecArray => !!m)
430-
.map((match) => {
431-
return [match[1], match[2]];
432-
}),
440+
const fromLayout = Object.fromEntries(
441+
widths.map((w) => w.split(" - ")).filter((p) => p.length === 2),
442+
);
443+
const allWidths: ColumnWidths = {};
444+
const defaultWidth = `${100 / columns.length}%`;
445+
columns.forEach((c) => {
446+
allWidths[c.uid] = fromLayout[c.uid] || defaultWidth;
447+
});
448+
return allWidths;
449+
});
450+
451+
const onDragStart = useCallback((e) => {
452+
const { leftColumnUid, rightColumnUid } = e.currentTarget.dataset;
453+
if (!leftColumnUid || !rightColumnUid || !tableRef.current) return;
454+
455+
const leftHeader = tableRef.current?.querySelector(
456+
`thead td[data-column="${leftColumnUid}"]`,
433457
);
434-
}, [layout]);
435-
const thRefs = useRef<Record<string, HTMLTableCellElement>>({});
436-
const onWidthUpdate = useCallback<OnWidthUpdate>(
437-
(args) => {
438-
const cell = thRefs.current[args.column];
439-
if (!cell) return;
440-
cell.style.width = args.width;
441-
if (args.save) {
442-
const layoutUid = getSubTree({ parentUid, key: "layout" }).uid;
443-
if (layoutUid)
444-
setInputSettings({
445-
blockUid: layoutUid,
446-
key: "widths",
447-
values: Object.entries(thRefs.current)
448-
.map(([k, v]) => [k, v.style.width])
449-
.filter(([k, v]) => !!k && !!v)
450-
.map(([k, v]) => `${k} - ${v}`),
451-
});
458+
const rightHeader = tableRef.current?.querySelector(
459+
`thead td[data-column="${rightColumnUid}"]`,
460+
);
461+
462+
if (!leftHeader || !rightHeader) return;
463+
464+
dragInfo.current = {
465+
startX: e.clientX,
466+
leftColumnUid,
467+
rightColumnUid,
468+
leftStartWidth: (leftHeader as HTMLElement).offsetWidth,
469+
rightStartWidth: (rightHeader as HTMLElement).offsetWidth,
470+
};
471+
}, []);
472+
473+
const onDrag = useCallback((e: React.DragEvent<HTMLDivElement>) => {
474+
if (e.clientX === 0) return;
475+
476+
const {
477+
startX,
478+
leftColumnUid,
479+
rightColumnUid,
480+
leftStartWidth,
481+
rightStartWidth,
482+
} = dragInfo.current;
483+
484+
if (!leftColumnUid || !rightColumnUid) return;
485+
486+
const delta = e.clientX - startX;
487+
const minWidth = 40;
488+
489+
let newLeftWidth = leftStartWidth + delta;
490+
let newRightWidth = rightStartWidth - delta;
491+
492+
const leftBelow = newLeftWidth < minWidth;
493+
const rightBelow = newRightWidth < minWidth;
494+
495+
if (leftBelow && !rightBelow) {
496+
const adjustment = minWidth - newLeftWidth;
497+
newLeftWidth = minWidth;
498+
newRightWidth -= adjustment;
499+
} else if (rightBelow && !leftBelow) {
500+
const adjustment = minWidth - newRightWidth;
501+
newRightWidth = minWidth;
502+
newLeftWidth -= adjustment;
503+
} else if (leftBelow && rightBelow) {
504+
const totalMin = minWidth * 2;
505+
const startTotal = leftStartWidth + rightStartWidth;
506+
507+
if (startTotal > totalMin) {
508+
const scale = totalMin / startTotal;
509+
newLeftWidth = Math.max(minWidth, leftStartWidth * scale);
510+
newRightWidth = Math.max(minWidth, rightStartWidth * scale);
511+
} else {
512+
newLeftWidth = leftStartWidth;
513+
newRightWidth = rightStartWidth;
452514
}
453-
},
454-
[thRefs, parentUid],
455-
);
515+
}
516+
517+
throttledSetColumnWidths({
518+
[leftColumnUid]: `${newLeftWidth}px`,
519+
[rightColumnUid]: `${newRightWidth}px`,
520+
});
521+
}, []);
522+
523+
const onDragEnd = useCallback(() => {
524+
if (rafIdRef.current !== null) {
525+
cancelAnimationFrame(rafIdRef.current);
526+
rafIdRef.current = null;
527+
}
528+
529+
const totalWidth = tableRef.current?.offsetWidth;
530+
if (!totalWidth || totalWidth === 0) {
531+
return;
532+
}
533+
const minWidth = 40;
534+
const minPercent = (minWidth / totalWidth) * 100;
535+
536+
const finalWidths: ColumnWidths = {};
537+
const uids = columns.map((c) => c.uid);
538+
uids.forEach((uid) => {
539+
const header = tableRef.current?.querySelector(
540+
`thead td[data-column="${uid}"]`,
541+
);
542+
if (header) {
543+
const headerWidth = (header as HTMLElement).offsetWidth;
544+
if (headerWidth > 0) {
545+
const percent = (headerWidth / totalWidth) * 100;
546+
finalWidths[uid] = `${Math.max(minPercent, percent)}%`;
547+
} else {
548+
finalWidths[uid] = columnWidths[uid] || "5%";
549+
}
550+
}
551+
});
552+
setColumnWidths(finalWidths);
553+
554+
const layoutUid = getSubTree({ parentUid, key: "layout" }).uid;
555+
if (layoutUid) {
556+
setInputSettings({
557+
blockUid: layoutUid,
558+
key: "widths",
559+
values: Object.entries(finalWidths).map(([k, v]) => `${k} - ${v}`),
560+
});
561+
}
562+
}, [columns, parentUid, columnWidths]);
563+
456564
const resultHeaderSetFilters = React.useCallback(
457565
(fs: FilterData) => {
458566
setFilters(fs);
@@ -538,6 +646,7 @@ const ResultsTable = ({
538646
}, [extraRowType, setExtraRowUid]);
539647
return (
540648
<HTMLTable
649+
elementRef={tableRef}
541650
style={{
542651
maxHeight: "400px",
543652
overflowY: "scroll",
@@ -554,7 +663,6 @@ const ResultsTable = ({
554663
<ResultHeader
555664
key={c.uid}
556665
c={c}
557-
ref={thRefs}
558666
allResults={allResults}
559667
activeSort={activeSort}
560668
setActiveSort={setActiveSort}
@@ -575,7 +683,9 @@ const ResultsTable = ({
575683
views={views}
576684
onRefresh={onRefresh}
577685
columns={columns}
578-
onWidthUpdate={onWidthUpdate}
686+
onDragStart={onDragStart}
687+
onDrag={onDrag}
688+
onDragEnd={onDragEnd}
579689
/>
580690
{extraRowUid === r.uid && (
581691
<tr className={`roamjs-${extraRowType}-row roamjs-extra-row`}>

0 commit comments

Comments
 (0)