@moneyforward/mfui-components 4.6.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/DataTable/DataTable.d.ts +1 -1
- package/dist/src/DataTable/DataTable.js +23 -3
- package/dist/src/DataTable/DataTableBody/DataTableBody.d.ts +2 -1
- package/dist/src/DataTable/DataTableBody/DataTableBody.js +72 -8
- package/dist/src/DataTable/DataTableBody/DataTableBody.types.d.ts +81 -1
- package/dist/src/DataTable/DataTableBody/hooks/useVirtualizedRows.d.ts +64 -0
- package/dist/src/DataTable/DataTableBody/hooks/useVirtualizedRows.js +53 -0
- package/dist/src/DataTable/DataTableBody/index.d.ts +1 -1
- package/dist/src/DataTable/DataTableHeader/DataTableHeader.js +13 -2
- package/dist/src/DataTable/DataTableHeaderRow/DataTableHeaderRow.js +6 -3
- 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/index.d.ts +1 -1
- package/dist/src/MultipleSelectBox/MultipleSelectBox.js +12 -3
- package/dist/src/MultipleSelectBox/hooks/useOptionKeyboardNavigation.d.ts +1 -0
- package/dist/src/MultipleSelectBox/hooks/useOptionKeyboardNavigation.js +23 -16
- package/dist/src/SelectBox/SelectBox.js +12 -3
- package/dist/src/SelectBox/hooks/useKeyboardNavigation.d.ts +1 -0
- package/dist/src/SelectBox/hooks/useKeyboardNavigation.js +29 -14
- package/dist/src/Tooltip/Tooltip.d.ts +6 -0
- package/dist/src/Tooltip/Tooltip.js +6 -0
- package/dist/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/tokens/index.js +4 -0
- package/dist/styled-system/tokens/tokens.d.ts +1 -1
- package/dist/styles.css +9 -4
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -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';
|
|
@@ -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,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';
|
|
@@ -7,9 +7,12 @@ import { Checkbox } from '../../Checkbox';
|
|
|
7
7
|
import { cx } from '../../../styled-system/css';
|
|
8
8
|
import { isComponentOrWrapped } from '../../utilities/react/isComponentOrWrapped';
|
|
9
9
|
import { flattenChildren } from '../../utilities/react/flattenChildren';
|
|
10
|
-
export const DataTableHeaderRow = forwardRef(({ children, checkboxProps, className, ...props }, ref) => {
|
|
11
|
-
const { enableRowSelection, setNumberColumns } = useDataTableContext();
|
|
10
|
+
export const DataTableHeaderRow = forwardRef(({ children, checkboxProps, className, headerRowIndex = 1, ...props }, ref) => {
|
|
11
|
+
const { enableRowSelection, setNumberColumns, registerHeaderRow, virtualizedRowCount } = useDataTableContext();
|
|
12
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]);
|
|
13
16
|
// Building the element before wrapping keeps everything the header row owns on the Checkbox itself.
|
|
14
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 }));
|
|
15
18
|
// Count the number of header cells, plus the selection checkbox column rendered
|
|
@@ -19,7 +22,7 @@ export const DataTableHeaderRow = forwardRef(({ children, checkboxProps, classNa
|
|
|
19
22
|
setNumberColumns(headerCells.length + (enableRowSelection ? 1 : 0));
|
|
20
23
|
}, [children, enableRowSelection, setNumberColumns]);
|
|
21
24
|
return (_jsxs("tr", { className: cx(classes.root, 'mfui-DataTableHeaderRow__root',
|
|
22
|
-
/* TODO: Remove in v5.0.0 */ 'mfui-DataGridHeaderRow__root', className), ...props, ref: ref, children: [enableRowSelection ? (_jsx(DataTableHeaderCell, { columnIndex: 0, disabled: checkboxProps?.disabled, children: checkboxProps?.wrapper ? checkboxProps.wrapper(selectionCheckbox) : selectionCheckbox })) : null, flattenChildren(children).map((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) => {
|
|
23
26
|
if (!isValidElement(child)) {
|
|
24
27
|
return child;
|
|
25
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'>;
|
|
@@ -5,10 +5,37 @@ export type DataTableSize = 'small' | 'medium';
|
|
|
5
5
|
* The props for the `DataTableContext` component.
|
|
6
6
|
*/
|
|
7
7
|
export type DataTableContextProps = Pick<DataTableProps, 'size' | 'enableRowSelection' | 'enableRowClickSelection' | 'fixedHeader' | 'showVerticalBorder' | 'leftFixedColumnIndex' | 'rightFixedColumnIndex' | 'loading' | 'layout'>;
|
|
8
|
+
/**
|
|
9
|
+
* The values the `DataTable` owns and shares with its composition components.
|
|
10
|
+
*
|
|
11
|
+
* This should only be used in DataTable component.
|
|
12
|
+
*/
|
|
13
|
+
export type DataTableVirtualizationValues = {
|
|
14
|
+
/**
|
|
15
|
+
* The wrapper element of the table, which is the element that scrolls.
|
|
16
|
+
* It is `null` until the wrapper is mounted, and when the body is used outside a `DataTable`.
|
|
17
|
+
*/
|
|
18
|
+
scrollElement: HTMLElement | null;
|
|
19
|
+
/**
|
|
20
|
+
* The number of rows the body holds while it is virtualized, or `null` when it is not.
|
|
21
|
+
* The body publishes it so that the table can report the real row count to assistive technology
|
|
22
|
+
* even though only a window of rows is mounted.
|
|
23
|
+
*/
|
|
24
|
+
virtualizedRowCount: number | null;
|
|
25
|
+
setVirtualizedRowCount: (rowCount: number | null) => void;
|
|
26
|
+
/**
|
|
27
|
+
* The number of mounted header rows, which offsets the `aria-rowindex` of the body rows.
|
|
28
|
+
*/
|
|
29
|
+
headerRowCount: number;
|
|
30
|
+
/**
|
|
31
|
+
* Registers a header row and returns the function to unregister it.
|
|
32
|
+
*/
|
|
33
|
+
registerHeaderRow: () => () => void;
|
|
34
|
+
};
|
|
8
35
|
export type DataTableProviderValues = {
|
|
9
36
|
numberColumns: number;
|
|
10
37
|
setNumberColumns: (number: number) => void;
|
|
11
|
-
} & DataTableContextProps;
|
|
38
|
+
} & DataTableContextProps & DataTableVirtualizationValues;
|
|
12
39
|
/**
|
|
13
40
|
* The context for the `DataTable` component.
|
|
14
41
|
*
|
|
@@ -24,4 +51,4 @@ export declare const useDataTableContext: () => DataTableProviderValues;
|
|
|
24
51
|
*
|
|
25
52
|
* This should only be used in DataTable component.
|
|
26
53
|
*/
|
|
27
|
-
export declare function DataTableProvider({ children, size, enableRowSelection, enableRowClickSelection, fixedHeader, showVerticalBorder, leftFixedColumnIndex, rightFixedColumnIndex, loading, layout, }: PropsWithChildren<DataTableContextProps>): import("react/jsx-runtime").JSX.Element;
|
|
54
|
+
export declare function DataTableProvider({ children, size, enableRowSelection, enableRowClickSelection, fixedHeader, showVerticalBorder, leftFixedColumnIndex, rightFixedColumnIndex, loading, layout, scrollElement, virtualizedRowCount, setVirtualizedRowCount, headerRowCount, registerHeaderRow, }: PropsWithChildren<DataTableContextProps & DataTableVirtualizationValues>): import("react/jsx-runtime").JSX.Element;
|
|
@@ -19,6 +19,15 @@ export const DataTableContext = createContext({
|
|
|
19
19
|
setNumberColumns: () => {
|
|
20
20
|
// Do nothing
|
|
21
21
|
},
|
|
22
|
+
scrollElement: null,
|
|
23
|
+
virtualizedRowCount: null,
|
|
24
|
+
setVirtualizedRowCount: () => {
|
|
25
|
+
// Do nothing
|
|
26
|
+
},
|
|
27
|
+
headerRowCount: 0,
|
|
28
|
+
registerHeaderRow: () => () => {
|
|
29
|
+
// Do nothing
|
|
30
|
+
},
|
|
22
31
|
});
|
|
23
32
|
/**
|
|
24
33
|
* The hook to use the `DataTableContext`.
|
|
@@ -29,7 +38,7 @@ export const useDataTableContext = () => useContext(DataTableContext);
|
|
|
29
38
|
*
|
|
30
39
|
* This should only be used in DataTable component.
|
|
31
40
|
*/
|
|
32
|
-
export function DataTableProvider({ children, size = 'medium', enableRowSelection = false, enableRowClickSelection = false, fixedHeader = false, showVerticalBorder = false, leftFixedColumnIndex, rightFixedColumnIndex, loading, layout = 'default', }) {
|
|
41
|
+
export function DataTableProvider({ children, size = 'medium', enableRowSelection = false, enableRowClickSelection = false, fixedHeader = false, showVerticalBorder = false, leftFixedColumnIndex, rightFixedColumnIndex, loading, layout = 'default', scrollElement, virtualizedRowCount, setVirtualizedRowCount, headerRowCount, registerHeaderRow, }) {
|
|
33
42
|
const [numberColumns, setNumberColumns] = useState(0);
|
|
34
43
|
const contextValue = useMemo(() => ({
|
|
35
44
|
size,
|
|
@@ -43,6 +52,11 @@ export function DataTableProvider({ children, size = 'medium', enableRowSelectio
|
|
|
43
52
|
layout,
|
|
44
53
|
numberColumns,
|
|
45
54
|
setNumberColumns,
|
|
55
|
+
scrollElement,
|
|
56
|
+
virtualizedRowCount,
|
|
57
|
+
setVirtualizedRowCount,
|
|
58
|
+
headerRowCount,
|
|
59
|
+
registerHeaderRow,
|
|
46
60
|
}), [
|
|
47
61
|
size,
|
|
48
62
|
enableRowSelection,
|
|
@@ -54,6 +68,11 @@ export function DataTableProvider({ children, size = 'medium', enableRowSelectio
|
|
|
54
68
|
loading,
|
|
55
69
|
layout,
|
|
56
70
|
numberColumns,
|
|
71
|
+
scrollElement,
|
|
72
|
+
virtualizedRowCount,
|
|
73
|
+
setVirtualizedRowCount,
|
|
74
|
+
headerRowCount,
|
|
75
|
+
registerHeaderRow,
|
|
57
76
|
]);
|
|
58
77
|
return _jsx(DataTableContext.Provider, { value: contextValue, children: children });
|
|
59
78
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { DataTable } from './DataTable';
|
|
2
2
|
export { getColumnWidthFromContentWidth } from './utils/getColumnWidthFromContentWidth';
|
|
3
3
|
export type { DataTableProps } from './DataTable.types';
|
|
4
|
-
export type { DataTableBodyProps } from './DataTableBody';
|
|
4
|
+
export type { DataTableBodyProps, DataTableBodyVirtualizationOptions } from './DataTableBody';
|
|
5
5
|
export type { DataTableCellProps } from './DataTableCell';
|
|
6
6
|
export type { DataTableHeaderProps } from './DataTableHeader';
|
|
7
7
|
export type { DataTableHeaderCellProps } from './DataTableHeaderCell';
|
|
@@ -31,12 +31,15 @@ import { useVirtualizedMultipleSelectBoxOptions } from './hooks/useVirtualizedMu
|
|
|
31
31
|
import { useInfiniteScroll } from '../utilities/dom/useInfiniteScroll';
|
|
32
32
|
// How many skeleton items to render while the component is loading.
|
|
33
33
|
const SKELETON_ITEM_COUNT = 4;
|
|
34
|
+
// The default for an omitted `options` prop. A default written inline (`options = []`) is a new
|
|
35
|
+
// array on every render, which invalidates every memoized value derived from the options.
|
|
36
|
+
const EMPTY_OPTIONS = [];
|
|
34
37
|
/**
|
|
35
38
|
* The MultipleSelectBox component.
|
|
36
39
|
* This component is separated from the SelectBox component because it has a different behavior.
|
|
37
40
|
* This component switches the variants of looks and behaviors depends on the props: size, invalid, disabled.
|
|
38
41
|
*/
|
|
39
|
-
export function MultipleSelectBox({ id, triggerProps, triggerWrapperProps, size, options =
|
|
42
|
+
export function MultipleSelectBox({ id, triggerProps, triggerWrapperProps, size, options = EMPTY_OPTIONS, defaultValue,
|
|
40
43
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
41
44
|
placeholder, emptyMessage, disabled, invalid, required, caution, targetDOMNode, name, onChange, value, showDisplayValueAsTag, renderDisplayValue, enableSearchOptions = false, notFoundMessage, searchBoxProps, loading = false, onSearchOptions, clearButtonProps, disableClearButton = false, optionPanelProps, popoverWrapperProps, enableAllOptionsControls = false, enableApplyControls = false, showSelectedCount = false, selectAllButtonProps, clearAllButtonProps, selectedCountProps, cancelButtonProps, applyButtonProps, renderOption, renderPopoverHeader, renderOptionList, renderPopoverFooter, onOpenStateChanged, enableAutoUnmount = true, onBlur, showGroupOptionDivider, enableVirtualization = false, virtualizationOptions, infiniteScroll, enableCreateOption = false, onCreateOption, createOptionProps, }) {
|
|
42
45
|
const classes = multipleSelectBoxSlotRecipe({ showGroupOptionDivider });
|
|
@@ -187,7 +190,7 @@ placeholder, emptyMessage, disabled, invalid, required, caution, targetDOMNode,
|
|
|
187
190
|
// Create an array of focusable (selectable and not disabled) options from filteredOptions
|
|
188
191
|
const focusableOptions = useMemo(() => filteredOptions.filter((option) => isSelectableOption(option) && option.value != null && !isOptionDisabled(option)), [filteredOptions]);
|
|
189
192
|
// Manage the keyboard navigation of options
|
|
190
|
-
const { handleOptionFocus, createKeyDownHandler, tabbableOptionIndex } = useOptionKeyboardNavigation({
|
|
193
|
+
const { handleOptionFocus, createKeyDownHandler, tabbableOptionIndex, resetTabbableIndex } = useOptionKeyboardNavigation({
|
|
191
194
|
filteredOptions: focusableOptions,
|
|
192
195
|
selectedValues: enableApplyControls ? temporaryValues : localSelectedValuesSet,
|
|
193
196
|
onSelectOption: handleSelectOption,
|
|
@@ -373,7 +376,13 @@ placeholder, emptyMessage, disabled, invalid, required, caution, targetDOMNode,
|
|
|
373
376
|
: undefined, children: renderListContent() }), isVirtualized ? renderInfiniteScrollLoading() : null, isVirtualized ? renderInfiniteScrollError('div') : null, _jsx(OptionGroupLabelDescriptions, { groups: rendersHiddenGroupLabels ? optionGroupLabels : [] })] }));
|
|
374
377
|
const createOptionAreaNode = isCreateOptionVisible ? (_jsxs("div", { className: cx(classes.createOptionArea, 'mfui-MultipleSelectBox__createOptionArea'), children: [_jsxs("button", { "data-mfui-content": "create-new-option", type: "button", className: cx(classes.createOptionButton, 'mfui-MultipleSelectBox__createOptionButton'), disabled: isCreating, onClick: handleCreateOption, children: [_jsx(Add, { "aria-hidden": true }), createOptionProps?.renderLabel?.(searchText.trim()) ?? (_jsxs(_Fragment, { children: [_jsx(Typography, { variant: "strongBody", children: searchText.trim() }), _jsx(Typography, { variant: "body", children: "\u3092\u8FFD\u52A0" })] }))] }), createError ? (_jsx("div", { className: cx(classes.createOptionErrorMessage, 'mfui-MultipleSelectBox__createOptionErrorMessage'), children: _jsx(HelpMessage, { messageType: "error", children: createOptionProps?.renderErrorMessage?.(searchText.trim()) ??
|
|
375
378
|
`${searchText.trim()}を追加できませんでした。` }) })) : null] })) : null;
|
|
379
|
+
const handleOpenStateChanged = (open) => {
|
|
380
|
+
// Every opening starts from the first selected option, not from the option focused last time.
|
|
381
|
+
if (open)
|
|
382
|
+
resetTabbableIndex();
|
|
383
|
+
toggleOptionPanel();
|
|
384
|
+
};
|
|
376
385
|
return (_jsx(Popover, { renderTrigger: ({ setTriggerRef, togglePopover, handleTriggerKeyDown, handleTriggerBlur }) => (_jsx(MultipleSelectBoxTrigger, { ref: triggerRef, wrapperRef: setTriggerRef, selectedOptions: localSelectedOptions, id: id, disabled: disabled, triggerProps: triggerProps, triggerWrapperProps: triggerWrapperProps, name: name,
|
|
377
386
|
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
378
|
-
placeholder: placeholder, size: size, listBoxId: listBoxId, isOptionPanelOpen: isOptionPanelOpen, invalid: invalid, required: required, caution: caution, showDisplayValueAsTag: showDisplayValueAsTag, renderDisplayValue: renderDisplayValue, clearButtonProps: clearButtonProps, disableClearButton: disableClearButton, updateSelectedValues: updateSelectedValues, onClick: togglePopover, onKeyDown: handleTriggerKeyDown, onBlur: handleTriggerBlur })), contentProps: { className: classes.popover }, minWidth: optionPanelProps?.minWidth, maxHeight: popoverWrapperProps?.maxHeight, allowedPlacements: optionPanelProps?.allowedPlacements, renderContent: () => (_jsxs("div", { ref: optionPanelRef, className: cx(classes.optionPanel, 'mfui-MultipleSelectBox__optionPanel', optionPanelProps?.className), tabIndex: -1, onKeyDown: handleKeyDownMenu, children: [enableSearchOptions || enableAllOptionsControls ? (_jsx(VStack, { className: cx(classes.menuHeader, 'mfui-MultipleSelectBox__menuHeader'), gap: "8px", children: renderPopoverHeader ? (renderPopoverHeader({ searchNode, allOptionsControlsNode })) : (_jsxs(_Fragment, { children: [searchNode, allOptionsControlsNode] })) })) : null, renderOptionList ? (renderOptionList({ optionsNode, createOptionAreaNode })) : (_jsxs(_Fragment, { children: [optionsNode, createOptionAreaNode] })), enableApplyControls || showSelectedCount ? (_jsx(HStack, { className: cx(classes.menuFooter, 'mfui-MultipleSelectBox__menuFooter'), justifyContent: "space-between", alignItems: "center", children: renderPopoverFooter ? (renderPopoverFooter({ selectedCountNode, applyControlsNode })) : (_jsxs(_Fragment, { children: [selectedCountNode, applyControlsNode] })) })) : null] })), open: isOptionPanelOpen, targetDOMNode: targetDOMNode, enableAutoUnmount: enableAutoUnmount, onOpenStateChanged:
|
|
387
|
+
placeholder: placeholder, size: size, listBoxId: listBoxId, isOptionPanelOpen: isOptionPanelOpen, invalid: invalid, required: required, caution: caution, showDisplayValueAsTag: showDisplayValueAsTag, renderDisplayValue: renderDisplayValue, clearButtonProps: clearButtonProps, disableClearButton: disableClearButton, updateSelectedValues: updateSelectedValues, onClick: togglePopover, onKeyDown: handleTriggerKeyDown, onBlur: handleTriggerBlur })), contentProps: { className: classes.popover }, minWidth: optionPanelProps?.minWidth, maxHeight: popoverWrapperProps?.maxHeight, allowedPlacements: optionPanelProps?.allowedPlacements, renderContent: () => (_jsxs("div", { ref: optionPanelRef, className: cx(classes.optionPanel, 'mfui-MultipleSelectBox__optionPanel', optionPanelProps?.className), tabIndex: -1, onKeyDown: handleKeyDownMenu, children: [enableSearchOptions || enableAllOptionsControls ? (_jsx(VStack, { className: cx(classes.menuHeader, 'mfui-MultipleSelectBox__menuHeader'), gap: "8px", children: renderPopoverHeader ? (renderPopoverHeader({ searchNode, allOptionsControlsNode })) : (_jsxs(_Fragment, { children: [searchNode, allOptionsControlsNode] })) })) : null, renderOptionList ? (renderOptionList({ optionsNode, createOptionAreaNode })) : (_jsxs(_Fragment, { children: [optionsNode, createOptionAreaNode] })), enableApplyControls || showSelectedCount ? (_jsx(HStack, { className: cx(classes.menuFooter, 'mfui-MultipleSelectBox__menuFooter'), justifyContent: "space-between", alignItems: "center", children: renderPopoverFooter ? (renderPopoverFooter({ selectedCountNode, applyControlsNode })) : (_jsxs(_Fragment, { children: [selectedCountNode, applyControlsNode] })) })) : null] })), open: isOptionPanelOpen, targetDOMNode: targetDOMNode, enableAutoUnmount: enableAutoUnmount, onOpenStateChanged: handleOpenStateChanged, onBlur: onBlur }));
|
|
379
388
|
}
|
|
@@ -34,5 +34,6 @@ export declare function useOptionKeyboardNavigation<T extends AllowedValueTypes
|
|
|
34
34
|
tabbableOptionIndex: number;
|
|
35
35
|
handleOptionFocus: (optionIndex: number) => void;
|
|
36
36
|
createKeyDownHandler: (option: MultipleSelectBoxOption<T, AdditionalProps>) => KeyboardEventHandler<HTMLLIElement>;
|
|
37
|
+
resetTabbableIndex: () => void;
|
|
37
38
|
};
|
|
38
39
|
export {};
|