@moneyforward/mfui-components 4.5.0 → 4.7.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.
Files changed (92) hide show
  1. package/dist/src/Badge/Badge.d.ts +1 -1
  2. package/dist/src/Badge/Badge.js +4 -4
  3. package/dist/src/Badge/Badge.types.d.ts +3 -1
  4. package/dist/src/Checkbox/Checkbox.js +7 -3
  5. package/dist/src/DataTable/DataTable.d.ts +1 -1
  6. package/dist/src/DataTable/DataTable.js +23 -3
  7. package/dist/src/DataTable/DataTable.types.d.ts +2 -2
  8. package/dist/src/DataTable/DataTableBody/DataTableBody.d.ts +2 -1
  9. package/dist/src/DataTable/DataTableBody/DataTableBody.js +72 -8
  10. package/dist/src/DataTable/DataTableBody/DataTableBody.types.d.ts +81 -1
  11. package/dist/src/DataTable/DataTableBody/hooks/useVirtualizedRows.d.ts +64 -0
  12. package/dist/src/DataTable/DataTableBody/hooks/useVirtualizedRows.js +53 -0
  13. package/dist/src/DataTable/DataTableBody/index.d.ts +1 -1
  14. package/dist/src/DataTable/DataTableCell/DataTableCell.js +21 -4
  15. package/dist/src/DataTable/DataTableCell/DataTableCell.types.d.ts +1 -1
  16. package/dist/src/DataTable/DataTableHeader/DataTableHeader.js +13 -2
  17. package/dist/src/DataTable/DataTableHeaderRow/DataTableHeaderRow.js +12 -7
  18. package/dist/src/DataTable/DataTableHeaderRow/DataTableHeaderRow.types.d.ts +9 -0
  19. package/dist/src/DataTable/DataTableProvider.d.ts +29 -2
  20. package/dist/src/DataTable/DataTableProvider.js +20 -1
  21. package/dist/src/DataTable/DataTableRow/DataTableRow.js +104 -32
  22. package/dist/src/DataTable/DataTableRow/DataTableRow.types.d.ts +44 -15
  23. package/dist/src/DataTable/DataTableRowActionContext.d.ts +34 -0
  24. package/dist/src/DataTable/DataTableRowActionContext.js +8 -0
  25. package/dist/src/DataTable/index.d.ts +1 -1
  26. package/dist/src/DateTimeSelection/DatePicker/DatePickerCalendar/DatePickerCalendar.js +7 -3
  27. package/dist/src/DateTimeSelection/DateRangePicker/DateRangePickerPopover/DateRangePickerCalendar/DateRangePickerCalendar.js +1 -1
  28. package/dist/src/DateTimeSelection/DateRangePicker/DateRangePickerPopover/utilities/createDateRangePickerPopoverTestUtility.js +2 -2
  29. package/dist/src/DateTimeSelection/DateRangePicker/DateRangePickerProvider/DateRangePickerProvider.js +13 -7
  30. package/dist/src/DateTimeSelection/DateRangePicker/DateRangePickerTrigger/DateRangePickerTrigger.js +1 -1
  31. package/dist/src/DateTimeSelection/FilterDatePicker/FilterDatePicker.js +1 -1
  32. package/dist/src/DateTimeSelection/FilterMonthPicker/FilterMonthPicker.js +1 -1
  33. package/dist/src/DateTimeSelection/FilterMonthRangePicker/FilterMonthRangePicker.js +1 -1
  34. package/dist/src/DateTimeSelection/MonthPicker/MonthPickerPanel/MonthPickerPanel.js +1 -1
  35. package/dist/src/DateTimeSelection/MonthPicker/constants.d.ts +2 -2
  36. package/dist/src/DateTimeSelection/MonthPicker/constants.js +6 -6
  37. package/dist/src/DateTimeSelection/MonthRangePicker/MonthRangePickerMonthCell/MonthRangePickerMonthCell.js +1 -1
  38. package/dist/src/DateTimeSelection/MonthRangePicker/MonthRangePickerNavigation/MonthRangePickerNavigation.js +2 -2
  39. package/dist/src/DateTimeSelection/TimePicker/TimePicker.js +1 -1
  40. package/dist/src/DateTimeSelection/shared/BasePicker/constants.d.ts +2 -2
  41. package/dist/src/DateTimeSelection/shared/BasePicker/constants.js +10 -10
  42. package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerPopover/BaseRangePickerCalendar/BaseRangePickerCalendar.js +1 -1
  43. package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerPopover/constants.js +4 -4
  44. package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerProvider/BaseRangePickerProvider.js +13 -7
  45. package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerTrigger/BaseRangePickerTrigger.js +2 -2
  46. package/dist/src/DateTimeSelection/shared/CalendarGrid/CalendarGrid.js +1 -1
  47. package/dist/src/DateTimeSelection/shared/utilities/japaneseCalendar.js +2 -2
  48. package/dist/src/FileDropZone/FileDropZone.js +1 -1
  49. package/dist/src/FileDropZone/utils/fileHelpers.js +1 -1
  50. package/dist/src/MainNavigation/BaseMainNavigation.js +1 -1
  51. package/dist/src/MainNavigation/NarrowViewportMainNavigation.js +1 -1
  52. package/dist/src/MultipleSelectBox/MultipleSelectBox.js +12 -3
  53. package/dist/src/MultipleSelectBox/MultipleSelectBoxTrigger/MultipleSelectBoxTrigger.js +1 -1
  54. package/dist/src/MultipleSelectBox/hooks/useCreateOption.js +8 -4
  55. package/dist/src/MultipleSelectBox/hooks/useOptionKeyboardNavigation.d.ts +1 -0
  56. package/dist/src/MultipleSelectBox/hooks/useOptionKeyboardNavigation.js +24 -12
  57. package/dist/src/MultipleSelectBox/hooks/useSelectedValues.js +16 -50
  58. package/dist/src/NumberBox/NumberBox.js +8 -4
  59. package/dist/src/PageHeader/PageHeader.types.d.ts +2 -0
  60. package/dist/src/Popover/Popover.js +11 -2
  61. package/dist/src/SelectBox/SelectBox.js +21 -13
  62. package/dist/src/SelectBox/hooks/useKeyboardNavigation.d.ts +1 -0
  63. package/dist/src/SelectBox/hooks/useKeyboardNavigation.js +29 -8
  64. package/dist/src/SplitView/hooks/useSplitViewPanelVisibility.js +5 -2
  65. package/dist/src/SubNavigation/SubNavigation.js +80 -19
  66. package/dist/src/SubNavigation/SubNavigation.types.d.ts +18 -6
  67. package/dist/src/Tabs/Tabs.js +8 -4
  68. package/dist/src/Tabs/hooks/useCalculatedSelectedTabId.js +7 -5
  69. package/dist/src/Toast/ToastContainer/ToastContainer.js +11 -2
  70. package/dist/src/Tooltip/Tooltip.d.ts +6 -0
  71. package/dist/src/Tooltip/Tooltip.js +14 -3
  72. package/dist/src/Tooltip/hooks/useTooltipDisplayController.js +77 -88
  73. package/dist/src/utilities/dom/getFocusableNodes.js +1 -3
  74. package/dist/src/utilities/dom/useFixedColumns.js +6 -8
  75. package/dist/src/utilities/react/flattenChildren.d.ts +15 -0
  76. package/dist/src/utilities/react/flattenChildren.js +28 -0
  77. package/dist/src/utilities/state/useLatest.d.ts +1 -1
  78. package/dist/src/utilities/state/useLatest.js +8 -2
  79. package/dist/styled-system/recipes/badge-slot-recipe.d.ts +1 -1
  80. package/dist/styled-system/recipes/badge-slot-recipe.js +4 -0
  81. package/dist/styled-system/recipes/data-table-body-slot-recipe.d.ts +1 -1
  82. package/dist/styled-system/recipes/data-table-body-slot-recipe.js +4 -0
  83. package/dist/styled-system/recipes/data-table-cell-slot-recipe.d.ts +1 -1
  84. package/dist/styled-system/recipes/data-table-cell-slot-recipe.js +4 -0
  85. package/dist/styled-system/recipes/data-table-row-slot-recipe.js +13 -6
  86. package/dist/styled-system/recipes/sub-navigation-slot-recipe.d.ts +1 -1
  87. package/dist/styled-system/recipes/sub-navigation-slot-recipe.js +12 -0
  88. package/dist/styled-system/tokens/index.js +4 -0
  89. package/dist/styled-system/tokens/tokens.d.ts +1 -1
  90. package/dist/styles.css +164 -39
  91. package/dist/tsconfig.build.tsbuildinfo +1 -1
  92. package/package.json +12 -12
