Skip to content

Commit 60e3500

Browse files
Refactor Table to use flex layouts (#2622)
1 parent 29a0d7e commit 60e3500

File tree

4 files changed

+136
-231
lines changed

4 files changed

+136
-231
lines changed

packages/gitbook/src/components/DocumentView/Table/RecordColumnValue.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { ContentRef, DocumentBlockTable } from '@gitbook/api';
22
import { Icon } from '@gitbook/icons';
33
import assertNever from 'assert-never';
44

5-
import { Checkbox, Emoji } from '@/components/primitives';
5+
import { Checkbox } from '@/components/primitives';
66
import { StyledLink } from '@/components/primitives';
77
import { Image } from '@/components/utils';
88
import { getNodeFragmentByName } from '@/lib/document';
Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,43 @@
11
import { DocumentTableViewGrid } from '@gitbook/api';
2-
3-
import { tcls } from '@/lib/tailwind';
2+
import React from 'react';
43

54
import { RecordColumnValue } from './RecordColumnValue';
65
import { TableRecordKV, TableViewProps } from './Table';
6+
import styles from './table.module.css';
7+
import { getColumnWidth } from './ViewGrid';
78

8-
export async function RecordRow(
9+
export function RecordRow(
910
props: TableViewProps<DocumentTableViewGrid> & {
1011
record: TableRecordKV;
12+
autoSizedColumns: string[];
13+
fixedColumns: string[];
1114
},
1215
) {
13-
const { view } = props;
14-
const columnsLengthThreshold = view.columns.length >= 7;
15-
16-
const tableTR = columnsLengthThreshold
17-
? ['[&>*+*]:border-l', '[&>*]:px-4']
18-
: ['[&>*+*]:border-l', '[&>*+*]:pl-4'];
16+
const { view, autoSizedColumns, fixedColumns } = props;
1917

2018
return (
21-
<tr className={tcls(tableTR, 'border-dark/3', 'dark:border-light/2')}>
19+
<div className={styles.row} role="row">
2220
{view.columns.map((column) => {
21+
const columnWidth = getColumnWidth({
22+
column,
23+
columnWidths: view.columnWidths,
24+
autoSizedColumns,
25+
fixedColumns,
26+
});
2327
return (
24-
<td
28+
<div
2529
key={column}
26-
className={tcls(
27-
'align-middle',
28-
'min-w-[8rem]',
29-
'border-dark/2',
30-
'py-3',
31-
'text-sm',
32-
'lg:text-base',
33-
'dark:border-light/2',
34-
)}
30+
role="cell"
31+
className={styles.cell}
32+
style={{
33+
width: columnWidth,
34+
minWidth: columnWidth || '100px',
35+
}}
3536
>
36-
<RecordColumnValue key={column} {...props} column={column} />
37-
</td>
37+
<RecordColumnValue {...props} column={column} />
38+
</div>
3839
);
3940
})}
40-
</tr>
41+
</div>
4142
);
4243
}
Lines changed: 79 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { DocumentTableViewGrid } from '@gitbook/api';
2+
import * as React from 'react';
23

34
import { tcls } from '@/lib/tailwind';
45

@@ -7,159 +8,106 @@ import { TableViewProps } from './Table';
78
import styles from './table.module.css';
89
import { getColumnAlignment } from './utils';
910

11+
/* Columns are sized in 3 ways:
12+
1. Set to auto-size by default, these columns share the available width
13+
2. Explicitly set by the user by dragging column separator (we then turn off auto-size)
14+
3. Auto-size is turned off without setting a width, we then default to a fixed width of 100px
15+
*/
1016
export function ViewGrid(props: TableViewProps<DocumentTableViewGrid>) {
1117
const { block, view, records, style } = props;
12-
const columnsOverThreshold = view.columns.length >= 7;
1318

14-
const tableWrapper = columnsOverThreshold
15-
? [
16-
// has over X columns
17-
'overflow-x-auto',
18-
'overflow-y-hidden',
19-
'mx-auto',
20-
'rounded-md',
21-
'border',
22-
'border-dark/3',
23-
'dark:border-light/2',
24-
]
25-
: ['overflow-x-auto', 'overflow-y-hidden', 'mx-auto'];
19+
/* Calculate how many columns are auto-sized vs fixed width */
20+
const columnWidths = view.columnWidths;
21+
const autoSizedColumns = view.columns.filter((column) => !columnWidths?.[column]);
22+
const fixedColumns = view.columns.filter((column) => columnWidths?.[column]);
2623

27-
const tableTR = columnsOverThreshold
28-
? ['[&>*+*]:border-l', '[&>*]:px-4']
29-
: ['[&>*+*]:border-l', '[&>*+*]:px-4'];
24+
const tableWidth = autoSizedColumns.length > 0 ? 'w-full' : 'w-fit';
3025

31-
const tableTH = columnsOverThreshold ? ['py-3'] : ['py-1', 'pt-0'];
32-
33-
// Only show the header when configured and not empty
26+
/* Only show the header when configured and not empty */
3427
const withHeader =
3528
!view.hideHeader &&
3629
view.columns.some((columnId) => block.data.definition[columnId].title.trim().length > 0);
3730

3831
return (
39-
<div
40-
className={`${tcls(style, 'relative', 'grid', tableWrapper, styles.progressContainer)}`}
41-
>
42-
{/* ProgressScroller: */}
43-
<div
44-
className={`${styles.progressOpacitySharp} ${tcls(
45-
'grid',
46-
'items-center',
47-
'grid-area-1-1',
48-
'w-[5rem]',
49-
'h-full',
50-
'top-0',
51-
'z-[1]',
52-
'sticky',
53-
'left-[calc(100%-5rem)]',
54-
'pointer-events-none',
55-
)}`}
56-
>
57-
<svg
58-
className={`${styles.progressSvg} ${tcls(
59-
'grid-area-1-1',
60-
'relative',
61-
'[strokeDasharray:_0_100]',
62-
'z-[1]',
63-
'w-7',
64-
'mt-3',
65-
'mr-3',
66-
'self-start',
67-
'justify-self-end',
68-
'stroke-primary-600',
69-
'shadow-1xs',
70-
'bg-light',
71-
'ring-1',
72-
'ring-inset',
73-
'rounded-full',
74-
'ring-dark/2',
75-
'dark:ring-light/2',
76-
'dark:bg-dark',
77-
'dark:stroke-primary-400',
78-
)}`}
79-
preserveAspectRatio="xMaxYMid meet"
80-
width="100%"
81-
viewBox="0 0 26 26"
82-
fill="none"
83-
xmlns="http://www.w3.org/2000/svg"
84-
>
85-
<circle
86-
cx="13"
87-
className={`${styles.strokeOpacityProgressInverted}`}
88-
cy="13"
89-
r="12.5"
90-
fill="none"
91-
stroke="inherit"
92-
strokeWidth="1.5"
93-
pathLength="100"
94-
strokeLinecap="round"
95-
strokeOpacity={0}
96-
/>
97-
98-
<path
99-
strokeDasharray="none"
100-
d="M12 10L15 13L12 16"
101-
stroke="inherit"
102-
fill="none"
103-
strokeOpacity={0.64}
104-
/>
105-
</svg>
106-
107-
<div
108-
className={`${styles.progressOpacity} ${tcls(
109-
'bg-gradient-to-r',
110-
'from-transparent',
111-
'to-light',
112-
'to-40%',
113-
'grid-area-1-1',
114-
'w-full',
115-
'h-full',
116-
'dark:from-transparent',
117-
'dark:to-dark/10',
118-
)}`}
119-
></div>
120-
</div>
121-
122-
{/* Table: */}
123-
<table className={tcls('w-full', 'grid-area-1-1', 'table-auto')}>
124-
{withHeader ? (
125-
<thead>
126-
<tr className={tcls(tableTR)}>
32+
<div className={tcls(style, styles.tableWrapper)}>
33+
{/* Table */}
34+
<div role="table" className={tcls('flex', 'flex-col')}>
35+
{/* Header */}
36+
{withHeader && (
37+
<div role="rowgroup" className={tcls('flex flex-col', tableWidth)}>
38+
<div role="row" className={tcls('flex', 'w-full', '[&>*+*]:border-l')}>
12739
{view.columns.map((column) => {
128-
const columnWidth = view.columnWidths?.[column];
12940
const alignment = getColumnAlignment(block.data.definition[column]);
130-
13141
return (
132-
<th
42+
<div
13343
key={column}
44+
role="columnheader"
13445
className={tcls(
135-
tableTH,
136-
'align-middle',
137-
'text-balance',
138-
'border-b',
139-
'border-b-dark/5',
140-
'text-left',
141-
'text-xs',
142-
'lg:text-base',
143-
'dark:border-l-light/2',
144-
'dark:border-b-light/4',
46+
styles.columnHeader,
14547
alignment === 'right' ? 'text-right' : null,
14648
alignment === 'center' ? 'text-center' : null,
14749
)}
148-
style={columnWidth ? { width: columnWidth } : undefined}
50+
style={{
51+
width: getColumnWidth({
52+
column,
53+
columnWidths,
54+
autoSizedColumns,
55+
fixedColumns,
56+
}),
57+
minWidth: columnWidths?.[column] || '100px',
58+
}}
59+
title={block.data.definition[column].title}
14960
>
15061
{block.data.definition[column].title}
151-
</th>
62+
</div>
15263
);
15364
})}
154-
</tr>
155-
</thead>
156-
) : null}
157-
<tbody className={tcls('[&>*+*]:border-t')}>
158-
{records.map((record) => {
159-
return <RecordRow key={record[0]} {...props} record={record} />;
160-
})}
161-
</tbody>
162-
</table>
65+
</div>
66+
</div>
67+
)}
68+
<div
69+
role="rowgroup"
70+
className={tcls('flex', 'flex-col', tableWidth, '[&>*+*]:border-t')}
71+
>
72+
{records.map((record) => (
73+
<RecordRow
74+
key={record[0]}
75+
record={record}
76+
autoSizedColumns={autoSizedColumns}
77+
fixedColumns={fixedColumns}
78+
{...props}
79+
/>
80+
))}
81+
</div>
82+
</div>
16383
</div>
16484
);
16585
}
86+
87+
export const getColumnWidth = ({
88+
column,
89+
columnWidths,
90+
autoSizedColumns,
91+
fixedColumns,
92+
}: {
93+
column: string;
94+
columnWidths: Record<string, number> | undefined;
95+
autoSizedColumns: string[];
96+
fixedColumns: string[];
97+
}) => {
98+
const columnWidth = columnWidths?.[column];
99+
100+
/* Column was explicitly set by user or user turned off auto-sizing (in that case, columnWidth should've also been set to 100px) */
101+
if (columnWidth) return `${columnWidth}px`;
102+
103+
/* Fallback minimum width for columns, so the columns don't become unreadable from being too narrow and instead table will become scrollable. */
104+
const minAutoColumnWidth = '100px';
105+
106+
const totalFixedWidth = fixedColumns.reduce((sum, col) => {
107+
return sum + (columnWidths?.[col] || 0);
108+
}, 0);
109+
110+
/* Column should use auto-sizing, which means it grows to fill available space */
111+
const availableWidth = `calc((100% - ${totalFixedWidth}px) / ${autoSizedColumns.length})`;
112+
return `clamp(${minAutoColumnWidth}, ${availableWidth}, 100%)`;
113+
};

0 commit comments

Comments
 (0)