@@ -327,6 +327,15 @@ type Range = {
327327 /** Picked by row numbers or column letters, and addressed as such: `3:5`, `B:D`. */
328328 readonly whole ?: "rows" | "columns" ;
329329} ;
330+ type Fill = {
331+ readonly columns : number ;
332+ readonly rows : number ;
333+ /** The scroller's inner size, and the header and row heights, in px. */
334+ readonly width : number ;
335+ readonly head : number ;
336+ readonly row : number ;
337+ } ;
338+ const NO_FILL : Fill = { columns : 0 , rows : 0 , width : 0 , head : 0 , row : 0 } ;
330339type TextStyle = {
331340 readonly font : string ;
332341 /** One figure's width with tabular figures on. */
@@ -346,6 +355,10 @@ const COLUMN_MIN_PX = 32;
346355const COLUMN_START_MAX_PX = 320 ;
347356const COLUMN_MAX_PX = 960 ;
348357const RESIZE_STEP_PX = 16 ;
358+ /** The width of the empty columns that run the grid out to its container's edge. */
359+ const FILL_COLUMN_PX = 64 ;
360+ const FILL_COLUMN_LINES =
361+ "linear-gradient(to left, var(--border) 1px, transparent 1px)" ;
349362const RESIZE_KEYS = "Alt+ArrowLeft Alt+ArrowRight Alt+Enter" ;
350363
351364const clamp = ( value : number , min : number , max : number ) =>
@@ -471,6 +484,7 @@ function CsvGrid({
471484 const deferredQuery = useDeferredValue ( query ) ;
472485 const scroller = useRef < HTMLDivElement > ( null ) ;
473486 const overlay = useRef < HTMLDivElement > ( null ) ;
487+ const [ fill , setFill ] = useState < Fill > ( NO_FILL ) ;
474488 const hadFocus = useRef ( false ) ;
475489 const refocus = useRef ( false ) ;
476490
@@ -607,14 +621,58 @@ function CsvGrid({
607621 textWidth (
608622 context ,
609623 metrics . gutter ,
610- ( order . length + 1 ) . toLocaleString ( ) ,
624+ ( order . length + 1 + fill . rows ) . toLocaleString ( ) ,
611625 true ,
612626 ) + metrics . gutter . padding ,
613627 ) ;
614628 const dataWidth =
615629 widths === null || gutterWidth === null
616630 ? null
617631 : gutterWidth + widths . reduce ( ( sum , width ) => sum + width , 0 ) ;
632+ const showsRows = view . length > 0 ;
633+
634+ // Empty gridlines out to the container's edges, as a spreadsheet draws
635+ // them: only as many as the visible area holds, re-counted when it resizes.
636+ useLayoutEffect ( ( ) => {
637+ const box = scroller . current ;
638+ if ( box === null || dataWidth === null ) return ;
639+ function measure ( ) {
640+ if ( box === null || dataWidth === null ) return ;
641+ const head = box . querySelector ( "thead" ) ?. offsetHeight ?? 0 ;
642+ const row = box . querySelector < HTMLElement > ( "tbody th" ) ?. offsetHeight ?? 0 ;
643+ // Whole rows only, so a short sheet never scrolls; the last empty column
644+ // takes what is left, so a wide one never does either.
645+ const next : Fill = {
646+ width : box . clientWidth ,
647+ head,
648+ row,
649+ columns : Math . max (
650+ 0 ,
651+ Math . ceil ( ( box . clientWidth - dataWidth ) / FILL_COLUMN_PX ) ,
652+ ) ,
653+ rows :
654+ row === 0
655+ ? 0
656+ : Math . max (
657+ 0 ,
658+ Math . floor ( ( box . clientHeight - head ) / row ) - shown . length ,
659+ ) ,
660+ } ;
661+ setFill ( ( current ) =>
662+ current . width === next . width &&
663+ current . head === next . head &&
664+ current . row === next . row &&
665+ current . columns === next . columns &&
666+ current . rows === next . rows
667+ ? current
668+ : next ,
669+ ) ;
670+ }
671+ measure ( ) ;
672+ const observer = new ResizeObserver ( measure ) ;
673+ observer . observe ( box ) ;
674+ return ( ) => observer . disconnect ( ) ;
675+ } , [ dataWidth , shown . length ] ) ;
618676
619677 // A filter that drops the focused row moves focus to the nearest row left,
620678 // in sheet order, rather than losing it to the page.
@@ -665,7 +723,7 @@ function CsvGrid({
665723 box . style . top = `${ start . top - origin . top } px` ;
666724 box . style . width = `${ end . right - start . left } px` ;
667725 box . style . height = `${ end . bottom - start . top } px` ;
668- } , [ rowFrom , rowTo , columnFrom , columnTo , lastShown , widths , shown ] ) ;
726+ } , [ rowFrom , rowTo , columnFrom , columnTo , lastShown , widths , shown , fill ] ) ;
669727
670728 const resizeColumn = useCallback ( ( column : number , width : number ) => {
671729 setWidths (
@@ -951,6 +1009,7 @@ function CsvGrid({
9511009 ...( summary . numbers > 0 ? [ wording . sum ( summary . sum ) ] : [ ] ) ,
9521010 ] ;
9531011 const frozen = widths !== null && gutterWidth !== null && dataWidth !== null ;
1012+ const fillers = Array . from ( { length : fill . columns } , ( _ , index ) => index ) ;
9541013
9551014 return (
9561015 < div className = "flex h-full min-h-0 min-w-0 flex-col gap-2" >
@@ -972,6 +1031,18 @@ function CsvGrid({
9721031 </ div >
9731032 < div ref = { scroller } className = "min-h-0 flex-[1_1_32rem] overflow-auto" >
9741033 < div className = "relative isolate w-max min-w-full" >
1034+ { frozen && fill . columns > 0 ? (
1035+ < div
1036+ aria-hidden
1037+ className = "absolute inset-y-0 right-0 -z-1"
1038+ style = { {
1039+ left : dataWidth ,
1040+ backgroundImage : `${ FILL_COLUMN_LINES } , linear-gradient(to top, var(--border) 1px, transparent 1px)` ,
1041+ backgroundSize : `${ FILL_COLUMN_PX } px 100%, 100% ${ fill . row } px` ,
1042+ backgroundPosition : `0 0, 0 ${ fill . head } px` ,
1043+ } }
1044+ />
1045+ ) : null }
9751046 < table
9761047 role = "grid"
9771048 aria-label = { caption }
@@ -981,7 +1052,9 @@ function CsvGrid({
9811052 "border-separate border-spacing-0" ,
9821053 frozen ? "table-fixed" : "min-w-full [&_td]:max-w-80" ,
9831054 ) }
984- style = { frozen ? { width : dataWidth } : undefined }
1055+ style = {
1056+ frozen ? { width : Math . max ( dataWidth , fill . width ) } : undefined
1057+ }
9851058 onKeyDown = { onKeyDown }
9861059 onFocus = { ( ) => {
9871060 hadFocus . current = true ;
@@ -998,6 +1071,17 @@ function CsvGrid({
9981071 { widths . map ( ( width , index ) => (
9991072 < col key = { index } style = { { width } } />
10001073 ) ) }
1074+ { fillers . map ( ( index ) => (
1075+ < col
1076+ key = { `fill-${ index } ` }
1077+ style = { {
1078+ width :
1079+ index === fill . columns - 1
1080+ ? undefined
1081+ : FILL_COLUMN_PX ,
1082+ } }
1083+ />
1084+ ) ) }
10011085 </ colgroup >
10021086 ) : null }
10031087 < thead className = "sticky top-0 z-3" >
@@ -1035,6 +1119,15 @@ function CsvGrid({
10351119 { columnLetter ( index ) }
10361120 </ th >
10371121 ) ) }
1122+ { fillers . map ( ( index ) => (
1123+ < th
1124+ key = { `fill-${ index } ` }
1125+ aria-hidden
1126+ className = "h-6 border-r border-b border-border bg-card px-1 text-center font-normal text-muted-foreground"
1127+ >
1128+ { columnLetter ( columnCount + index ) }
1129+ </ th >
1130+ ) ) }
10381131 </ tr >
10391132 < tr aria-rowindex = { 2 } >
10401133 < th
@@ -1084,6 +1177,17 @@ function CsvGrid({
10841177 </ th >
10851178 ) ;
10861179 } ) }
1180+ { fill . columns > 0 ? (
1181+ < th
1182+ aria-hidden
1183+ colSpan = { fill . columns }
1184+ className = "h-8 border-b border-border bg-background"
1185+ style = { {
1186+ backgroundImage : FILL_COLUMN_LINES ,
1187+ backgroundSize : `${ FILL_COLUMN_PX } px 100%` ,
1188+ } }
1189+ />
1190+ ) : null }
10871191 </ tr >
10881192 </ thead >
10891193 < tbody className = "select-none" >
@@ -1104,6 +1208,21 @@ function CsvGrid({
11041208 />
11051209 ) ;
11061210 } ) }
1211+ { showsRows
1212+ ? Array . from ( { length : fill . rows } , ( _ , index ) => (
1213+ < tr key = { `fill-${ index } ` } aria-hidden >
1214+ < th className = "sticky left-0 z-2 h-7 border-r border-b border-border bg-card px-2 text-right font-normal text-muted-foreground tabular-nums" >
1215+ { ( order . length + 2 + index ) . toLocaleString ( ) }
1216+ </ th >
1217+ { sheet . columns . map ( ( _ , column ) => (
1218+ < td
1219+ key = { column }
1220+ className = "border-r border-b border-border"
1221+ />
1222+ ) ) }
1223+ </ tr >
1224+ ) )
1225+ : null }
11071226 </ tbody >
11081227 </ table >
11091228 < div
0 commit comments