@@ -5,7 +5,7 @@ import { type BadgeProps } from './Badge.types';
5
5
  * - If `dot` is `true`, the badge will be displayed as a dot.
6
6
  * - If `count` is not `undefined`, the badge will be displayed as a number calculated by `digit` prop.
7
7
  * - If `children` is not `undefined`, the badge will be displayed in the top-right corner of the children.
8
- * - In addition, the `aria-describedby` attribute will be added to the children to link the aria-label of the badge.
8
+ * - In addition, the `aria-describedby` attribute will be added to the children to link the visually hidden label of the badge.
9
9
  *
10
10
  * Also extends the props of `<span>` element.
11
11
  *
@@ -9,13 +9,13 @@ import { Typography } from '../Typography';
9
9
  * - If `dot` is `true`, the badge will be displayed as a dot.
10
10
  * - If `count` is not `undefined`, the badge will be displayed as a number calculated by `digit` prop.
11
11
  * - If `children` is not `undefined`, the badge will be displayed in the top-right corner of the children.
12
- * - In addition, the `aria-describedby` attribute will be added to the children to link the aria-label of the badge.
12
+ * - In addition, the `aria-describedby` attribute will be added to the children to link the visually hidden label of the badge.
13
13
  *
14
14
  * Also extends the props of `<span>` element.
15
15
  *
16
16
  * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/span
17
17
  */
