@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.
- package/dist/src/Badge/Badge.d.ts +1 -1
- package/dist/src/Badge/Badge.js +4 -4
- package/dist/src/Badge/Badge.types.d.ts +3 -1
- package/dist/src/Checkbox/Checkbox.js +7 -3
- package/dist/src/DataTable/DataTable.d.ts +1 -1
- package/dist/src/DataTable/DataTable.js +23 -3
- package/dist/src/DataTable/DataTable.types.d.ts +2 -2
- 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/DataTableCell/DataTableCell.js +21 -4
- package/dist/src/DataTable/DataTableCell/DataTableCell.types.d.ts +1 -1
- package/dist/src/DataTable/DataTableHeader/DataTableHeader.js +13 -2
- package/dist/src/DataTable/DataTableHeaderRow/DataTableHeaderRow.js +12 -7
- package/dist/src/DataTable/DataTableHeaderRow/DataTableHeaderRow.types.d.ts +9 -0
- package/dist/src/DataTable/DataTableProvider.d.ts +29 -2
- package/dist/src/DataTable/DataTableProvider.js +20 -1
- package/dist/src/DataTable/DataTableRow/DataTableRow.js +104 -32
- package/dist/src/DataTable/DataTableRow/DataTableRow.types.d.ts +44 -15
- package/dist/src/DataTable/DataTableRowActionContext.d.ts +34 -0
- package/dist/src/DataTable/DataTableRowActionContext.js +8 -0
- package/dist/src/DataTable/index.d.ts +1 -1
- package/dist/src/DateTimeSelection/DatePicker/DatePickerCalendar/DatePickerCalendar.js +7 -3
- package/dist/src/DateTimeSelection/DateRangePicker/DateRangePickerPopover/DateRangePickerCalendar/DateRangePickerCalendar.js +1 -1
- package/dist/src/DateTimeSelection/DateRangePicker/DateRangePickerPopover/utilities/createDateRangePickerPopoverTestUtility.js +2 -2
- package/dist/src/DateTimeSelection/DateRangePicker/DateRangePickerProvider/DateRangePickerProvider.js +13 -7
- package/dist/src/DateTimeSelection/DateRangePicker/DateRangePickerTrigger/DateRangePickerTrigger.js +1 -1
- package/dist/src/DateTimeSelection/FilterDatePicker/FilterDatePicker.js +1 -1
- package/dist/src/DateTimeSelection/FilterMonthPicker/FilterMonthPicker.js +1 -1
- package/dist/src/DateTimeSelection/FilterMonthRangePicker/FilterMonthRangePicker.js +1 -1
- package/dist/src/DateTimeSelection/MonthPicker/MonthPickerPanel/MonthPickerPanel.js +1 -1
- package/dist/src/DateTimeSelection/MonthPicker/constants.d.ts +2 -2
- package/dist/src/DateTimeSelection/MonthPicker/constants.js +6 -6
- package/dist/src/DateTimeSelection/MonthRangePicker/MonthRangePickerMonthCell/MonthRangePickerMonthCell.js +1 -1
- package/dist/src/DateTimeSelection/MonthRangePicker/MonthRangePickerNavigation/MonthRangePickerNavigation.js +2 -2
- package/dist/src/DateTimeSelection/TimePicker/TimePicker.js +1 -1
- package/dist/src/DateTimeSelection/shared/BasePicker/constants.d.ts +2 -2
- package/dist/src/DateTimeSelection/shared/BasePicker/constants.js +10 -10
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerPopover/BaseRangePickerCalendar/BaseRangePickerCalendar.js +1 -1
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerPopover/constants.js +4 -4
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerProvider/BaseRangePickerProvider.js +13 -7
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerTrigger/BaseRangePickerTrigger.js +2 -2
- package/dist/src/DateTimeSelection/shared/CalendarGrid/CalendarGrid.js +1 -1
- package/dist/src/DateTimeSelection/shared/utilities/japaneseCalendar.js +2 -2
- package/dist/src/FileDropZone/FileDropZone.js +1 -1
- package/dist/src/FileDropZone/utils/fileHelpers.js +1 -1
- package/dist/src/MainNavigation/BaseMainNavigation.js +1 -1
- package/dist/src/MainNavigation/NarrowViewportMainNavigation.js +1 -1
- package/dist/src/MultipleSelectBox/MultipleSelectBox.js +12 -3
- package/dist/src/MultipleSelectBox/MultipleSelectBoxTrigger/MultipleSelectBoxTrigger.js +1 -1
- package/dist/src/MultipleSelectBox/hooks/useCreateOption.js +8 -4
- package/dist/src/MultipleSelectBox/hooks/useOptionKeyboardNavigation.d.ts +1 -0
- package/dist/src/MultipleSelectBox/hooks/useOptionKeyboardNavigation.js +24 -12
- package/dist/src/MultipleSelectBox/hooks/useSelectedValues.js +16 -50
- package/dist/src/NumberBox/NumberBox.js +8 -4
- package/dist/src/PageHeader/PageHeader.types.d.ts +2 -0
- package/dist/src/Popover/Popover.js +11 -2
- package/dist/src/SelectBox/SelectBox.js +21 -13
- package/dist/src/SelectBox/hooks/useKeyboardNavigation.d.ts +1 -0
- package/dist/src/SelectBox/hooks/useKeyboardNavigation.js +29 -8
- package/dist/src/SplitView/hooks/useSplitViewPanelVisibility.js +5 -2
- package/dist/src/SubNavigation/SubNavigation.js +80 -19
- package/dist/src/SubNavigation/SubNavigation.types.d.ts +18 -6
- package/dist/src/Tabs/Tabs.js +8 -4
- package/dist/src/Tabs/hooks/useCalculatedSelectedTabId.js +7 -5
- package/dist/src/Toast/ToastContainer/ToastContainer.js +11 -2
- package/dist/src/Tooltip/Tooltip.d.ts +6 -0
- package/dist/src/Tooltip/Tooltip.js +14 -3
- package/dist/src/Tooltip/hooks/useTooltipDisplayController.js +77 -88
- package/dist/src/utilities/dom/getFocusableNodes.js +1 -3
- package/dist/src/utilities/dom/useFixedColumns.js +6 -8
- package/dist/src/utilities/react/flattenChildren.d.ts +15 -0
- package/dist/src/utilities/react/flattenChildren.js +28 -0
- package/dist/src/utilities/state/useLatest.d.ts +1 -1
- package/dist/src/utilities/state/useLatest.js +8 -2
- package/dist/styled-system/recipes/badge-slot-recipe.d.ts +1 -1
- package/dist/styled-system/recipes/badge-slot-recipe.js +4 -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-cell-slot-recipe.d.ts +1 -1
- package/dist/styled-system/recipes/data-table-cell-slot-recipe.js +4 -0
- package/dist/styled-system/recipes/data-table-row-slot-recipe.js +13 -6
- package/dist/styled-system/recipes/sub-navigation-slot-recipe.d.ts +1 -1
- package/dist/styled-system/recipes/sub-navigation-slot-recipe.js +12 -0
- package/dist/styled-system/tokens/index.js +4 -0
- package/dist/styled-system/tokens/tokens.d.ts +1 -1
- package/dist/styles.css +164 -39
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- 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
|
|
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
|
*
|
package/dist/src/Badge/Badge.js
CHANGED
|
@@ -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
|
|
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
|
|
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),
|
|
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
|
-
*
|
|
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("
|
|
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
|
-
|
|
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
|
|
53
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
|
|
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:
|
|
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
|
-
|
|
17
|
-
|
|
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 =
|
|
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
|
|
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 (
|
|
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:
|
|
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 {
|
|
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
|
-
|
|
10
|
-
|
|
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 =
|
|
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,
|
|
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'>;
|