@moneyforward/mfui-components 4.6.0 → 4.8.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.d.ts +1 -1
- package/dist/src/DataTable/DataTable.js +59 -6
- package/dist/src/DataTable/DataTable.types.d.ts +101 -0
- package/dist/src/DataTable/DataTableBody/DataTableBody.d.ts +2 -1
- package/dist/src/DataTable/DataTableBody/DataTableBody.js +72 -8
- package/dist/src/DataTable/DataTableBody/DataTableBody.types.d.ts +81 -1
- package/dist/src/DataTable/DataTableBody/hooks/useVirtualizedRows.d.ts +64 -0
- package/dist/src/DataTable/DataTableBody/hooks/useVirtualizedRows.js +53 -0
- package/dist/src/DataTable/DataTableBody/index.d.ts +1 -1
- package/dist/src/DataTable/DataTableHeader/DataTableHeader.js +13 -2
- package/dist/src/DataTable/DataTableHeaderCell/DataTableHeaderCell.js +5 -2
- package/dist/src/DataTable/DataTableHeaderRow/DataTableHeaderRow.js +35 -16
- package/dist/src/DataTable/DataTableHeaderRow/DataTableHeaderRow.types.d.ts +9 -0
- package/dist/src/DataTable/DataTableProvider.d.ts +45 -3
- package/dist/src/DataTable/DataTableProvider.js +30 -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 +2 -2
- 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 +20 -8
- package/dist/src/MultipleSelectBox/MultipleSelectBox.types.d.ts +33 -0
- package/dist/src/MultipleSelectBox/hooks/useOptionKeyboardNavigation.d.ts +1 -0
- package/dist/src/MultipleSelectBox/hooks/useOptionKeyboardNavigation.js +23 -16
- package/dist/src/SelectBox/SelectBox.js +18 -7
- 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/SelectBox/hooks/useKeyboardNavigation.d.ts +1 -0
- package/dist/src/SelectBox/hooks/useKeyboardNavigation.js +29 -14
- package/dist/src/Tooltip/Tooltip.d.ts +6 -0
- package/dist/src/Tooltip/Tooltip.js +6 -0
- 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-body-slot-recipe.d.ts +1 -1
- package/dist/styled-system/recipes/data-table-body-slot-recipe.js +4 -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 +40 -4
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +7 -7
|
@@ -1,35 +1,54 @@
|
|
|
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
|
-
|
|
11
|
-
|
|
10
|
+
import { useIsomorphicLayoutEffect } from '../../utilities/react/useIsomorphicLayoutEffect';
|
|
11
|
+
import { renderVirtualizedColumns } from '../utils/renderVirtualizedColumns';
|
|
12
|
+
export const DataTableHeaderRow = forwardRef(({ children, checkboxProps, className, headerRowIndex = 1, ...props }, ref) => {
|
|
13
|
+
const { enableRowSelection, setNumberColumns, registerHeaderRow, virtualizedRowCount, columnWindow, reportDeclaredColumnWidths, } = useDataTableContext();
|
|
12
14
|
const classes = dataTableHeaderRowSlotRecipe();
|
|
15
|
+
const isColumnVirtualized = columnWindow !== null;
|
|
16
|
+
// A virtualized body offsets the `aria-rowindex` of its rows by the number of header rows, which
|
|
17
|
+
// only the header rows themselves can report.
|
|
18
|
+
useEffect(() => registerHeaderRow(), [registerHeaderRow]);
|
|
13
19
|
// Building the element before wrapping keeps everything the header row owns on the Checkbox itself.
|
|
14
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 }));
|
|
15
21
|
// Count the number of header cells, plus the selection checkbox column rendered
|
|
16
22
|
// below when `enableRowSelection` is on (it isn't part of `children`).
|
|
17
|
-
|
|
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(() => {
|
|
18
29
|
const headerCells = flattenChildren(children).filter((child) => isValidElement(child) && isComponentOrWrapped(child, DataTableHeaderCell));
|
|
19
30
|
setNumberColumns(headerCells.length + (enableRowSelection ? 1 : 0));
|
|
20
|
-
|
|
31
|
+
if (isColumnVirtualized) {
|
|
32
|
+
reportDeclaredColumnWidths(headerCells.map((cell) => cell.props.style?.width));
|
|
33
|
+
}
|
|
34
|
+
}, [children, enableRowSelection, isColumnVirtualized, setNumberColumns, reportDeclaredColumnWidths]);
|
|
21
35
|
return (_jsxs("tr", { className: cx(classes.root, 'mfui-DataTableHeaderRow__root',
|
|
22
|
-
/* TODO: Remove in v5.0.0 */ 'mfui-DataGridHeaderRow__root', className), ...props, ref: ref, children: [enableRowSelection ? (_jsx(DataTableHeaderCell, { columnIndex: 0, disabled: checkboxProps?.disabled, children: checkboxProps?.wrapper ? checkboxProps.wrapper(selectionCheckbox) : selectionCheckbox })) : null,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
+
},
|
|
33
52
|
})] }));
|
|
34
53
|
});
|
|
35
54
|
DataTableHeaderRow.displayName = 'DataTableHeaderRow';
|
|
@@ -44,4 +44,13 @@ export type DataTableHeaderRowProps = {
|
|
|
44
44
|
checkboxProps?: Pick<CheckboxProps, 'aria-label' | 'checked' | 'indeterminate' | 'onChange' | 'disabled' | 'onClick'> & {
|
|
45
45
|
wrapper?: DataTableSelectionCheckboxWrapper;
|
|
46
46
|
};
|
|
47
|
+
/**
|
|
48
|
+
* **Do not pass this prop manually.**
|
|
49
|
+
*
|
|
50
|
+
* The 1-based position of this row among the header rows.
|
|
51
|
+
* Used for the `aria-rowindex` of the row while the body is virtualized.
|
|
52
|
+
*
|
|
53
|
+
* @default undefined
|
|
54
|
+
*/
|
|
55
|
+
headerRowIndex?: number;
|
|
47
56
|
} & ComponentPropsWithRef<'tr'>;
|
|
@@ -1,14 +1,56 @@
|
|
|
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.
|
|
6
8
|
*/
|
|
7
9
|
export type DataTableContextProps = Pick<DataTableProps, 'size' | 'enableRowSelection' | 'enableRowClickSelection' | 'fixedHeader' | 'showVerticalBorder' | 'leftFixedColumnIndex' | 'rightFixedColumnIndex' | 'loading' | 'layout'>;
|
|
8
|
-
|
|
10
|
+
/**
|
|
11
|
+
* The values the `DataTable` owns and shares with its composition components.
|
|
12
|
+
*
|
|
13
|
+
* This should only be used in DataTable component.
|
|
14
|
+
*/
|
|
15
|
+
export type DataTableVirtualizationValues = {
|
|
16
|
+
/**
|
|
17
|
+
* The wrapper element of the table, which is the element that scrolls.
|
|
18
|
+
* It is `null` until the wrapper is mounted, and when the body is used outside a `DataTable`.
|
|
19
|
+
*/
|
|
20
|
+
scrollElement: HTMLElement | null;
|
|
21
|
+
/**
|
|
22
|
+
* The number of rows the body holds while it is virtualized, or `null` when it is not.
|
|
23
|
+
* The body publishes it so that the table can report the real row count to assistive technology
|
|
24
|
+
* even though only a window of rows is mounted.
|
|
25
|
+
*/
|
|
26
|
+
virtualizedRowCount: number | null;
|
|
27
|
+
setVirtualizedRowCount: (rowCount: number | null) => void;
|
|
28
|
+
/**
|
|
29
|
+
* The number of mounted header rows, which offsets the `aria-rowindex` of the body rows.
|
|
30
|
+
*/
|
|
31
|
+
headerRowCount: number;
|
|
32
|
+
/**
|
|
33
|
+
* Registers a header row and returns the function to unregister it.
|
|
34
|
+
*/
|
|
35
|
+
registerHeaderRow: () => () => void;
|
|
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
|
+
*/
|
|
9
50
|
numberColumns: number;
|
|
10
51
|
setNumberColumns: (number: number) => void;
|
|
11
|
-
}
|
|
52
|
+
};
|
|
53
|
+
export type DataTableProviderValues = DataTableContextProps & DataTableVirtualizationValues;
|
|
12
54
|
/**
|
|
13
55
|
* The context for the `DataTable` component.
|
|
14
56
|
*
|
|
@@ -24,4 +66,4 @@ export declare const useDataTableContext: () => DataTableProviderValues;
|
|
|
24
66
|
*
|
|
25
67
|
* This should only be used in DataTable component.
|
|
26
68
|
*/
|
|
27
|
-
export declare function DataTableProvider({ children, size, enableRowSelection, enableRowClickSelection, fixedHeader, showVerticalBorder, leftFixedColumnIndex, rightFixedColumnIndex, loading, layout, }: PropsWithChildren<DataTableContextProps>): 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
|
*
|
|
@@ -19,6 +19,19 @@ export const DataTableContext = createContext({
|
|
|
19
19
|
setNumberColumns: () => {
|
|
20
20
|
// Do nothing
|
|
21
21
|
},
|
|
22
|
+
scrollElement: null,
|
|
23
|
+
virtualizedRowCount: null,
|
|
24
|
+
setVirtualizedRowCount: () => {
|
|
25
|
+
// Do nothing
|
|
26
|
+
},
|
|
27
|
+
headerRowCount: 0,
|
|
28
|
+
registerHeaderRow: () => () => {
|
|
29
|
+
// Do nothing
|
|
30
|
+
},
|
|
31
|
+
columnWindow: null,
|
|
32
|
+
reportDeclaredColumnWidths: () => {
|
|
33
|
+
// Do nothing
|
|
34
|
+
},
|
|
22
35
|
});
|
|
23
36
|
/**
|
|
24
37
|
* The hook to use the `DataTableContext`.
|
|
@@ -29,8 +42,7 @@ export const useDataTableContext = () => useContext(DataTableContext);
|
|
|
29
42
|
*
|
|
30
43
|
* This should only be used in DataTable component.
|
|
31
44
|
*/
|
|
32
|
-
export function DataTableProvider({ children, size = 'medium', enableRowSelection = false, enableRowClickSelection = false, fixedHeader = false, showVerticalBorder = false, leftFixedColumnIndex, rightFixedColumnIndex, loading, layout = 'default', }) {
|
|
33
|
-
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, }) {
|
|
34
46
|
const contextValue = useMemo(() => ({
|
|
35
47
|
size,
|
|
36
48
|
enableRowSelection,
|
|
@@ -43,6 +55,13 @@ export function DataTableProvider({ children, size = 'medium', enableRowSelectio
|
|
|
43
55
|
layout,
|
|
44
56
|
numberColumns,
|
|
45
57
|
setNumberColumns,
|
|
58
|
+
scrollElement,
|
|
59
|
+
virtualizedRowCount,
|
|
60
|
+
setVirtualizedRowCount,
|
|
61
|
+
headerRowCount,
|
|
62
|
+
registerHeaderRow,
|
|
63
|
+
columnWindow,
|
|
64
|
+
reportDeclaredColumnWidths,
|
|
46
65
|
}), [
|
|
47
66
|
size,
|
|
48
67
|
enableRowSelection,
|
|
@@ -54,6 +73,14 @@ export function DataTableProvider({ children, size = 'medium', enableRowSelectio
|
|
|
54
73
|
loading,
|
|
55
74
|
layout,
|
|
56
75
|
numberColumns,
|
|
76
|
+
setNumberColumns,
|
|
77
|
+
scrollElement,
|
|
78
|
+
virtualizedRowCount,
|
|
79
|
+
setVirtualizedRowCount,
|
|
80
|
+
headerRowCount,
|
|
81
|
+
registerHeaderRow,
|
|
82
|
+
columnWindow,
|
|
83
|
+
reportDeclaredColumnWidths,
|
|
57
84
|
]);
|
|
58
85
|
return _jsx(DataTableContext.Provider, { value: contextValue, children: children });
|
|
59
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,7 +1,7 @@
|
|
|
1
1
|
export { DataTable } from './DataTable';
|
|
2
2
|
export { getColumnWidthFromContentWidth } from './utils/getColumnWidthFromContentWidth';
|
|
3
|
-
export type { DataTableProps } from './DataTable.types';
|
|
4
|
-
export type { DataTableBodyProps } from './DataTableBody';
|
|
3
|
+
export type { DataTableProps, DataTableColumnVirtualizationOptions } from './DataTable.types';
|
|
4
|
+
export type { DataTableBodyProps, DataTableBodyVirtualizationOptions } from './DataTableBody';
|
|
5
5
|
export type { DataTableCellProps } from './DataTableCell';
|
|
6
6
|
export type { DataTableHeaderProps } from './DataTableHeader';
|
|
7
7
|
export type { DataTableHeaderCellProps } from './DataTableHeaderCell';
|
|
@@ -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})]";
|
|
@@ -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 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})]';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type DataTableColumnWindow } from '../hooks/useVirtualizedColumns';
|
|
3
|
+
type RenderVirtualizedColumnsParams = {
|
|
4
|
+
/**
|
|
5
|
+
* The cells of the row, flattened, without the selection checkbox cell the row renders itself.
|
|
6
|
+
*/
|
|
7
|
+
cells: ReactNode[];
|
|
8
|
+
/**
|
|
9
|
+
* The grid column of the first cell, which is 1 when the row renders a selection checkbox cell.
|
|
10
|
+
*/
|
|
11
|
+
columnOffset: number;
|
|
12
|
+
/**
|
|
13
|
+
* The columns to mount, or `null` to mount every cell.
|
|
14
|
+
*/
|
|
15
|
+
columnWindow: DataTableColumnWindow | null;
|
|
16
|
+
/**
|
|
17
|
+
* The class name of the spacer cells.
|
|
18
|
+
*/
|
|
19
|
+
spacerClassName: string;
|
|
20
|
+
/**
|
|
21
|
+
* Renders a mounted cell.
|
|
22
|
+
*
|
|
23
|
+
* @param cell - The cell to render.
|
|
24
|
+
* @param columnIndex - The grid column of the cell.
|
|
25
|
+
*/
|
|
26
|
+
renderCell: (cell: ReactNode, columnIndex: number) => ReactNode;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Applies the column window to the cells of a row. The header row and the rows share it, so that
|
|
30
|
+
* they cannot disagree about which columns are mounted.
|
|
31
|
+
*
|
|
32
|
+
* Each gap left by the columns that are not mounted is filled with a single spacer cell spanning
|
|
33
|
+
* them. Under `table-layout: fixed` the spacer takes exactly the width of the `<col>` elements it
|
|
34
|
+
* spans, so the mounted cells stay in their columns and the sticky offsets of the fixed columns,
|
|
35
|
+
* which are summed over the cells of the row, stay what they are without virtualization.
|
|
36
|
+
*
|
|
37
|
+
* @param params - The cells of the row and the window to apply.
|
|
38
|
+
*
|
|
39
|
+
* @returns The cells to mount, with the spacer cells in place of the columns that are not mounted.
|
|
40
|
+
*/
|
|
41
|
+
export declare function renderVirtualizedColumns({ cells, columnOffset, columnWindow, spacerClassName, renderCell, }: RenderVirtualizedColumnsParams): ReactNode[];
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Applies the column window to the cells of a row. The header row and the rows share it, so that
|
|
4
|
+
* they cannot disagree about which columns are mounted.
|
|
5
|
+
*
|
|
6
|
+
* Each gap left by the columns that are not mounted is filled with a single spacer cell spanning
|
|
7
|
+
* them. Under `table-layout: fixed` the spacer takes exactly the width of the `<col>` elements it
|
|
8
|
+
* spans, so the mounted cells stay in their columns and the sticky offsets of the fixed columns,
|
|
9
|
+
* which are summed over the cells of the row, stay what they are without virtualization.
|
|
10
|
+
*
|
|
11
|
+
* @param params - The cells of the row and the window to apply.
|
|
12
|
+
*
|
|
13
|
+
* @returns The cells to mount, with the spacer cells in place of the columns that are not mounted.
|
|
14
|
+
*/
|
|
15
|
+
export function renderVirtualizedColumns({ cells, columnOffset, columnWindow, spacerClassName, renderCell, }) {
|
|
16
|
+
if (columnWindow === null) {
|
|
17
|
+
return cells.map((cell, index) => renderCell(cell, index + columnOffset));
|
|
18
|
+
}
|
|
19
|
+
const rowColumnCount = cells.length + columnOffset;
|
|
20
|
+
// The bounds are clamped to the row, so a row with fewer cells than the header still lays out.
|
|
21
|
+
const suffixStart = Math.min(columnWindow.suffixStart ?? rowColumnCount, rowColumnCount);
|
|
22
|
+
const prefixEnd = Math.min(columnWindow.prefixEnd, suffixStart);
|
|
23
|
+
const windowStart = Math.min(Math.max(columnWindow.windowStart, prefixEnd), suffixStart);
|
|
24
|
+
const windowEnd = Math.min(Math.max(columnWindow.windowEnd, windowStart), suffixStart);
|
|
25
|
+
const renderRange = (start, end) => cells
|
|
26
|
+
.slice(Math.max(start - columnOffset, 0), Math.max(end - columnOffset, 0))
|
|
27
|
+
.map((cell, index) => renderCell(cell, Math.max(start, columnOffset) + index));
|
|
28
|
+
// The spacer stands in for the columns that are not mounted, so it reserves their width without
|
|
29
|
+
// being announced as a cell of the table.
|
|
30
|
+
const renderSpacer = (position, colSpan) => (_jsx("td", {
|
|
31
|
+
// oxlint-disable-next-line jsx-a11y/no-interactive-element-to-noninteractive-role -- A `td` is not interactive; the role only keeps this empty layout cell from being announced as a cell.
|
|
32
|
+
role: "presentation", colSpan: colSpan, className: spacerClassName, "data-mfui-virtual-spacer": position }, `mfui-virtual-column-spacer-${position}`));
|
|
33
|
+
return [
|
|
34
|
+
...renderRange(0, prefixEnd),
|
|
35
|
+
windowStart > prefixEnd ? renderSpacer('start', windowStart - prefixEnd) : null,
|
|
36
|
+
...renderRange(windowStart, windowEnd),
|
|
37
|
+
suffixStart > windowEnd ? renderSpacer('end', suffixStart - windowEnd) : null,
|
|
38
|
+
...renderRange(suffixStart, rowColumnCount),
|
|
39
|
+
];
|
|
40
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type CSSProperties } from 'react';
|
|
2
|
+
import { type DataTableColumnVirtualizationOptions } from '../DataTable.types';
|
|
3
|
+
/**
|
|
4
|
+
* The width of a column in pixels when neither `getColumnWidth` nor its header cell declares one.
|
|
5
|
+
*/
|
|
6
|
+
export declare const DEFAULT_COLUMN_WIDTH = 160;
|
|
7
|
+
/**
|
|
8
|
+
* The `style.width` of a header cell, which the header row reports for each column.
|
|
9
|
+
*/
|
|
10
|
+
export type DeclaredColumnWidth = CSSProperties['width'];
|
|
11
|
+
/**
|
|
12
|
+
* Resolves the width of every consumer column, in pixels, for column virtualization.
|
|
13
|
+
*
|
|
14
|
+
* Each column takes the first of:
|
|
15
|
+
*
|
|
16
|
+
* 1. `getColumnWidth(index)`, when it returns a width.
|
|
17
|
+
* 2. The `style.width` of the column's `DataTable.HeaderCell`, when it is a number or a `px` string.
|
|
18
|
+
* 3. {@link DEFAULT_COLUMN_WIDTH}.
|
|
19
|
+
*
|
|
20
|
+
* @param declaredColumnWidths - The `style.width` of each header cell, which also gives the number
|
|
21
|
+
* of consumer columns.
|
|
22
|
+
*
|
|
23
|
+
* @param getColumnWidth - The consumer's width callback, which receives the column index counted as
|
|
24
|
+
* `leftFixedColumnIndex` is, without the selection column.
|
|
25
|
+
*
|
|
26
|
+
* @returns The width of each consumer column in pixels.
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveColumnWidths(declaredColumnWidths: readonly DeclaredColumnWidth[], getColumnWidth: DataTableColumnVirtualizationOptions['getColumnWidth']): number[];
|