18
- export const Badge = forwardRef(({ dot, count: number, digit, className, children, ...props }, ref) => {
18
+ export const Badge = forwardRef(({ dot, count: number, digit, className, children, 'aria-label': ariaLabel, ...props }, ref) => {
19
19
  const classes = badgeSlotRecipe({ hasChildren: children !== undefined });
20
20
  const displayContent = (() => {
21
21
  if (number === undefined)
@@ -25,10 +25,10 @@ export const Badge = forwardRef(({ dot, count: number, digit, className, childre
25
25
  const maxValue = 10 ** digit - 1;
26
26
  return number > maxValue ? `${String(maxValue)}+` : String(number);
27
27
  })();
28
- const badgeId = useId();
28
+ const labelId = useId();
29
29
  const displayBadge = dot !== undefined || number !== undefined;
30
30
  // If the badge is not displayed, just return the children.
31
31
  if (!displayBadge)
32
32
  return children;
33
- return (_jsxs("span", { ref: ref, className: cx(classes.root, 'mfui-Badge__root', className), id: badgeId, ...props, children: [children ? cloneElement(children, { 'aria-describedby': badgeId }) : null, _jsx("span", { className: cx(classes.indicator, 'mfui-Badge__indicator'), children: number !== undefined ? _jsx(Typography, { variant: "condensedBody", children: displayContent }) : null })] }));
33
+ return (_jsxs("span", { ref: ref, className: cx(classes.root, 'mfui-Badge__root', className), ...props, children: [children ? cloneElement(children, { 'aria-describedby': labelId }) : null, _jsx("span", { className: cx(classes.indicator, 'mfui-Badge__indicator'), children: number !== undefined ? (_jsx(Typography, { "aria-hidden": true, variant: "condensedBody", children: displayContent })) : null }), _jsx("span", { id: labelId, className: cx(classes.srOnlyLabel, 'mfui-Badge__srOnlyLabel'), children: ariaLabel })] }));
34
34
  });
@@ -50,7 +50,9 @@ export type BadgeProps = {
50
50
  * The invisible label to specify how you want the badge to be recognized and read by screen readers.
51
51
  * This prop is required.
52
52
  *
53
- * @see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-label
53
+ * The value is not set as an `aria-label` attribute, because it is prohibited on role-less elements.
54
+ * Instead, it is rendered as visually hidden text inside the badge.
55
+ * When `children` is specified, that text is also referenced by the `aria-describedby` attribute of the children.
54
56
  *
55
57
  * @example
56
58
  * ```jsx
@@ -21,6 +21,13 @@ export const Checkbox = forwardRef(({ className, label, indeterminate = false, n
21
21
  const checkboxGroupContext = useContext(CheckboxGroupContext);
22
22
  const [localChecked, setLocalChecked] = useState(!!defaultChecked);
23
23
  const [localIndeterminate, setLocalIndeterminate] = useState(indeterminate);
24
+ // Adjusted during render (React's documented pattern for resyncing state to a changed
25
+ // prop) rather than in an effect, so the UI never flashes the stale value for one frame.
26
+ const [prevIndeterminateProp, setPrevIndeterminateProp] = useState(indeterminate);
27
+ if (indeterminate !== prevIndeterminateProp) {
28
+ setPrevIndeterminateProp(indeterminate);
29
+ setLocalIndeterminate(indeterminate);
30
+ }
24
31
  const classes = checkboxSlotRecipe();
25
32
  const WrapperTag = label ? 'label' : 'span';
26
33
  const wrapperProps = {
@@ -36,9 +43,6 @@ export const Checkbox = forwardRef(({ className, label, indeterminate = false, n
36
43
  checkboxInputRef.current.indeterminate = checkboxGroupContext.values === undefined ? localIndeterminate : false;
37
44
  }
38
45
  }, [checkboxInputRef, localIndeterminate, checkboxGroupContext.values]);
39
- useEffect(() => {
40
- setLocalIndeterminate(indeterminate);
41
- }, [indeterminate]);
42
46
  const icon = (() => {
43
47
  if (isIndeterminate) {
44
48
  return _jsx(CheckboxIndeterminate, {});
@@ -72,7 +72,7 @@ export declare const DataTable: import("react").ForwardRefExoticComponent<Omit<D
72
72
  * Body.displayName = 'CustomDataTableBody'; // Will cause hooks violation
73
73
  * ```
74
74
  */
75
- Body: import("react").ForwardRefExoticComponent<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLTableSectionElement>, HTMLTableSectionElement>, "ref"> & import("react").RefAttributes<HTMLTableSectionElement>>;
75
+ Body: import("react").ForwardRefExoticComponent<Omit<import("./DataTableBody").DataTableBodyProps, "ref"> & import("react").RefAttributes<HTMLTableSectionElement>>;
76
76
  /**
77
77
  * The `DataTableCell` component.
78
78
  * This component should be used inside the `DataTable` component.
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { Children, isValidElement, useMemo, useRef, forwardRef } from 'react';
3
+ import { Children, isValidElement, useCallback, useMemo, useRef, useState, forwardRef } from 'react';
4
4
  import { DataTableProvider } from './DataTableProvider';
5
5
  import { dataTableSlotRecipe } from '../../styled-system/recipes';
6
6
  import { DataTableBody } from './DataTableBody';
@@ -18,6 +18,20 @@ import { useTableEdgeCellMarkers } from '../utilities/dom/useTableEdgeCellMarker
18
18
  const Base = forwardRef(({ className, size = 'medium', enableRowSelection = false, enableRowClickSelection = false, fixedHeader = false, showVerticalBorder = false, leftFixedColumnIndex, rightFixedColumnIndex, layout = 'default', wrapperProps, loading = false, emptyStateSlot, children, ...props }, ref) => {
19
19
  const classes = dataTableSlotRecipe({ layout });
20
20
  const tableRef = useRef(null);
21
+ // A virtualized body needs the element that owns `overflow: auto`, which is the wrapper below.
22
+ // A ref object would still be `null` when the body's layout effects run, because host refs are
23
+ // attached child-first, so the element is held in state and the body re-renders once it lands.
24
+ const [scrollElement, setScrollElement] = useState(null);
25
+ // A virtualized body mounts only a window of its rows, so the row count it publishes here is what
26
+ // keeps `aria-rowcount` honest. It stays `null` while no body is virtualized.
27
+ const [virtualizedRowCount, setVirtualizedRowCount] = useState(null);
28
+ const [headerRowCount, setHeaderRowCount] = useState(0);
29
+ const registerHeaderRow = useCallback(() => {
30
+ setHeaderRowCount((count) => count + 1);
31
+ return () => {
32
+ setHeaderRowCount((count) => count - 1);
33
+ };
34
+ }, []);
21
35
  // `:first-child` / `:last-child` cannot tell which grid column a cell renders in, so the
22
36
  // table-edge spacing of the cell recipes misfires on the rows a `rowSpan` extends into.
23
37
  // This marks the displaced cells so that spacing can be cancelled on them.
@@ -29,6 +43,7 @@ const Base = forwardRef(({ className, size = 'medium', enableRowSelection = fals
29
43
  const bodyElement = Children.toArray(children).find((child) => isValidElement(child) &&
30
44
  (isComponentOrWrapped(child, DataTableBody) ||
31
45
  (typeof child.type !== 'string' &&
46
+ typeof child.type !== 'symbol' &&
32
47
  'displayName' in child.type &&
33
48
  child.type.displayName === 'DataGridBody')));
34
49
  if (!bodyElement || !isValidElement(bodyElement))
@@ -38,16 +53,21 @@ const Base = forwardRef(({ className, size = 'medium', enableRowSelection = fals
38
53
  const bodyChildren = Children.toArray(bodyElement.props.children);
39
54
  return bodyChildren.some((child) => isValidElement(child) &&
40
55
  (isComponentOrWrapped(child, DataTableRow) ||
56
+ // A `Fragment` has a symbol as its type, which `in` cannot search.
41
57
  (typeof child.type !== 'string' &&
58
+ typeof child.type !== 'symbol' &&
42
59
  'displayName' in child.type &&
43
60
  child.type.displayName === 'DataGridRow')));
44
61
  }
45
62
  return false;
46
63
  }, [children, loading]);
47
64
  const shouldShowEmptyState = !hasRows && !!emptyStateSlot && !loading;
48
- return (_jsx(DataTableProvider, { size: size, enableRowSelection: enableRowSelection, enableRowClickSelection: enableRowClickSelection, fixedHeader: fixedHeader, showVerticalBorder: showVerticalBorder, leftFixedColumnIndex: leftFixedColumnIndex, rightFixedColumnIndex: rightFixedColumnIndex, loading: loading, layout: layout, children: _jsxs("div", { ...wrapperProps, className: cx(classes.wrapper, 'mfui-DataTable__wrapper',
65
+ // A virtualized body holds only a window of its rows, so the mounted rows are not the row count.
66
+ // This is spread before `props` so that a consumer-supplied `aria-rowcount` still wins.
67
+ const ariaRowCountProps = virtualizedRowCount !== null ? { 'aria-rowcount': virtualizedRowCount + headerRowCount } : undefined;
68
+ 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, children: _jsxs("div", { ...wrapperProps, ref: setScrollElement, className: cx(classes.wrapper, 'mfui-DataTable__wrapper',
49
69
  /* TODO: Remove in v5.0.0 */ 'mfui-DataGrid__wrapper', wrapperProps?.className), children: [_jsx("table", { ref: mergeRefs(ref, tableRef), className: cx(classes.table, 'mfui-DataTable__table',
50
- /* TODO: Remove in v5.0.0 */ 'mfui-DataGrid__table', className), ...props, children: children }), shouldShowEmptyState ? (_jsx("div", { className: cx(classes.emptyState, 'mfui-DataTable__emptyState',
70
+ /* TODO: Remove in v5.0.0 */ 'mfui-DataGrid__table', className), ...ariaRowCountProps, ...props, children: children }), shouldShowEmptyState ? (_jsx("div", { className: cx(classes.emptyState, 'mfui-DataTable__emptyState',
51
71
  /* TODO: Remove in v5.0.0 */ 'mfui-DataGrid__emptyState'), role: "status", "aria-live": "polite", children: emptyStateSlot })) : null] }) }));
52
72
  });
