diff --git a/src/ui/csv-table.tsx b/src/ui/csv-table.tsx index b0e6ba7..9c5a2d3 100644 --- a/src/ui/csv-table.tsx +++ b/src/ui/csv-table.tsx @@ -327,6 +327,15 @@ type Range = { /** Picked by row numbers or column letters, and addressed as such: `3:5`, `B:D`. */ readonly whole?: "rows" | "columns"; }; +type Fill = { + readonly columns: number; + readonly rows: number; + /** The scroller's inner size, and the header and row heights, in px. */ + readonly width: number; + readonly head: number; + readonly row: number; +}; +const NO_FILL: Fill = { columns: 0, rows: 0, width: 0, head: 0, row: 0 }; type TextStyle = { readonly font: string; /** One figure's width with tabular figures on. */ @@ -346,6 +355,10 @@ const COLUMN_MIN_PX = 32; const COLUMN_START_MAX_PX = 320; const COLUMN_MAX_PX = 960; const RESIZE_STEP_PX = 16; +/** The width of the empty columns that run the grid out to its container's edge. */ +const FILL_COLUMN_PX = 64; +const FILL_COLUMN_LINES = + "linear-gradient(to left, var(--border) 1px, transparent 1px)"; const RESIZE_KEYS = "Alt+ArrowLeft Alt+ArrowRight Alt+Enter"; const clamp = (value: number, min: number, max: number) => @@ -471,6 +484,7 @@ function CsvGrid({ const deferredQuery = useDeferredValue(query); const scroller = useRef(null); const overlay = useRef(null); + const [fill, setFill] = useState(NO_FILL); const hadFocus = useRef(false); const refocus = useRef(false); @@ -607,7 +621,7 @@ function CsvGrid({ textWidth( context, metrics.gutter, - (order.length + 1).toLocaleString(), + (order.length + 1 + fill.rows).toLocaleString(), true, ) + metrics.gutter.padding, ); @@ -615,6 +629,50 @@ function CsvGrid({ widths === null || gutterWidth === null ? null : gutterWidth + widths.reduce((sum, width) => sum + width, 0); + const showsRows = view.length > 0; + + // Empty gridlines out to the container's edges, as a spreadsheet draws + // them: only as many as the visible area holds, re-counted when it resizes. + useLayoutEffect(() => { + const box = scroller.current; + if (box === null || dataWidth === null) return; + function measure() { + if (box === null || dataWidth === null) return; + const head = box.querySelector("thead")?.offsetHeight ?? 0; + const row = box.querySelector("tbody th")?.offsetHeight ?? 0; + // Whole rows only, so a short sheet never scrolls; the last empty column + // takes what is left, so a wide one never does either. + const next: Fill = { + width: box.clientWidth, + head, + row, + columns: Math.max( + 0, + Math.ceil((box.clientWidth - dataWidth) / FILL_COLUMN_PX), + ), + rows: + row === 0 + ? 0 + : Math.max( + 0, + Math.floor((box.clientHeight - head) / row) - shown.length, + ), + }; + setFill((current) => + current.width === next.width && + current.head === next.head && + current.row === next.row && + current.columns === next.columns && + current.rows === next.rows + ? current + : next, + ); + } + measure(); + const observer = new ResizeObserver(measure); + observer.observe(box); + return () => observer.disconnect(); + }, [dataWidth, shown.length]); // A filter that drops the focused row moves focus to the nearest row left, // in sheet order, rather than losing it to the page. @@ -665,7 +723,7 @@ function CsvGrid({ box.style.top = `${start.top - origin.top}px`; box.style.width = `${end.right - start.left}px`; box.style.height = `${end.bottom - start.top}px`; - }, [rowFrom, rowTo, columnFrom, columnTo, lastShown, widths, shown]); + }, [rowFrom, rowTo, columnFrom, columnTo, lastShown, widths, shown, fill]); const resizeColumn = useCallback((column: number, width: number) => { setWidths( @@ -951,6 +1009,7 @@ function CsvGrid({ ...(summary.numbers > 0 ? [wording.sum(summary.sum)] : []), ]; const frozen = widths !== null && gutterWidth !== null && dataWidth !== null; + const fillers = Array.from({ length: fill.columns }, (_, index) => index); return (
@@ -972,6 +1031,18 @@ function CsvGrid({
+ {frozen && fill.columns > 0 ? ( +
+ ) : null} { hadFocus.current = true; @@ -998,6 +1071,17 @@ function CsvGrid({ {widths.map((width, index) => ( ))} + {fillers.map((index) => ( + + ))} ) : null} @@ -1035,6 +1119,15 @@ function CsvGrid({ {columnLetter(index)} ))} + {fillers.map((index) => ( + + ))} @@ -1104,6 +1208,21 @@ function CsvGrid({ /> ); })} + {showsRows + ? Array.from({ length: fill.rows }, (_, index) => ( + + + {sheet.columns.map((_, column) => ( + + )) + : null}
+ {columnLetter(columnCount + index)} +
); })} + {fill.columns > 0 ? ( + + ) : null}
+ {(order.length + 2 + index).toLocaleString()} + + ))} +