diff --git a/packages/ui/components/ui/data-table.tsx b/packages/ui/components/ui/data-table.tsx index e5e492b5f8..c7f93e87a9 100644 --- a/packages/ui/components/ui/data-table.tsx +++ b/packages/ui/components/ui/data-table.tsx @@ -144,6 +144,59 @@ export function DataTable({ [clampColumnWidth], ); + // Double-click on the resize handle sizes the column to its content, the + // convention every spreadsheet and grid shares. It replaces column.resetSize, + // which cleared the stored width and let TanStack fall back to its generic + // 150 — a number unrelated to any of this table's designed widths, so + // "reset" widened some columns and collapsed others. + // + // Fixed table-layout ignores content, and cells truncate their own text, so + // nothing on screen reports the width the content actually wants. The + // measurement lifts both constraints on the column's cells, reads them, and + // puts everything back within the same task — the browser paints once, after + // the restore, so the intermediate layout is never seen. + const autoFitColumn = React.useCallback( + (header: TanstackHeader) => { + const container = scrollRef.current; + const tableElement = container?.querySelector("table"); + if (!container || !tableElement) return; + const cells = container.querySelectorAll( + `[data-column-id="${CSS.escape(header.column.id)}"]`, + ); + if (!cells.length) return; + + const previousLayout = tableElement.style.tableLayout; + const previous = Array.from(cells, (cell) => ({ + cell, + width: cell.style.width, + maxWidth: cell.style.maxWidth, + overflow: cell.style.overflow, + })); + + tableElement.style.tableLayout = "auto"; + for (const cell of cells) { + cell.style.width = "max-content"; + cell.style.maxWidth = "none"; + cell.style.overflow = "visible"; + } + + let widest = 0; + for (const cell of cells) { + widest = Math.max(widest, cell.getBoundingClientRect().width); + } + + for (const entry of previous) { + entry.cell.style.width = entry.width; + entry.cell.style.maxWidth = entry.maxWidth; + entry.cell.style.overflow = entry.overflow; + } + tableElement.style.tableLayout = previousLayout; + + if (widest > 0) setColumnWidth(header, widest); + }, + [setColumnWidth], + ); + const beginColumnResize = React.useCallback( ( header: TanstackHeader, @@ -423,7 +476,7 @@ export function DataTable({ onDoubleClick={(event) => { event.preventDefault(); event.stopPropagation(); - header.column.resetSize(); + autoFitColumn(header); }} onKeyDown={(event) => handleResizeKeyDown(header, event) @@ -538,6 +591,7 @@ function DataTableBody({ return ( { ).toBe("300px"); }); + it("sizes a column to its widest rendered cell on double-click", () => { + const { onSizingChange, handle } = setup(); + + // Fixed table-layout ignores content and the cells truncate their own + // text, so the measurement lifts both constraints before reading. Stand in + // for the layout jsdom will not perform. + for (const cell of document.querySelectorAll( + '[data-column-id="status"]', + )) { + cell.getBoundingClientRect = () => + ({ width: cell.tagName === "TH" ? 96 : 268 }) as DOMRect; + } + + act(() => { + handle.dispatchEvent(new MouseEvent("dblclick", { bubbles: true })); + }); + + // The widest cell wins, not the header and not the current width. + expect(onSizingChange).toHaveBeenCalledWith( + expect.objectContaining({ status: 268 }), + ); + }); + + it("leaves the column alone when double-clicked with nothing rendered", () => { + const { onSizingChange, handle } = setup(); + + for (const cell of document.querySelectorAll( + '[data-column-id="status"]', + )) { + cell.getBoundingClientRect = () => ({ width: 0 }) as DOMRect; + } + + act(() => { + handle.dispatchEvent(new MouseEvent("dblclick", { bubbles: true })); + }); + + // A zero measurement means the column was never laid out; committing it + // would collapse the column to its minimum for no reason. + expect(onSizingChange).not.toHaveBeenCalled(); + }); + it("casts a shadow past the frozen columns only once scrolled sideways", () => { render();