53
73
  Base.displayName = 'DataTable';
@@ -49,8 +49,8 @@ export type DataTableProps = {
49
49
  * - Clicking the row, including its checkbox cell, calls the `checkboxProps.onChange` of the `DataTable.Row` component once.
50
50
  * The checkbox stops responding to clicks on its own, so a single click never toggles the selection twice.
51
51
  * The checkbox cell does not have independent hover, focus, or active styles; the checkbox icon follows the row state.
52
- * - The row is focusable, and pressing Enter or Space while the row itself is focused toggles the selection.
53
- * The checkbox is removed from the tab order, so keyboard selection is handled through the row.
52
+ * - The checkbox stays in the tab order and is the keyboard control: pressing Enter or Space while it is focused toggles the selection.
53
+ * While the checkbox is focused, the focus indicator is displayed on the whole row.
54
54
  * - The row selection is not toggled when the click originates from another interactive element inside the row,
55
55
  * such as a button or a link, or when the row or its checkbox is disabled.
56
56
  *
@@ -1 +1,2 @@
1
- export declare const DataTableBody: import("react").ForwardRefExoticComponent<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLTableSectionElement>, HTMLTableSectionElement>, "ref"> & import("react").RefAttributes<HTMLTableSectionElement>>;
1
+ import { type DataTableBodyProps } from './DataTableBody.types';
2
+ export declare const DataTableBody: import("react").ForwardRefExoticComponent<Omit<DataTableBodyProps, "ref"> & import("react").RefAttributes<HTMLTableSectionElement>>;
@@ -1,19 +1,83 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { forwardRef } from 'react';
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Children, cloneElement, forwardRef, Fragment, isValidElement, useCallback, useEffect, useMemo, useState, } from 'react';
3
+ import { useVirtualizedRows, VIRTUAL_ROW_INDEX_ATTRIBUTE } from './hooks/useVirtualizedRows';
3
4
  import { dataTableBodySlotRecipe } from '../../../styled-system/recipes';
4
5
  import { cx } from '../../../styled-system/css';
5
6
  import { useDataTableContext } from '../DataTableProvider';
6
7
  import { DataTableRow } from '../DataTableRow';
7
8
  import { DataTableCell } from '../DataTableCell';
9
+ import { mergeRefs } from '../../utilities/dom/mergeRefs';
8
10
  const NUMBER_OF_LOADING_ROWS = 4;
