@moneyforward/mfui-components 4.7.0 → 4.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/DataTable/DataTable.js +38 -5
- package/dist/src/DataTable/DataTable.types.d.ts +101 -0
- package/dist/src/DataTable/DataTableHeaderCell/DataTableHeaderCell.js +5 -2
- package/dist/src/DataTable/DataTableHeaderRow/DataTableHeaderRow.js +31 -15
- package/dist/src/DataTable/DataTableProvider.d.ts +19 -4
- package/dist/src/DataTable/DataTableProvider.js +11 -3
- package/dist/src/DataTable/DataTableRow/DataTableRow.js +21 -12
- package/dist/src/DataTable/hooks/useVirtualizedColumns.d.ts +73 -0
- package/dist/src/DataTable/hooks/useVirtualizedColumns.js +109 -0
- package/dist/src/DataTable/index.d.ts +1 -1
- package/dist/src/DataTable/shared/commonStyles.d.ts +11 -0
- package/dist/src/DataTable/shared/commonStyles.js +11 -0
- package/dist/src/DataTable/utils/renderVirtualizedColumns.d.ts +42 -0
- package/dist/src/DataTable/utils/renderVirtualizedColumns.js +40 -0
- package/dist/src/DataTable/utils/resolveColumnWidths.d.ts +28 -0
- package/dist/src/DataTable/utils/resolveColumnWidths.js +41 -0
- package/dist/src/MultipleSelectBox/MultipleSelectBox.d.ts +1 -1
- package/dist/src/MultipleSelectBox/MultipleSelectBox.js +10 -7
- package/dist/src/MultipleSelectBox/MultipleSelectBox.types.d.ts +33 -0
- package/dist/src/Popover/Popover.d.ts +7 -0
- package/dist/src/Popover/Popover.js +7 -0
- package/dist/src/SelectBox/SelectBox.js +6 -4
- package/dist/src/SelectBox/SelectBox.types.d.ts +33 -0
- package/dist/src/SelectBox/SelectBoxOption.d.ts +5 -1
- package/dist/src/SelectBox/SelectBoxOption.js +3 -4
- package/dist/src/SelectBox/SelectBoxOptionGroup.d.ts +5 -1
- package/dist/src/SelectBox/SelectBoxOptionGroup.js +2 -2
- package/dist/src/shared/OptionLabel/OptionLabel.d.ts +45 -0
- package/dist/src/shared/OptionLabel/OptionLabel.js +9 -0
- package/dist/src/shared/OptionLabel/index.d.ts +1 -0
- package/dist/src/shared/OptionLabel/index.js +1 -0
- package/dist/styled-system/recipes/data-table-slot-recipe.d.ts +5 -1
- package/dist/styled-system/recipes/data-table-slot-recipe.js +19 -1
- package/dist/styled-system/tokens/index.js +4 -0
- package/dist/styled-system/tokens/tokens.d.ts +1 -1
- package/dist/styles.css +33 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +7 -7
|
@@ -15,8 +15,11 @@ import { cx } from '../../styled-system/css';
|
|
|
15
15
|
import { isComponentOrWrapped } from '../utilities/react/isComponentOrWrapped';
|
|
16
16
|
import { mergeRefs } from '../utilities/dom/mergeRefs';
|
|
17
17
|
import { useTableEdgeCellMarkers } from '../utilities/dom/useTableEdgeCellMarkers';
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
import { useVirtualizedColumns } from './hooks/useVirtualizedColumns';
|
|
19
|
+
import { flattenChildren } from '../utilities/react/flattenChildren';
|
|
20
|
+
import { resolveColumnWidths } from './utils/resolveColumnWidths';
|
|
21
|
+
const Base = forwardRef(({ className, size = 'medium', enableRowSelection = false, enableRowClickSelection = false, fixedHeader = false, showVerticalBorder = false, leftFixedColumnIndex, rightFixedColumnIndex, layout = 'default', wrapperProps, loading = false, emptyStateSlot, enableColumnVirtualization = false, columnVirtualizationOptions, children, ...props }, ref) => {
|
|
22
|
+
const classes = dataTableSlotRecipe({ layout, columnVirtualization: enableColumnVirtualization });
|
|
20
23
|
const tableRef = useRef(null);
|
|
21
24
|
// A virtualized body needs the element that owns `overflow: auto`, which is the wrapper below.
|
|
22
25
|
// A ref object would still be `null` when the body's layout effects run, because host refs are
|
|
@@ -32,6 +35,34 @@ const Base = forwardRef(({ className, size = 'medium', enableRowSelection = fals
|
|
|
32
35
|
setHeaderRowCount((count) => count - 1);
|
|
33
36
|
};
|
|
34
37
|
}, []);
|
|
38
|
+
// The header row counts all of its cells, mounted or not, and the column virtualizer windows that count.
|
|
39
|
+
const [numberColumns, setNumberColumns] = useState(0);
|
|
40
|
+
// The header row holds the element of every column, mounted or not, so it reports the
|
|
41
|
+
// `style.width` of each header cell for the `<colgroup>` of a table that windows its columns.
|
|
42
|
+
const [declaredColumnWidths, setDeclaredColumnWidths] = useState([]);
|
|
43
|
+
const reportDeclaredColumnWidths = useCallback((widths) => {
|
|
44
|
+
// The header row reports a new array on every render of its children, which must not re-render
|
|
45
|
+
// the whole table when no width changed.
|
|
46
|
+
setDeclaredColumnWidths((current) => current.length === widths.length && current.every((width, index) => width === widths[index]) ? current : widths);
|
|
47
|
+
}, []);
|
|
48
|
+
const columnWidths = resolveColumnWidths(enableColumnVirtualization ? declaredColumnWidths : [], columnVirtualizationOptions?.getColumnWidth);
|
|
49
|
+
// No single row owns the columns, so the window is computed here once and every row applies it
|
|
50
|
+
// to its own cells. The header and the body therefore always mount the same columns.
|
|
51
|
+
const columnWindow = useVirtualizedColumns({
|
|
52
|
+
enabled: enableColumnVirtualization,
|
|
53
|
+
scrollElement,
|
|
54
|
+
tableRef,
|
|
55
|
+
columnCount: numberColumns,
|
|
56
|
+
selectionColumnCount: enableRowSelection ? 1 : 0,
|
|
57
|
+
columnWidths,
|
|
58
|
+
leftFixedColumnIndex,
|
|
59
|
+
rightFixedColumnIndex,
|
|
60
|
+
overscan: columnVirtualizationOptions?.overscan,
|
|
61
|
+
});
|
|
62
|
+
// A consumer `<colgroup>` replaces the table's own. Only an element of type `'colgroup'` among the
|
|
63
|
+
// direct children (Fragments expanded) is detected, as documented on `enableColumnVirtualization`.
|
|
64
|
+
const hasConsumerColgroup = useMemo(() => enableColumnVirtualization &&
|
|
65
|
+
flattenChildren(children).some((child) => isValidElement(child) && child.type === 'colgroup'), [enableColumnVirtualization, children]);
|
|
35
66
|
// `:first-child` / `:last-child` cannot tell which grid column a cell renders in, so the
|
|
36
67
|
// table-edge spacing of the cell recipes misfires on the rows a `rowSpan` extends into.
|
|
37
68
|
// This marks the displaced cells so that spacing can be cancelled on them.
|
|
@@ -65,9 +96,11 @@ const Base = forwardRef(({ className, size = 'medium', enableRowSelection = fals
|
|
|
65
96
|
// A virtualized body holds only a window of its rows, so the mounted rows are not the row count.
|
|
66
97
|
// This is spread before `props` so that a consumer-supplied `aria-rowcount` still wins.
|
|
67
98
|
const ariaRowCountProps = virtualizedRowCount !== null ? { 'aria-rowcount': virtualizedRowCount + headerRowCount } : undefined;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
99
|
+
// The same holds for the columns: only a window of the cells of each row is mounted.
|
|
100
|
+
const ariaColCountProps = columnWindow !== null && numberColumns > 0 ? { 'aria-colcount': numberColumns } : undefined;
|
|
101
|
+
return (_jsx(DataTableProvider, { size: size, enableRowSelection: enableRowSelection, enableRowClickSelection: enableRowClickSelection, fixedHeader: fixedHeader, showVerticalBorder: showVerticalBorder, leftFixedColumnIndex: leftFixedColumnIndex, rightFixedColumnIndex: rightFixedColumnIndex, loading: loading, layout: layout, scrollElement: scrollElement, virtualizedRowCount: virtualizedRowCount, setVirtualizedRowCount: setVirtualizedRowCount, headerRowCount: headerRowCount, registerHeaderRow: registerHeaderRow, columnWindow: columnWindow, reportDeclaredColumnWidths: reportDeclaredColumnWidths, numberColumns: numberColumns, setNumberColumns: setNumberColumns, children: _jsxs("div", { ...wrapperProps, ref: setScrollElement, className: cx(classes.wrapper, 'mfui-DataTable__wrapper',
|
|
102
|
+
/* TODO: Remove in v5.0.0 */ 'mfui-DataGrid__wrapper', wrapperProps?.className), children: [_jsxs("table", { ref: mergeRefs(ref, tableRef), className: cx(classes.table, 'mfui-DataTable__table',
|
|
103
|
+
/* TODO: Remove in v5.0.0 */ 'mfui-DataGrid__table', className), ...ariaRowCountProps, ...ariaColCountProps, ...props, children: [enableColumnVirtualization && !hasConsumerColgroup ? (_jsxs("colgroup", { children: [enableRowSelection ? (_jsx("col", { className: cx(classes.selectionColumn, 'mfui-DataTable__selectionColumn') })) : null, columnWidths.map((width, index) => (_jsx("col", { style: { width } }, index)))] })) : null, children] }), shouldShowEmptyState ? (_jsx("div", { className: cx(classes.emptyState, 'mfui-DataTable__emptyState',
|
|
71
104
|
/* TODO: Remove in v5.0.0 */ 'mfui-DataGrid__emptyState'), role: "status", "aria-live": "polite", children: emptyStateSlot })) : null] }) }));
|
|
72
105
|
});
|
|
73
106
|
Base.displayName = 'DataTable';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ComponentPropsWithRef, type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
|
2
|
+
import { type VirtualizerOptions } from '@tanstack/react-virtual';
|
|
2
3
|
import { type DataTableSize } from './DataTableProvider';
|
|
3
4
|
import { type DataTableSlotRecipeVariant } from '../../styled-system/recipes';
|
|
4
5
|
/**
|
|
@@ -18,6 +19,19 @@ export type CellDataType = 'text' | 'amount';
|
|
|
18
19
|
* @param checkbox - The built-in selection checkbox element.
|
|
19
20
|
*/
|
|
20
21
|
export type DataTableSelectionCheckboxWrapper = (checkbox: ReactNode) => ReactNode;
|
|
22
|
+
/**
|
|
23
|
+
* The options for the column virtualization of the `DataTable` component.
|
|
24
|
+
*/
|
|
25
|
+
export type DataTableColumnVirtualizationOptions = {
|
|
26
|
+
/**
|
|
27
|
+
* Returns the width of a column in pixels.
|
|
28
|
+
*
|
|
29
|
+
* `index` counts the columns as `leftFixedColumnIndex` does, without the selection column.
|
|
30
|
+
* Return `undefined` to fall back to the `style.width` of the column's `DataTable.HeaderCell`,
|
|
31
|
+
* and then to 160px.
|
|
32
|
+
*/
|
|
33
|
+
getColumnWidth?: (index: number) => number | undefined;
|
|
34
|
+
} & Pick<VirtualizerOptions<HTMLElement, Element>, 'overscan'>;
|
|
21
35
|
/**
|
|
22
36
|
* The props for the `DataTable` component.
|
|
23
37
|
*
|
|
@@ -142,4 +156,91 @@ export type DataTableProps = {
|
|
|
142
156
|
* @default undefined
|
|
143
157
|
*/
|
|
144
158
|
emptyStateSlot?: ReactNode;
|
|
159
|
+
/**
|
|
160
|
+
* Whether to render only the columns near the viewport (column virtualization).
|
|
161
|
+
*
|
|
162
|
+
* When enabled, the header row and every row mount a window of their cells instead of all of them,
|
|
163
|
+
* and reserve the width of the columns they leave out with a spacer cell on each side of the window.
|
|
164
|
+
* This removes the DOM and mount cost of a wide table, and composes with `enableVirtualization` on
|
|
165
|
+
* `DataTable.Body` to window a table in both directions.
|
|
166
|
+
*
|
|
167
|
+
* The first column, the columns up to `leftFixedColumnIndex` and the columns from
|
|
168
|
+
* `rightFixedColumnIndex` are always mounted; only the columns between them are windowed.
|
|
169
|
+
*
|
|
170
|
+
* Layout:
|
|
171
|
+
*
|
|
172
|
+
* - The table switches to `table-layout: fixed`, because the cells of a row stay in their columns only
|
|
173
|
+
* when every column takes its width from a `<col>`. A `tableLayout` you set through `style` or
|
|
174
|
+
* `className` still wins.
|
|
175
|
+
* - The table renders a `<colgroup>` with one `<col>` per column, including the selection column when
|
|
176
|
+
* `enableRowSelection` is `true`. Each column takes the first width of:
|
|
177
|
+
* 1. `columnVirtualizationOptions.getColumnWidth(index)`, when it returns a width.
|
|
178
|
+
* 2. The `style.width` of the column's `DataTable.HeaderCell`, when it is a number or a `px` string.
|
|
179
|
+
* Other units (`%`, `rem`, `calc()`) are ignored, because the window is computed in pixels before
|
|
180
|
+
* anything is laid out.
|
|
181
|
+
* 3. 160px.
|
|
182
|
+
* - Content wider than its column overflows the cell under the fixed layout instead of widening the
|
|
183
|
+
* column. Truncate it, e.g. with `text-overflow: ellipsis`, or give the column a wider width.
|
|
184
|
+
* - To declare the columns yourself, render a `<colgroup>` as a direct child of `DataTable` (a
|
|
185
|
+
* Fragment around it is fine). The table then renders none of its own. It detects yours by the
|
|
186
|
+
* element type `'colgroup'` only, so a `<colgroup>` rendered by your own component is not detected
|
|
187
|
+
* and the table would end up with two (a warning is logged in development). Keep the widths of
|
|
188
|
+
* your `<col>` elements in sync with the resolved widths above, since the window is computed from those.
|
|
189
|
+
* `getColumnWidthFromContentWidth` turns a content width into the matching column width.
|
|
190
|
+
* - The table has to render a `DataTable.HeaderRow`, which is what the columns are counted from.
|
|
191
|
+
*
|
|
192
|
+
* `aria-colcount` on the table and `aria-colindex` on each cell are set for you, so that assistive
|
|
193
|
+
* technology is told the real column count rather than the number of mounted cells. Both stay
|
|
194
|
+
* overridable by the values you pass yourself.
|
|
195
|
+
*
|
|
196
|
+
* **Not supported together with merged cells.** A `colSpan` / `rowSpan` shifts the source-order
|
|
197
|
+
* index the window is resolved by, so the wrong cells are mounted.
|
|
198
|
+
*
|
|
199
|
+
* @default false
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* ```tsx
|
|
203
|
+
* <div style={{ height: 500 }}>
|
|
204
|
+
* <DataTable enableColumnVirtualization leftFixedColumnIndex={0}>
|
|
205
|
+
* <DataTable.Header>
|
|
206
|
+
* <DataTable.HeaderRow>
|
|
207
|
+
* {columns.map((column) => (
|
|
208
|
+
* <DataTable.HeaderCell key={column.id} style={{ width: column.width }}>
|
|
209
|
+
* {column.label}
|
|
210
|
+
* </DataTable.HeaderCell>
|
|
211
|
+
* ))}
|
|
212
|
+
* </DataTable.HeaderRow>
|
|
213
|
+
* </DataTable.Header>
|
|
214
|
+
* <DataTable.Body>
|
|
215
|
+
* {rows.map((row) => (
|
|
216
|
+
* <DataTable.Row key={row.id}>
|
|
217
|
+
* {columns.map((column) => (
|
|
218
|
+
* <DataTable.Cell key={column.id}>{row[column.id]}</DataTable.Cell>
|
|
219
|
+
* ))}
|
|
220
|
+
* </DataTable.Row>
|
|
221
|
+
* ))}
|
|
222
|
+
* </DataTable.Body>
|
|
223
|
+
* </DataTable>
|
|
224
|
+
* </div>
|
|
225
|
+
* ```
|
|
226
|
+
*/
|
|
227
|
+
enableColumnVirtualization?: boolean;
|
|
228
|
+
/**
|
|
229
|
+
* The configuration of the column virtualization.
|
|
230
|
+
* Only applies when `enableColumnVirtualization` is `true`.
|
|
231
|
+
*
|
|
232
|
+
* @property getColumnWidth - Returns the width of a column in pixels. It receives the column index counted
|
|
233
|
+
* as `leftFixedColumnIndex` is, without the selection column. It takes precedence over the `style.width`
|
|
234
|
+
* of the header cell; return `undefined` to fall back to it, and then to 160px.
|
|
235
|
+
* @property overscan - The number of columns rendered outside the visible area, on each side. Defaults to `2`.
|
|
236
|
+
*
|
|
237
|
+
* @example
|
|
238
|
+
* ```tsx
|
|
239
|
+
* <DataTable
|
|
240
|
+
* enableColumnVirtualization
|
|
241
|
+
* columnVirtualizationOptions={{ getColumnWidth: (index) => columns[index]?.width, overscan: 4 }}
|
|
242
|
+
* >
|
|
243
|
+
* ```
|
|
244
|
+
*/
|
|
245
|
+
columnVirtualizationOptions?: DataTableColumnVirtualizationOptions;
|
|
145
246
|
} & ComponentPropsWithRef<'table'>;
|
|
@@ -9,7 +9,7 @@ import { cx } from '../../../styled-system/css';
|
|
|
9
9
|
import { useFixedColumns } from '../../utilities/dom/useFixedColumns';
|
|
10
10
|
import { handleCheckboxCellClick } from '../utils/handleCheckbox';
|
|
11
11
|
import { mergeRefs } from '../../utilities/dom/mergeRefs';
|
|
12
|
-
export const DataTableHeaderCell = forwardRef(({ children, type = 'text', sortButtonProps, sortOrder, actions, ascendingIconProps, descendingIconProps, unsortedIconProps, className, columnIndex, disabled = false, ...props }, ref) => {
|
|
12
|
+
export const DataTableHeaderCell = forwardRef(({ children, type = 'text', sortButtonProps, sortOrder, actions, ascendingIconProps, descendingIconProps, unsortedIconProps, className, columnIndex, disabled = false, style, ...props }, ref) => {
|
|
13
13
|
const { size, leftFixedColumnIndex, rightFixedColumnIndex, enableRowSelection, showVerticalBorder } = useDataTableContext();
|
|
14
14
|
const { cellRef, isFixedColumn, isEdgeFixedColumn, fixedPositionStyles } = useFixedColumns({
|
|
15
15
|
enableRowSelection,
|
|
@@ -62,6 +62,9 @@ export const DataTableHeaderCell = forwardRef(({ children, type = 'text', sortBu
|
|
|
62
62
|
// scope is stated explicitly rather than left to the auto-scope algorithm, whose result
|
|
63
63
|
// is harder to predict with `colSpan` / `rowSpan` and fixed columns.
|
|
64
64
|
scope: "col", className: cx(classes.root, 'mfui-DataTableHeaderCell__root',
|
|
65
|
-
/* TODO: Remove in v5.0.0 */ 'mfui-DataGridHeaderCell__root', className), "aria-sort": sortOrder,
|
|
65
|
+
/* TODO: Remove in v5.0.0 */ 'mfui-DataGridHeaderCell__root', className), "aria-sort": sortOrder, ...props,
|
|
66
|
+
// Merged rather than replaced, so that a consumer `style` (e.g. the column `width`) keeps the
|
|
67
|
+
// sticky offsets of a fixed column, as `DataTableCell` does.
|
|
68
|
+
style: { ...style, ...fixedPositionStyles }, children: cellContentWithActions }));
|
|
66
69
|
});
|
|
67
70
|
DataTableHeaderCell.displayName = 'DataTableHeaderCell';
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cloneElement, forwardRef, isValidElement, useEffect } from 'react';
|
|
3
|
-
import { dataTableHeaderRowSlotRecipe } from '../../../styled-system/recipes';
|
|
3
|
+
import { dataTableHeaderRowSlotRecipe, dataTableSlotRecipe } from '../../../styled-system/recipes';
|
|
4
4
|
import { useDataTableContext } from '../DataTableProvider';
|
|
5
5
|
import { DataTableHeaderCell } from '../DataTableHeaderCell';
|
|
6
6
|
import { Checkbox } from '../../Checkbox';
|
|
7
7
|
import { cx } from '../../../styled-system/css';
|
|
8
8
|
import { isComponentOrWrapped } from '../../utilities/react/isComponentOrWrapped';
|
|
9
9
|
import { flattenChildren } from '../../utilities/react/flattenChildren';
|
|
10
|
+
import { useIsomorphicLayoutEffect } from '../../utilities/react/useIsomorphicLayoutEffect';
|
|
11
|
+
import { renderVirtualizedColumns } from '../utils/renderVirtualizedColumns';
|
|
10
12
|
export const DataTableHeaderRow = forwardRef(({ children, checkboxProps, className, headerRowIndex = 1, ...props }, ref) => {
|
|
11
|
-
const { enableRowSelection, setNumberColumns, registerHeaderRow, virtualizedRowCount } = useDataTableContext();
|
|
13
|
+
const { enableRowSelection, setNumberColumns, registerHeaderRow, virtualizedRowCount, columnWindow, reportDeclaredColumnWidths, } = useDataTableContext();
|
|
12
14
|
const classes = dataTableHeaderRowSlotRecipe();
|
|
15
|
+
const isColumnVirtualized = columnWindow !== null;
|
|
13
16
|
// A virtualized body offsets the `aria-rowindex` of its rows by the number of header rows, which
|
|
14
17
|
// only the header rows themselves can report.
|
|
15
18
|
useEffect(() => registerHeaderRow(), [registerHeaderRow]);
|
|
@@ -17,22 +20,35 @@ export const DataTableHeaderRow = forwardRef(({ children, checkboxProps, classNa
|
|
|
17
20
|
const selectionCheckbox = (_jsx(Checkbox, { checked: checkboxProps?.checked, indeterminate: checkboxProps?.indeterminate, "aria-label": checkboxProps?.['aria-label'] ?? 'すべての行を選択', disabled: checkboxProps?.disabled, onChange: checkboxProps?.onChange, onClick: checkboxProps?.onClick }));
|
|
18
21
|
// Count the number of header cells, plus the selection checkbox column rendered
|
|
19
22
|
// below when `enableRowSelection` is on (it isn't part of `children`).
|
|
20
|
-
|
|
23
|
+
// Every cell is counted rather than the mounted ones, so that the count stays the total number
|
|
24
|
+
// of columns while the columns are virtualized.
|
|
25
|
+
// While the columns are virtualized, the declared width of every header cell is reported as well,
|
|
26
|
+
// for the `<colgroup>` the table renders. This runs as a layout effect so that the table re-renders
|
|
27
|
+
// with its `<col>` elements before the first paint, rather than one frame later.
|
|
28
|
+
useIsomorphicLayoutEffect(() => {
|
|
21
29
|
const headerCells = flattenChildren(children).filter((child) => isValidElement(child) && isComponentOrWrapped(child, DataTableHeaderCell));
|
|
22
30
|
setNumberColumns(headerCells.length + (enableRowSelection ? 1 : 0));
|
|
23
|
-
|
|
31
|
+
if (isColumnVirtualized) {
|
|
32
|
+
reportDeclaredColumnWidths(headerCells.map((cell) => cell.props.style?.width));
|
|
33
|
+
}
|
|
34
|
+
}, [children, enableRowSelection, isColumnVirtualized, setNumberColumns, reportDeclaredColumnWidths]);
|
|
24
35
|
return (_jsxs("tr", { className: cx(classes.root, 'mfui-DataTableHeaderRow__root',
|
|
25
|
-
/* TODO: Remove in v5.0.0 */ 'mfui-DataGridHeaderRow__root', className), ...(virtualizedRowCount !== null && { 'aria-rowindex': headerRowIndex }), ...props, ref: ref, children: [enableRowSelection ? (_jsx(DataTableHeaderCell, { columnIndex: 0, disabled: checkboxProps?.disabled, children: checkboxProps?.wrapper ? checkboxProps.wrapper(selectionCheckbox) : selectionCheckbox })) : null,
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
/* TODO: Remove in v5.0.0 */ 'mfui-DataGridHeaderRow__root', className), ...(virtualizedRowCount !== null && { 'aria-rowindex': headerRowIndex }), ...props, ref: ref, children: [enableRowSelection ? (_jsx(DataTableHeaderCell, { columnIndex: 0, disabled: checkboxProps?.disabled, ...(isColumnVirtualized && { 'aria-colindex': 1 }), children: checkboxProps?.wrapper ? checkboxProps.wrapper(selectionCheckbox) : selectionCheckbox })) : null, renderVirtualizedColumns({
|
|
37
|
+
cells: flattenChildren(children),
|
|
38
|
+
// Add selectable offset to columnIndex if selectable is true
|
|
39
|
+
columnOffset: enableRowSelection ? 1 : 0,
|
|
40
|
+
columnWindow,
|
|
41
|
+
spacerClassName: cx(dataTableSlotRecipe().headerColumnSpacer, 'mfui-DataTable__headerColumnSpacer'),
|
|
42
|
+
renderCell: (child, columnIndex) => {
|
|
43
|
+
// Check if the component is DataTableHeaderCell or wraps DataTableHeaderCell
|
|
44
|
+
if (!isValidElement(child) || !isComponentOrWrapped(child, DataTableHeaderCell)) {
|
|
45
|
+
return child;
|
|
46
|
+
}
|
|
47
|
+
return cloneElement(child, {
|
|
48
|
+
columnIndex,
|
|
49
|
+
...(isColumnVirtualized && { 'aria-colindex': child.props['aria-colindex'] ?? columnIndex + 1 }),
|
|
50
|
+
});
|
|
51
|
+
},
|
|
36
52
|
})] }));
|
|
37
53
|
});
|
|
38
54
|
DataTableHeaderRow.displayName = 'DataTableHeaderRow';
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type PropsWithChildren } from 'react';
|
|
2
2
|
import { type DataTableProps } from './DataTable.types';
|
|
3
|
+
import { type DataTableColumnWindow } from './hooks/useVirtualizedColumns';
|
|
4
|
+
import { type DeclaredColumnWidth } from './utils/resolveColumnWidths';
|
|
3
5
|
export type DataTableSize = 'small' | 'medium';
|
|
4
6
|
/**
|
|
5
7
|
* The props for the `DataTableContext` component.
|
|
@@ -31,11 +33,24 @@ export type DataTableVirtualizationValues = {
|
|
|
31
33
|
* Registers a header row and returns the function to unregister it.
|
|
32
34
|
*/
|
|
33
35
|
registerHeaderRow: () => () => void;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
/**
|
|
37
|
+
* The columns each row mounts while the columns are virtualized, or `null` when they are not.
|
|
38
|
+
* The table computes it once so that the header row and every row window the same columns.
|
|
39
|
+
*/
|
|
40
|
+
columnWindow: DataTableColumnWindow | null;
|
|
41
|
+
/**
|
|
42
|
+
* Reports the `style.width` of every header cell, mounted or not, which the table turns into the
|
|
43
|
+
* widths of its `<col>` elements while the columns are virtualized.
|
|
44
|
+
*/
|
|
45
|
+
reportDeclaredColumnWidths: (widths: readonly DeclaredColumnWidth[]) => void;
|
|
46
|
+
/**
|
|
47
|
+
* The total number of columns, counted by the header row from all of its cells rather than the
|
|
48
|
+
* mounted ones, plus the selection checkbox column.
|
|
49
|
+
*/
|
|
36
50
|
numberColumns: number;
|
|
37
51
|
setNumberColumns: (number: number) => void;
|
|
38
|
-
}
|
|
52
|
+
};
|
|
53
|
+
export type DataTableProviderValues = DataTableContextProps & DataTableVirtualizationValues;
|
|
39
54
|
/**
|
|
40
55
|
* The context for the `DataTable` component.
|
|
41
56
|
*
|
|
@@ -51,4 +66,4 @@ export declare const useDataTableContext: () => DataTableProviderValues;
|
|
|
51
66
|
*
|
|
52
67
|
* This should only be used in DataTable component.
|
|
53
68
|
*/
|
|
54
|
-
export declare function DataTableProvider({ children, size, enableRowSelection, enableRowClickSelection, fixedHeader, showVerticalBorder, leftFixedColumnIndex, rightFixedColumnIndex, loading, layout, scrollElement, virtualizedRowCount, setVirtualizedRowCount, headerRowCount, registerHeaderRow, }: PropsWithChildren<DataTableContextProps & DataTableVirtualizationValues>): import("react/jsx-runtime").JSX.Element;
|
|
69
|
+
export declare function DataTableProvider({ children, size, enableRowSelection, enableRowClickSelection, fixedHeader, showVerticalBorder, leftFixedColumnIndex, rightFixedColumnIndex, loading, layout, scrollElement, virtualizedRowCount, setVirtualizedRowCount, headerRowCount, registerHeaderRow, columnWindow, reportDeclaredColumnWidths, numberColumns, setNumberColumns, }: PropsWithChildren<DataTableContextProps & DataTableVirtualizationValues>): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { createContext, useContext, useMemo
|
|
3
|
+
import { createContext, useContext, useMemo } from 'react';
|
|
4
4
|
/**
|
|
5
5
|
* The context for the `DataTable` component.
|
|
6
6
|
*
|
|
@@ -28,6 +28,10 @@ export const DataTableContext = createContext({
|
|
|
28
28
|
registerHeaderRow: () => () => {
|
|
29
29
|
// Do nothing
|
|
30
30
|
},
|
|
31
|
+
columnWindow: null,
|
|
32
|
+
reportDeclaredColumnWidths: () => {
|
|
33
|
+
// Do nothing
|
|
34
|
+
},
|
|
31
35
|
});
|
|
32
36
|
/**
|
|
33
37
|
* The hook to use the `DataTableContext`.
|
|
@@ -38,8 +42,7 @@ export const useDataTableContext = () => useContext(DataTableContext);
|
|
|
38
42
|
*
|
|
39
43
|
* This should only be used in DataTable component.
|
|
40
44
|
*/
|
|
41
|
-
export function DataTableProvider({ children, size = 'medium', enableRowSelection = false, enableRowClickSelection = false, fixedHeader = false, showVerticalBorder = false, leftFixedColumnIndex, rightFixedColumnIndex, loading, layout = 'default', scrollElement, virtualizedRowCount, setVirtualizedRowCount, headerRowCount, registerHeaderRow, }) {
|
|
42
|
-
const [numberColumns, setNumberColumns] = useState(0);
|
|
45
|
+
export function DataTableProvider({ children, size = 'medium', enableRowSelection = false, enableRowClickSelection = false, fixedHeader = false, showVerticalBorder = false, leftFixedColumnIndex, rightFixedColumnIndex, loading, layout = 'default', scrollElement, virtualizedRowCount, setVirtualizedRowCount, headerRowCount, registerHeaderRow, columnWindow, reportDeclaredColumnWidths, numberColumns, setNumberColumns, }) {
|
|
43
46
|
const contextValue = useMemo(() => ({
|
|
44
47
|
size,
|
|
45
48
|
enableRowSelection,
|
|
@@ -57,6 +60,8 @@ export function DataTableProvider({ children, size = 'medium', enableRowSelectio
|
|
|
57
60
|
setVirtualizedRowCount,
|
|
58
61
|
headerRowCount,
|
|
59
62
|
registerHeaderRow,
|
|
63
|
+
columnWindow,
|
|
64
|
+
reportDeclaredColumnWidths,
|
|
60
65
|
}), [
|
|
61
66
|
size,
|
|
62
67
|
enableRowSelection,
|
|
@@ -68,11 +73,14 @@ export function DataTableProvider({ children, size = 'medium', enableRowSelectio
|
|
|
68
73
|
loading,
|
|
69
74
|
layout,
|
|
70
75
|
numberColumns,
|
|
76
|
+
setNumberColumns,
|
|
71
77
|
scrollElement,
|
|
72
78
|
virtualizedRowCount,
|
|
73
79
|
setVirtualizedRowCount,
|
|
74
80
|
headerRowCount,
|
|
75
81
|
registerHeaderRow,
|
|
82
|
+
columnWindow,
|
|
83
|
+
reportDeclaredColumnWidths,
|
|
76
84
|
]);
|
|
77
85
|
return _jsx(DataTableContext.Provider, { value: contextValue, children: children });
|
|
78
86
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cloneElement, forwardRef, isValidElement, useCallback, useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { DataTableRowActionContext, } from '../DataTableRowActionContext';
|
|
4
|
-
import { dataTableRowSlotRecipe } from '../../../styled-system/recipes';
|
|
4
|
+
import { dataTableRowSlotRecipe, dataTableSlotRecipe } from '../../../styled-system/recipes';
|
|
5
|
+
import { renderVirtualizedColumns } from '../utils/renderVirtualizedColumns';
|
|
5
6
|
import { DataTableCell } from '../DataTableCell';
|
|
6
7
|
import { Checkbox } from '../../Checkbox';
|
|
7
8
|
import { useDataTableContext } from '../DataTableProvider';
|
|
@@ -50,7 +51,8 @@ function isClickFromNestedInteractiveElement(event) {
|
|
|
50
51
|
return !interactiveElement.matches(ROW_ACTION_BUTTON_SELECTOR);
|
|
51
52
|
}
|
|
52
53
|
export const DataTableRow = forwardRef(({ children, disabled = false, highlighted, checkboxProps, className, onClickRow, rowActionButtonProps, selectable, 'aria-expanded': ariaExpandedProp, ...props }, ref) => {
|
|
53
|
-
const { enableRowSelection: contextEnableRowSelection, enableRowClickSelection, layout } = useDataTableContext();
|
|
54
|
+
const { enableRowSelection: contextEnableRowSelection, enableRowClickSelection, layout, columnWindow, } = useDataTableContext();
|
|
55
|
+
const isColumnVirtualized = columnWindow !== null;
|
|
54
56
|
const selectionCheckboxRef = useRef(null);
|
|
55
57
|
const isSelectable = selectable ?? contextEnableRowSelection;
|
|
56
58
|
// A selected row is always highlighted, and `highlighted` stays an independent signal on top of it,
|
|
@@ -159,16 +161,23 @@ export const DataTableRow = forwardRef(({ children, disabled = false, highlighte
|
|
|
159
161
|
onClickRow?.(event);
|
|
160
162
|
};
|
|
161
163
|
return (_jsx(DataTableRowActionContext.Provider, { value: rowActionContextValue, children: _jsxs("tr", { ref: ref, className: cx(classes.root, 'mfui-DataTableRow__root',
|
|
162
|
-
/* TODO: Remove in v5.0.0 */ 'mfui-DataGridRow__root', className), ...(!hasRowActionButton && ariaExpandedProp !== undefined && { 'aria-expanded': ariaExpandedProp }), ...props, ...(disabled && { 'aria-disabled': disabled }), onClick: handleClickRow, children: [isSelectable ? (_jsx(DataTableCell, { columnIndex: 0, isRowHighlighted: isRowHighlighted, disabled: disabled, children: checkboxProps?.wrapper ? checkboxProps.wrapper(selectionCheckbox) : selectionCheckbox })) : null,
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
164
|
+
/* TODO: Remove in v5.0.0 */ 'mfui-DataGridRow__root', className), ...(!hasRowActionButton && ariaExpandedProp !== undefined && { 'aria-expanded': ariaExpandedProp }), ...props, ...(disabled && { 'aria-disabled': disabled }), onClick: handleClickRow, children: [isSelectable ? (_jsx(DataTableCell, { columnIndex: 0, isRowHighlighted: isRowHighlighted, disabled: disabled, ...(isColumnVirtualized && { 'aria-colindex': 1 }), children: checkboxProps?.wrapper ? checkboxProps.wrapper(selectionCheckbox) : selectionCheckbox })) : null, renderVirtualizedColumns({
|
|
165
|
+
cells: flatChildren,
|
|
166
|
+
// Add selectable offset to columnIndex if selectable is true
|
|
167
|
+
columnOffset: isSelectable ? 1 : 0,
|
|
168
|
+
columnWindow,
|
|
169
|
+
spacerClassName: cx(dataTableSlotRecipe().columnSpacer, 'mfui-DataTable__columnSpacer'),
|
|
170
|
+
renderCell: (child, columnIndex) => {
|
|
171
|
+
if (!isCellElement(child)) {
|
|
172
|
+
return child;
|
|
173
|
+
}
|
|
174
|
+
return cloneElement(child, {
|
|
175
|
+
isRowHighlighted,
|
|
176
|
+
columnIndex,
|
|
177
|
+
disabled,
|
|
178
|
+
...(isColumnVirtualized && { 'aria-colindex': child.props['aria-colindex'] ?? columnIndex + 1 }),
|
|
179
|
+
});
|
|
180
|
+
},
|
|
172
181
|
})] }) }));
|
|
173
182
|
});
|
|
174
183
|
DataTableRow.displayName = 'DataTableRow';
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { type DataTableColumnVirtualizationOptions } from '../DataTable.types';
|
|
2
|
+
/**
|
|
3
|
+
* The columns of a row that are mounted while the columns are virtualized.
|
|
4
|
+
*
|
|
5
|
+
* Every index is a grid column index, which counts the selection checkbox column as column 0
|
|
6
|
+
* when the rows are selectable, and every end is exclusive.
|
|
7
|
+
* A row mounts `[0, prefixEnd)`, `[windowStart, windowEnd)` and `[suffixStart, end of row)`,
|
|
8
|
+
* and fills each gap between them with a single spacer cell.
|
|
9
|
+
*/
|
|
10
|
+
export type DataTableColumnWindow = {
|
|
11
|
+
/**
|
|
12
|
+
* The end of the always-mounted columns on the left: the selection checkbox column, the first
|
|
13
|
+
* consumer column and the left fixed columns.
|
|
14
|
+
*/
|
|
15
|
+
prefixEnd: number;
|
|
16
|
+
/**
|
|
17
|
+
* The first column of the window of scrollable columns that is mounted.
|
|
18
|
+
*/
|
|
19
|
+
windowStart: number;
|
|
20
|
+
/**
|
|
21
|
+
* The end of the window of scrollable columns that is mounted.
|
|
22
|
+
*/
|
|
23
|
+
windowEnd: number;
|
|
24
|
+
/**
|
|
25
|
+
* The first of the always-mounted right fixed columns, or `undefined` without right fixed columns.
|
|
26
|
+
*/
|
|
27
|
+
suffixStart: number | undefined;
|
|
28
|
+
};
|
|
29
|
+
export type UseVirtualizedColumnsParams = {
|
|
30
|
+
/**
|
|
31
|
+
* Whether the columns are windowed.
|
|
32
|
+
*/
|
|
33
|
+
enabled: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* The element that scrolls the columns. It is `null` until it is mounted.
|
|
36
|
+
*/
|
|
37
|
+
scrollElement: HTMLElement | null;
|
|
38
|
+
/**
|
|
39
|
+
* The table, whose first row is measured for the width of the always-mounted left columns.
|
|
40
|
+
*/
|
|
41
|
+
tableRef: React.RefObject<HTMLTableElement | null>;
|
|
42
|
+
/**
|
|
43
|
+
* The total number of grid columns, including the selection checkbox column.
|
|
44
|
+
*/
|
|
45
|
+
columnCount: number;
|
|
46
|
+
/**
|
|
47
|
+
* The number of columns that come before the consumer's columns, which is 1 when the rows are
|
|
48
|
+
* selectable and 0 otherwise.
|
|
49
|
+
*/
|
|
50
|
+
selectionColumnCount: number;
|
|
51
|
+
/**
|
|
52
|
+
* The width of each consumer column in pixels, which is also the width of its `<col>`.
|
|
53
|
+
*/
|
|
54
|
+
columnWidths: readonly number[];
|
|
55
|
+
leftFixedColumnIndex?: number;
|
|
56
|
+
rightFixedColumnIndex?: number;
|
|
57
|
+
} & Pick<DataTableColumnVirtualizationOptions, 'overscan'>;
|
|
58
|
+
/**
|
|
59
|
+
* Windows the columns of a `DataTable` with `@tanstack/react-virtual`, so that only the columns
|
|
60
|
+
* near the viewport are mounted.
|
|
61
|
+
*
|
|
62
|
+
* Only the columns between the left and the right fixed columns are windowed. The first column,
|
|
63
|
+
* after the selection checkbox column, is always mounted as well, because it hosts the row action
|
|
64
|
+
* button of a row that is not selectable and usually the row header.
|
|
65
|
+
*
|
|
66
|
+
* The columns are never measured: under `table-layout: fixed` each column is exactly as wide as its
|
|
67
|
+
* `<col>`, so the declared widths are the real ones.
|
|
68
|
+
*
|
|
69
|
+
* @param params - The virtualization configuration.
|
|
70
|
+
*
|
|
71
|
+
* @returns The columns to mount, or `null` when virtualization is disabled.
|
|
72
|
+
*/
|
|
73
|
+
export declare function useVirtualizedColumns({ enabled, scrollElement, tableRef, columnCount, selectionColumnCount, columnWidths, leftFixedColumnIndex, rightFixedColumnIndex, overscan, }: UseVirtualizedColumnsParams): DataTableColumnWindow | null;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
3
|
+
import { useIsomorphicLayoutEffect } from '../../utilities/react/useIsomorphicLayoutEffect';
|
|
4
|
+
import { DEFAULT_COLUMN_WIDTH } from '../utils/resolveColumnWidths';
|
|
5
|
+
/**
|
|
6
|
+
* The number of columns rendered outside the visible area, on each side.
|
|
7
|
+
*/
|
|
8
|
+
const DEFAULT_OVERSCAN = 2;
|
|
9
|
+
/**
|
|
10
|
+
* Windows the columns of a `DataTable` with `@tanstack/react-virtual`, so that only the columns
|
|
11
|
+
* near the viewport are mounted.
|
|
12
|
+
*
|
|
13
|
+
* Only the columns between the left and the right fixed columns are windowed. The first column,
|
|
14
|
+
* after the selection checkbox column, is always mounted as well, because it hosts the row action
|
|
15
|
+
* button of a row that is not selectable and usually the row header.
|
|
16
|
+
*
|
|
17
|
+
* The columns are never measured: under `table-layout: fixed` each column is exactly as wide as its
|
|
18
|
+
* `<col>`, so the declared widths are the real ones.
|
|
19
|
+
*
|
|
20
|
+
* @param params - The virtualization configuration.
|
|
21
|
+
*
|
|
22
|
+
* @returns The columns to mount, or `null` when virtualization is disabled.
|
|
23
|
+
*/
|
|
24
|
+
export function useVirtualizedColumns({ enabled, scrollElement, tableRef, columnCount, selectionColumnCount, columnWidths, leftFixedColumnIndex, rightFixedColumnIndex, overscan = DEFAULT_OVERSCAN, }) {
|
|
25
|
+
// The first consumer column stays mounted even without left fixed columns: it hosts the row action
|
|
26
|
+
// button of a row that is not selectable, and the row header that names each row.
|
|
27
|
+
const prefixEnd = selectionColumnCount + Math.max(leftFixedColumnIndex ?? 0, 0) + 1;
|
|
28
|
+
const suffixStart = rightFixedColumnIndex === undefined ? undefined : selectionColumnCount + rightFixedColumnIndex;
|
|
29
|
+
const scrollableColumnCount = Math.max(0, Math.min(suffixStart ?? columnCount, columnCount) - prefixEnd);
|
|
30
|
+
// The scrollable columns start after the always-mounted left columns, not at the left edge of the
|
|
31
|
+
// scroll element, so without the margin the window would be shifted right by their width.
|
|
32
|
+
// It is measured rather than summed from the widths, because the width of the selection column is
|
|
33
|
+
// a CSS expression of design tokens.
|
|
34
|
+
const [scrollMargin, setScrollMargin] = useState(0);
|
|
35
|
+
const hasWarnedDuplicateColgroup = useRef(false);
|
|
36
|
+
useIsomorphicLayoutEffect(() => {
|
|
37
|
+
const table = tableRef.current;
|
|
38
|
+
if (!enabled || !table)
|
|
39
|
+
return;
|
|
40
|
+
const measureScrollMargin = () => {
|
|
41
|
+
const cells = table.rows[0]?.cells;
|
|
42
|
+
let width = 0;
|
|
43
|
+
for (let index = 0; index < prefixEnd; index++) {
|
|
44
|
+
width += cells?.[index]?.offsetWidth ?? 0;
|
|
45
|
+
}
|
|
46
|
+
setScrollMargin(width);
|
|
47
|
+
};
|
|
48
|
+
measureScrollMargin();
|
|
49
|
+
// The always-mounted columns resize with the table, e.g. when the wrapper is resized.
|
|
50
|
+
const resizeObserver = new ResizeObserver(measureScrollMargin);
|
|
51
|
+
resizeObserver.observe(table);
|
|
52
|
+
return () => {
|
|
53
|
+
resizeObserver.disconnect();
|
|
54
|
+
};
|
|
55
|
+
}, [enabled, tableRef, prefixEnd, columnCount]);
|
|
56
|
+
// The table skips its own `<colgroup>` only when it finds the consumer's among its direct children,
|
|
57
|
+
// so a `<colgroup>` rendered by a consumer component ends up next to the table's without any error.
|
|
58
|
+
useIsomorphicLayoutEffect(() => {
|
|
59
|
+
const table = tableRef.current;
|
|
60
|
+
if (process.env['NODE_ENV'] === 'production' || !enabled || !table || hasWarnedDuplicateColgroup.current)
|
|
61
|
+
return;
|
|
62
|
+
if (table.querySelectorAll(':scope > colgroup').length > 1) {
|
|
63
|
+
hasWarnedDuplicateColgroup.current = true;
|
|
64
|
+
console.warn('[MFUI] DataTable received enableColumnVirtualization and found more than one <colgroup>. ' +
|
|
65
|
+
'Pass your <colgroup> element directly as a child of DataTable so that DataTable skips its own, ' +
|
|
66
|
+
'or remove it and let DataTable declare the columns from columnVirtualizationOptions.getColumnWidth ' +
|
|
67
|
+
'and the style.width of each DataTable.HeaderCell.');
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
// Always call useVirtualizer to keep the hook order stable, but with 0 count when disabled.
|
|
71
|
+
const virtualizer = useVirtualizer({
|
|
72
|
+
count: enabled ? scrollableColumnCount : 0,
|
|
73
|
+
horizontal: true,
|
|
74
|
+
getScrollElement: () => scrollElement,
|
|
75
|
+
// The widths count the consumer columns, as `leftFixedColumnIndex` does, without the selection column.
|
|
76
|
+
estimateSize: (index) => columnWidths[prefixEnd + index - selectionColumnCount] ?? DEFAULT_COLUMN_WIDTH,
|
|
77
|
+
overscan,
|
|
78
|
+
enabled,
|
|
79
|
+
// Keyed by the grid column, so that a column keeps its key when the number of fixed columns changes.
|
|
80
|
+
getItemKey: (index) => prefixEnd + index,
|
|
81
|
+
scrollMargin,
|
|
82
|
+
});
|
|
83
|
+
// The virtualizer keeps its item sizes until it is told to recompute them, so a column whose
|
|
84
|
+
// width changes would otherwise keep its old offset. The widths are compared by value, because
|
|
85
|
+
// `getColumnWidth` is usually an inline function and they are resolved again on every render.
|
|
86
|
+
const columnWidthsKey = columnWidths.join(',');
|
|
87
|
+
const previousColumnWidthsKey = useRef(columnWidthsKey);
|
|
88
|
+
useIsomorphicLayoutEffect(() => {
|
|
89
|
+
if (previousColumnWidthsKey.current === columnWidthsKey)
|
|
90
|
+
return;
|
|
91
|
+
previousColumnWidthsKey.current = columnWidthsKey;
|
|
92
|
+
virtualizer.measure();
|
|
93
|
+
}, [columnWidthsKey, virtualizer]);
|
|
94
|
+
const virtualColumns = enabled ? virtualizer.getVirtualItems() : [];
|
|
95
|
+
const firstIndex = virtualColumns[0]?.index;
|
|
96
|
+
const lastIndex = virtualColumns.at(-1)?.index;
|
|
97
|
+
// Kept referentially stable while the mounted columns stay the same, so that the rows only
|
|
98
|
+
// re-render when the window actually moves.
|
|
99
|
+
return useMemo(() => {
|
|
100
|
+
if (!enabled)
|
|
101
|
+
return null;
|
|
102
|
+
return {
|
|
103
|
+
prefixEnd,
|
|
104
|
+
windowStart: firstIndex === undefined ? prefixEnd : prefixEnd + firstIndex,
|
|
105
|
+
windowEnd: lastIndex === undefined ? prefixEnd : prefixEnd + lastIndex + 1,
|
|
106
|
+
suffixStart,
|
|
107
|
+
};
|
|
108
|
+
}, [enabled, prefixEnd, firstIndex, lastIndex, suffixStart]);
|
|
109
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { DataTable } from './DataTable';
|
|
2
2
|
export { getColumnWidthFromContentWidth } from './utils/getColumnWidthFromContentWidth';
|
|
3
|
-
export type { DataTableProps } from './DataTable.types';
|
|
3
|
+
export type { DataTableProps, DataTableColumnVirtualizationOptions } from './DataTable.types';
|
|
4
4
|
export type { DataTableBodyProps, DataTableBodyVirtualizationOptions } from './DataTableBody';
|
|
5
5
|
export type { DataTableCellProps } from './DataTableCell';
|
|
6
6
|
export type { DataTableHeaderProps } from './DataTableHeader';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The width of the selection checkbox column.
|
|
3
|
+
*
|
|
4
|
+
* The checkbox cell is always the table's start-edge cell, so its border-box has to hold the
|
|
5
|
+
* start-edge padding from `_first` as well as the inner cell's padding around the checkbox.
|
|
6
|
+
* Otherwise the checkbox overflows into the next column under `table-layout: fixed`.
|
|
7
|
+
*
|
|
8
|
+
* The header cell, the body cell and the `<col>` that `DataTable` renders for column virtualization
|
|
9
|
+
* all take the width from here, so that they cannot disagree.
|
|
10
|
+
*/
|
|
11
|
+
export declare const SELECTION_COLUMN_WIDTH = "[calc({sizes.mfui.size.dimension.icon.square.comfort} + {spacing.mfui.size.padding.data-table-cell.horizontal.comfort} * 2 + {spacing.mfui.size.padding.container.horizontal.comfort})]";
|