9
- export const DataTableBody = forwardRef(({ className, ...props }, ref) => {
10
- const { numberColumns, loading } = useDataTableContext();
11
+ /**
12
+ * React 19 exposes the ref of an element through its props, while React 18 keeps it on the element
13
+ * itself. Both are supported peers, so both have to be read to merge rather than clobber a
14
+ * consumer-supplied ref on a row.
15
+ *
16
+ * @param element - The row element to read the ref from.
17
+ *
18
+ * @returns The ref the consumer put on the row, if any.
19
+ */
20
+ function getRowRef(element) {
21
+ return element.props.ref ?? element.ref;
22
+ }
23
+ export const DataTableBody = forwardRef(({ className, children, enableVirtualization = false, virtualizationOptions, ...props }, ref) => {
24
+ const { numberColumns, loading, scrollElement, setVirtualizedRowCount, headerRowCount } = useDataTableContext();
11
25
  const classes = dataTableBodySlotRecipe();
26
+ // The body is held in state so that its offset below the `<thead>` can be read while rendering.
27
+ const [bodyElement, setBodyElement] = useState(null);
28
+ const bodyRef = useMemo(() => mergeRefs(ref, setBodyElement), [ref]);
29
+ const rows = useMemo(() => (enableVirtualization ? Children.toArray(children) : []), [children, enableVirtualization]);
30
+ const isVirtualized = enableVirtualization && !loading;
31
+ // `Children.toArray` gives every element a key, so measured heights stay with their row on reorder.
32
+ const getRowKey = useCallback((index) => {
33
+ const row = rows[index];
34
+ return isValidElement(row) && row.key != null ? row.key : index;
35
+ }, [rows]);
36
+ // Called before the `loading` branch below returns, so that the hook order stays stable.
37
+ const { virtualRows, paddingTop, paddingBottom, measureElement } = useVirtualizedRows({
38
+ enabled: isVirtualized,
39
+ scrollElement,
40
+ rowCount: rows.length,
41
+ getItemKey: getRowKey,
42
+ scrollMargin: bodyElement?.offsetTop ?? 0,
43
+ ...virtualizationOptions,
44
+ });
45
+ // Only a window of the rows is mounted, so the table cannot count them. Publishing the real
46
+ // count is what lets the table report `aria-rowcount` instead of the mounted count.
47
+ useEffect(() => {
48
+ if (!isVirtualized)
49
+ return;
50
+ setVirtualizedRowCount(rows.length);
51
+ return () => {
52
+ setVirtualizedRowCount(null);
53
+ };
54
+ }, [isVirtualized, rows.length, setVirtualizedRowCount]);
55
+ const bodyClassName = cx(classes.root, 'mfui-DataTableBody__root',
56
+ /* TODO: Remove in v5.0.0 */ 'mfui-DataGridBody__root', className);
12
57
  if (loading) {
13
- return (_jsx("tbody", { className: cx(classes.root, 'mfui-DataTableBody__root',
14
- /* TODO: Remove in v5.0.0 */ 'mfui-DataGridBody__root', className), ...props, ref: ref, children: Array.from({ length: NUMBER_OF_LOADING_ROWS }).map((_, rowIndex) => (_jsx(DataTableRow, { children: Array.from({ length: numberColumns }).map((_, colIndex) => (_jsx(DataTableCell, {}, colIndex))) }, rowIndex))) }));
58
+ return (_jsx("tbody", { className: bodyClassName, ...props, ref: ref, children: Array.from({ length: NUMBER_OF_LOADING_ROWS }).map((_, rowIndex) => (_jsx(DataTableRow, { children: Array.from({ length: numberColumns }).map((_, colIndex) => (_jsx(DataTableCell, {}, colIndex))) }, rowIndex))) }));
15
59
  }
16
- return (_jsx("tbody", { className: cx(classes.root, 'mfui-DataTableBody__root',
17
- /* TODO: Remove in v5.0.0 */ 'mfui-DataGridBody__root', className), ...props, ref: ref }));
60
+ if (!isVirtualized) {
61
+ return (_jsx("tbody", { className: bodyClassName, ...props, ref: ref, children: children }));
62
+ }
63
+ // `numberColumns` already counts the selection checkbox column, and falls back to a single
64
+ // column when the table renders no header row for it to count.
65
+ const spacerColSpan = Math.max(numberColumns, 1);
66
+ // The spacer stands in for the rows that are not mounted, so it reserves their height without
67
+ // being announced as a row of the table.
68
+ const renderSpacer = (position, height) => (_jsx("tr", { role: "presentation", "data-mfui-virtual-spacer": position, children: _jsx("td", { colSpan: spacerColSpan, className: classes.spacer, style: { height } }) }));
69
+ return (_jsxs("tbody", { className: bodyClassName, ...props, ref: bodyRef, children: [paddingTop > 0 ? renderSpacer('start', paddingTop) : null, virtualRows.map((virtualRow) => {
70
+ const child = rows[virtualRow.index];
71
+ // A `Fragment` cannot take the measuring ref or the ARIA attributes, so it is left as it is.
72
+ if (!isValidElement(child) || child.type === Fragment) {
73
+ return child;
74
+ }
75
+ return cloneElement(child, {
76
+ [VIRTUAL_ROW_INDEX_ATTRIBUTE]: virtualRow.index,
77
+ // The header rows come first in the table, so the first data row is the row after them.
78
+ 'aria-rowindex': child.props['aria-rowindex'] ?? virtualRow.index + headerRowCount + 1,
79
+ ref: mergeRefs(getRowRef(child), measureElement),
80
+ });
81
+ }), paddingBottom > 0 ? renderSpacer('end', paddingBottom) : null] }));
18
82
  });
19
83
  DataTableBody.displayName = 'DataTableBody';
@@ -1,4 +1,9 @@
1
1
  import { type ComponentPropsWithRef } from 'react';
2
+ import { type VirtualizerOptions } from '@tanstack/react-virtual';
3
+ /**
4
+ * The options for the row virtualization of the `DataTableBody` component.
5
+ */
6
+ export type DataTableBodyVirtualizationOptions = Pick<VirtualizerOptions<HTMLElement, Element>, 'estimateSize' | 'overscan'>;
2
7
  /**
3
8
  * The props for the `DataTableBody` component.
4
9
  *
@@ -6,4 +11,79 @@ import { type ComponentPropsWithRef } from 'react';
6
11
  *
7
12
  * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tbody
8
13
  */
9
- export type DataTableBodyProps = ComponentPropsWithRef<'tbody'>;
14
+ export type DataTableBodyProps = {
15
+ /**
16
+ * Whether to render only the rows near the viewport (row virtualization).
17
+ *
18
+ * When enabled, the body mounts a window of its `DataTable.Row` children instead of all of them,
19
+ * and reserves the height of the rows it leaves out with spacer rows. This removes the DOM and
20
+ * mount cost of a large table, which is what dominates at thousands of rows.
21
+ * Note that the rows are still *created* by the consumer's own `map`, so creating the row elements
22
+ * stays O(number of rows).
23
+ *
24
+ * Row heights are measured as the rows mount, so rows whose content wraps to different heights
25
+ * are supported.
26
+ *
27
+ * Requirements:
28
+ *
29
+ * - An ancestor has to constrain the height of the `DataTable`, because the wrapper of the
30
+ * `DataTable` is the element that scrolls and it is `height: 100%`.
31
+ * - The table has to use `table-layout: fixed` with explicit column widths. Column widths are
32
+ * otherwise derived from the mounted rows only, so they shift while scrolling.
33
+ * `getColumnWidthFromContentWidth` turns a content width into the matching column width.
34
+ *
35
+ * `aria-rowcount` on the table and `aria-rowindex` on each row are set for you, so that assistive
36
+ * technology is told the real row count rather than the number of mounted rows. Both stay
37
+ * overridable by the values you pass yourself.
38
+ *
39
+ * **Not supported together with merged cells.** A `rowSpan` reaches across rows that virtualization
40
+ * unmounts, so the merged cell is lost as soon as the row that owns it scrolls out of the window.
41
+ *
42
+ * **Each child has to be a single `DataTable.Row`.** A `Fragment` that groups several rows is
43
+ * windowed as one row and its rows are not measured, so pass the rows as a flat list instead.
44
+ *
45
+ * @default false
46
+ *
47
+ * @example
48
+ * ```tsx
49
+ * <div style={{ height: 500 }}>
50
+ * <DataTable fixedHeader style={{ tableLayout: 'fixed' }}>
51
+ * <colgroup>
52
+ * <col style={{ width: getColumnWidthFromContentWidth(96, { position: 'first' }) }} />
53
+ * <col />
54
+ * </colgroup>
55
+ * <DataTable.Header>
56
+ * <DataTable.HeaderRow>
57
+ * <DataTable.HeaderCell>行</DataTable.HeaderCell>
58
+ * <DataTable.HeaderCell>エラー内容</DataTable.HeaderCell>
59
+ * </DataTable.HeaderRow>
60
+ * </DataTable.Header>
61
+ * <DataTable.Body enableVirtualization>
62
+ * {rows.map((row) => (
63
+ * <DataTable.Row key={row.id}>
64
+ * <DataTable.Cell>{row.line}</DataTable.Cell>
65
+ * <DataTable.Cell>{row.message}</DataTable.Cell>
66
+ * </DataTable.Row>
67
+ * ))}
68
+ * </DataTable.Body>
69
+ * </DataTable>
70
+ * </div>
71
+ * ```
72
+ */
73
+ enableVirtualization?: boolean;
74
+ /**
75
+ * The configuration of the row virtualization.
76
+ * Only applies when `enableVirtualization` is `true`.
77
+ *
78
+ * @property estimateSize - The estimated height of a row in pixels, used until the row is measured. Defaults to `() => 44`.
79
+ * @property overscan - The number of rows rendered outside the visible area, on each side. Defaults to `8`. *
80
+ * @example
81
+ * ```tsx
82
+ * <DataTable.Body
83
+ * enableVirtualization
84
+ * virtualizationOptions={{ estimateSize: () => 36, overscan: 12 }}
85
+ * >
86
+ * ```
87
+ */
88
+ virtualizationOptions?: DataTableBodyVirtualizationOptions;
89
+ } & ComponentPropsWithRef<'tbody'>;
@@ -0,0 +1,64 @@
1
+ import { type Key } from 'react';
2
+ import { type VirtualItem } from '@tanstack/react-virtual';
3
+ import { type DataTableBodyVirtualizationOptions } from '../DataTableBody.types';
4
+ /**
5
+ * The attribute the virtualizer reads the row index from while measuring a mounted row.
6
+ * TanStack's default is `data-index`, which the repository's `data-mfui-` prefix rule forbids.
7
+ */
8
+ export declare const VIRTUAL_ROW_INDEX_ATTRIBUTE = "data-mfui-index";
9
+ export type UseVirtualizedRowsParams = {
10
+ /**
11
+ * Whether the rows are windowed.
12
+ */
13
+ enabled: boolean;
14
+ /**
15
+ * The element that scrolls the rows. It is `null` until it is mounted.
16
+ */
17
+ scrollElement: HTMLElement | null;
18
+ /**
19
+ * The total number of rows the body holds, not the number of mounted rows.
20
+ */
21
+ rowCount: number;
22
+ /**
23
+ * The distance in pixels from the top of the scroll element's content to the first row,
24
+ * which is the height of the `<thead>` above the body.
25
+ */
26
+ scrollMargin: number;
27
+ /**
28
+ * Returns a stable key for the row at the index, so that measured heights follow the row
29
+ * rather than its position when rows are reordered.
30
+ */
31
+ getItemKey?: (index: number) => Key;
32
+ } & Partial<DataTableBodyVirtualizationOptions>;
33
+ export type UseVirtualizedRowsReturn = {
34
+ /**
35
+ * The rows to mount, which is the visible rows plus the overscan.
36
+ * It is empty when virtualization is disabled.
37
+ */
38
+ virtualRows: VirtualItem[];
39
+ /**
40
+ * The height in pixels the spacer row before the window has to reserve.
41
+ */
42
+ paddingTop: number;
43
+ /**
44
+ * The height in pixels the spacer row after the window has to reserve.
45
+ */
46
+ paddingBottom: number;
47
+ /**
48
+ * The ref callback that measures a mounted row.
49
+ * The row must carry the index in {@link VIRTUAL_ROW_INDEX_ATTRIBUTE}.
50
+ */
51
+ measureElement: (node: Element | null) => void;
52
+ };
53
+ /**
54
+ * Windows the rows of a `DataTable.Body` with `@tanstack/react-virtual`, so that only the rows near
55
+ * the viewport are mounted.
56
+ *
57
+ * Rows are measured as they mount rather than trusted to `estimateSize`, so rows whose content wraps
58
+ * to different heights are supported.
59
+ *
60
+ * @param params - The virtualization configuration.
61
+ *
62
+ * @returns The rows to mount and the spacer heights that stand in for the rows that are not mounted.
63
+ */
64
+ export declare function useVirtualizedRows({ enabled, scrollElement, rowCount, scrollMargin, estimateSize, overscan, getItemKey, }: UseVirtualizedRowsParams): UseVirtualizedRowsReturn;
@@ -0,0 +1,53 @@
1
+ import { useVirtualizer } from '@tanstack/react-virtual';
2
+ /**
3
+ * The attribute the virtualizer reads the row index from while measuring a mounted row.
4
+ * TanStack's default is `data-index`, which the repository's `data-mfui-` prefix rule forbids.
5
+ */
6
+ export const VIRTUAL_ROW_INDEX_ATTRIBUTE = 'data-mfui-index';
7
+ /**
8
+ * The estimated height of a row in pixels, used before a row has been measured.
9
+ * It only affects the scroll range of the not-yet-mounted rows, because every mounted row is
10
+ * measured for real.
11
+ */
12
+ const DEFAULT_ESTIMATED_ROW_HEIGHT = 44;
13
+ /**
14
+ * The number of rows rendered outside the visible area, on each side.
15
+ */
16
+ const DEFAULT_OVERSCAN = 8;
17
+ /**
18
+ * Windows the rows of a `DataTable.Body` with `@tanstack/react-virtual`, so that only the rows near
19
+ * the viewport are mounted.
20
+ *
21
+ * Rows are measured as they mount rather than trusted to `estimateSize`, so rows whose content wraps
22
+ * to different heights are supported.
23
+ *
24
+ * @param params - The virtualization configuration.
25
+ *
26
+ * @returns The rows to mount and the spacer heights that stand in for the rows that are not mounted.
27
+ */
28
+ export function useVirtualizedRows({ enabled, scrollElement, rowCount, scrollMargin, estimateSize = () => DEFAULT_ESTIMATED_ROW_HEIGHT, overscan = DEFAULT_OVERSCAN, getItemKey, }) {
29
+ // Always call useVirtualizer to keep the hook order stable, but with 0 count when disabled.
30
+ const virtualizer = useVirtualizer({
31
+ count: enabled ? rowCount : 0,
32
+ getScrollElement: () => scrollElement,
33
+ estimateSize,
34
+ overscan,
35
+ enabled,
36
+ getItemKey,
37
+ indexAttribute: VIRTUAL_ROW_INDEX_ATTRIBUTE,
38
+ // The rows start below the `<thead>`, not at the top of the scroll element, so without the margin
39
+ // the window would be shifted down by the header height.
40
+ scrollMargin,
41
+ });
42
+ const virtualRows = enabled ? virtualizer.getVirtualItems() : [];
43
+ const [firstVirtualRow] = virtualRows;
44
+ const lastVirtualRow = virtualRows.at(-1);
45
+ // `start` / `end` include the scroll margin while `getTotalSize()` does not, so the margin is
46
+ // removed to get distances within the body.
47
+ return {
48
+ virtualRows,
49
+ paddingTop: firstVirtualRow ? firstVirtualRow.start - scrollMargin : 0,
50
+ paddingBottom: lastVirtualRow ? virtualizer.getTotalSize() - (lastVirtualRow.end - scrollMargin) : 0,
51
+ measureElement: virtualizer.measureElement,
52
+ };
53
+ }
@@ -1,2 +1,2 @@
1
1
  export { DataTableBody } from './DataTableBody';
2
- export type { DataTableBodyProps } from './DataTableBody.types';
2
+ export type { DataTableBodyProps, DataTableBodyVirtualizationOptions } from './DataTableBody.types';
@@ -1,5 +1,5 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { forwardRef } from 'react';
2
+ import { forwardRef, useState } from 'react';
3
3
  import { DisclosureBasicCollapsed, DisclosureBasicExpanded, DisclosureNestedCollapsed, DisclosureNestedExpanded, } from '@moneyforward/mfui-icons-react';
4
4
  import { dataTableCellSlotRecipe } from '../../../styled-system/recipes';
5
5
  import { FocusIndicator } from '../../FocusIndicator';
@@ -10,6 +10,8 @@ import { cx } from '../../../styled-system/css';
10
10
  import { useFixedColumns } from '../../utilities/dom/useFixedColumns';
11
11
  import { handleCheckboxCellClick } from '../utils/handleCheckbox';
12
12
  import { mergeRefs } from '../../utilities/dom/mergeRefs';
13
+ import { useIsomorphicLayoutEffect } from '../../utilities/react/useIsomorphicLayoutEffect';
14
+ import { useDataTableRowActionContext } from '../DataTableRowActionContext';
13
15
  export const DataTableCell = forwardRef(({ onClickCell, children, type = 'text', className, isRowHighlighted, isRowHeader = false, disabled = false, depth = 0, columnIndex, disclosureIconProps: nestingIconProps, style, ...props }, ref) => {
14
16
  const { size, leftFixedColumnIndex, rightFixedColumnIndex, loading, enableRowSelection, enableRowClickSelection, showVerticalBorder, layout, } = useDataTableContext();
15
17
  const { cellRef, isFixedColumn, isEdgeFixedColumn, fixedPositionStyles } = useFixedColumns({
@@ -20,6 +22,21 @@ export const DataTableCell = forwardRef(({ onClickCell, children, type = 'text',
20
22
  });
21
23
  const isCheckboxCell = enableRowSelection && columnIndex === 0;
22
24
  const isCheckboxCellInteractive = isCheckboxCell && !enableRowClickSelection;
25
+ // Take part in hosting the row action button of a clickable row. The row picks the host among
26
+ // the registered cells, so the button reaches a cell however the consumer composes the row.
27
+ const rowAction = useDataTableRowActionContext();
28
+ const registerRowActionCell = rowAction?.registerCell;
29
+ const [isRowActionHost, setIsRowActionHost] = useState(false);
30
+ useIsomorphicLayoutEffect(() => {
31
+ const element = cellRef.current;
32
+ if (!registerRowActionCell || !element) {
33
+ return;
34
+ }
35
+ return registerRowActionCell({ element, isRowHeader, setIsHost: setIsRowActionHost });
36
+ // `columnIndex` changes when the consumer reorders keyed cells, and the re-registration lets
37
+ // the row pick the host again by the new cell positions.
38
+ }, [registerRowActionCell, isRowHeader, cellRef, columnIndex]);
39
+ const rowActionButtonProps = isRowActionHost ? rowAction?.buttonProps : undefined;
23
40
  const classes = dataTableCellSlotRecipe({
24
41
  size,
25
42
  type,
@@ -27,7 +44,7 @@ export const DataTableCell = forwardRef(({ onClickCell, children, type = 'text',
27
44
  fixedColumn: isFixedColumn,
28
45
  edgeFixedColumn: isEdgeFixedColumn,
29
46
  isCheckboxCell,
30
- isCheckboxCellInteractive: isCheckboxCellInteractive || undefined,
47
+ isCheckboxCellInteractive: isCheckboxCellInteractive ? true : undefined,
31
48
  verticalBorder: showVerticalBorder,
32
49
  layout,
33
50
  // Passing `false` would emit a class for the unused `false` variant value, changing the
@@ -59,8 +76,8 @@ export const DataTableCell = forwardRef(({ onClickCell, children, type = 'text',
59
76
  // A row header identifies its row, so it must be a `th` for assistive technology.
60
77
  // `scope` is only interpreted on `th`, which is why it cannot be passed to a plain cell.
61
78
  const Tag = isRowHeader ? 'th' : 'td';
62
- return (_jsx(Tag, { ref: mergeRefs(ref, cellRef), scope: isRowHeader ? 'row' : undefined, className: cx(classes.root, 'mfui-DataTableCell__root',
63
- /* TODO: Remove in v5.0.0 */ 'mfui-DataGridCell__root', className), style: { ...style, ...fixedPositionStyles, zIndex: isFixedColumn ? 1 : undefined }, ...props, children: cellContent }));
79
+ return (_jsxs(Tag, { ref: mergeRefs(ref, cellRef), scope: isRowHeader ? 'row' : undefined, className: cx(classes.root, 'mfui-DataTableCell__root',
80
+ /* TODO: Remove in v5.0.0 */ 'mfui-DataGridCell__root', className), style: { ...style, ...fixedPositionStyles, zIndex: isFixedColumn ? 1 : undefined }, ...props, children: [rowActionButtonProps ? (_jsx("button", { type: "button", className: cx(classes.rowActionButton, 'mfui-DataTableCell__rowActionButton'), "data-mfui-content": "data-table-row-action-button", ...rowActionButtonProps })) : null, cellContent] }));
64
81
  });
65
82
  DataTableCell.displayName = 'DataTableCell';
66
83
  function CellContent({ children, depth, disclosureIconProps, classes, }) {
@@ -109,7 +109,7 @@ export type DataTableCellProps = {
109
109
  *
110
110
  * `aria-expanded` is set automatically based on `isOpen`:
111
111
  * on the cell button when the cell is interactive (`onClickCell`),
112
- * or on the row when the row is interactive (`onClickRow`).
112
+ * or on the visually hidden row action button when the row is interactive (`onClickRow`).
113
113
  *
114
114
  * Note: the attribute on the cell button is fully derived from `isOpen`
115
115
  * and cannot be overridden, while an `aria-expanded` passed to
@@ -1,12 +1,23 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { forwardRef } from 'react';
2
+ import { Children, cloneElement, forwardRef, isValidElement } from 'react';
3
3
  import { dataTableHeaderSlotRecipe } from '../../../styled-system/recipes';
4
4
  import { useDataTableContext } from '../DataTableProvider';
5
+ import { DataTableHeaderRow } from '../DataTableHeaderRow';
5
6
  import { cx } from '../../../styled-system/css';
7
+ import { isComponentOrWrapped } from '../../utilities/react/isComponentOrWrapped';
6
8
  export const DataTableHeader = forwardRef(({ children, className, ...props }, ref) => {
7
9
  const { fixedHeader } = useDataTableContext();
8
10
  const classes = dataTableHeaderSlotRecipe({ fixedHeader });
11
+ // Each header row needs its own position for `aria-rowindex`, which only the header can tell.
12
+ const items = Children.toArray(children);
13
+ const headerRows = items.filter((child) => isValidElement(child) && isComponentOrWrapped(child, DataTableHeaderRow));
9
14
  return (_jsx("thead", { className: cx(classes.root, 'mfui-DataTableHeader__root',
10
- /* TODO: Remove in v5.0.0 */ 'mfui-DataGridHeader__root', className), ...props, ref: ref, children: children }));
15
+ /* TODO: Remove in v5.0.0 */ 'mfui-DataGridHeader__root', className), ...props, ref: ref, children: items.map((child) => {
16
+ const headerRowIndex = headerRows.indexOf(child) + 1;
17
+ if (headerRowIndex === 0 || !isValidElement(child)) {
18
+ return child;
19
+ }
20
+ return cloneElement(child, { headerRowIndex });
21
+ }) }));
11
22
  });
12
23
  DataTableHeader.displayName = 'DataTableHeader';
@@ -1,23 +1,28 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Children, cloneElement, forwardRef, isValidElement, useEffect } from 'react';
2
+ import { cloneElement, forwardRef, isValidElement, useEffect } from 'react';
3
3
  import { dataTableHeaderRowSlotRecipe } 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
- export const DataTableHeaderRow = forwardRef(({ children, checkboxProps, className, ...props }, ref) => {
10
- const { enableRowSelection, setNumberColumns } = useDataTableContext();
9
+ import { flattenChildren } from '../../utilities/react/flattenChildren';
10
+ export const DataTableHeaderRow = forwardRef(({ children, checkboxProps, className, headerRowIndex = 1, ...props }, ref) => {
11
+ const { enableRowSelection, setNumberColumns, registerHeaderRow, virtualizedRowCount } = useDataTableContext();
11
12
  const classes = dataTableHeaderRowSlotRecipe();
13
+ // A virtualized body offsets the `aria-rowindex` of its rows by the number of header rows, which
14
+ // only the header rows themselves can report.
15
+ useEffect(() => registerHeaderRow(), [registerHeaderRow]);
12
16
  // Building the element before wrapping keeps everything the header row owns on the Checkbox itself.
13
17
  const selectionCheckbox = (_jsx(Checkbox, { checked: checkboxProps?.checked, indeterminate: checkboxProps?.indeterminate, "aria-label": checkboxProps?.['aria-label'] ?? 'すべての行を選択', disabled: checkboxProps?.disabled, onChange: checkboxProps?.onChange, onClick: checkboxProps?.onClick }));
14
- // Count the number of header cells
18
+ // Count the number of header cells, plus the selection checkbox column rendered
19
+ // below when `enableRowSelection` is on (it isn't part of `children`).
15
20
  useEffect(() => {
16
- const headerCells = Children.toArray(children).filter((child) => isValidElement(child) && isComponentOrWrapped(child, DataTableHeaderCell));
17
- setNumberColumns(headerCells.length);
21
+ const headerCells = flattenChildren(children).filter((child) => isValidElement(child) && isComponentOrWrapped(child, DataTableHeaderCell));
22
+ setNumberColumns(headerCells.length + (enableRowSelection ? 1 : 0));
18
23
  }, [children, enableRowSelection, setNumberColumns]);
19
24
  return (_jsxs("tr", { className: cx(classes.root, 'mfui-DataTableHeaderRow__root',
20
- /* 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, Children.map(children, (child, index) => {
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, flattenChildren(children).map((child, index) => {
21
26
  if (!isValidElement(child)) {
22
27
  return child;
23
28
  }
@@ -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'>;