@moneyforward/mfui-components 3.36.1 → 3.37.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 +4 -0
- package/dist/src/DataTable/DataTable.js +4 -0
- package/dist/src/DataTable/DataTableCell/DataTableCell.js +10 -2
- package/dist/src/DataTable/DataTableCell/DataTableCell.types.d.ts +32 -0
- package/dist/src/DataTable/DataTableHeaderCell/DataTableHeaderCell.js +5 -1
- package/dist/src/DataTable/DataTableRow/DataTableRow.types.d.ts +7 -0
- package/dist/src/MainNavigation/BaseMainNavigation.d.ts +1 -1
- package/dist/src/MainNavigation/BaseMainNavigation.js +23 -12
- package/dist/src/MainNavigation/MainNavigation.js +2 -2
- package/dist/src/MainNavigation/MainNavigation.types.d.ts +17 -1
- package/dist/src/MultipleSelectBox/MultipleSelectBox.js +54 -38
- package/dist/src/MultipleSelectBox/hooks/useVirtualizedMultipleSelectBoxOptions.d.ts +7 -6
- package/dist/src/MultipleSelectBox/hooks/useVirtualizedMultipleSelectBoxOptions.js +3 -3
- package/dist/src/SelectBox/SelectBox.js +67 -28
- package/dist/src/SelectBox/SelectBoxOption.d.ts +5 -0
- package/dist/src/SelectBox/SelectBoxOption.js +2 -2
- package/dist/src/SelectBox/SelectBoxOptionGroup.d.ts +9 -0
- package/dist/src/SelectBox/SelectBoxOptionGroup.js +6 -3
- package/dist/src/SelectBox/hooks/useVirtualizedOptions.d.ts +7 -6
- package/dist/src/SelectBox/hooks/useVirtualizedOptions.js +3 -3
- package/dist/src/shared/OptionGroupLabelDescriptions/OptionGroupLabelDescriptions.d.ts +37 -0
- package/dist/src/shared/OptionGroupLabelDescriptions/OptionGroupLabelDescriptions.js +32 -0
- package/dist/src/shared/OptionGroupLabelDescriptions/index.d.ts +1 -0
- package/dist/src/shared/OptionGroupLabelDescriptions/index.js +1 -0
- package/dist/src/utilities/dom/getGroupLabelId.d.ts +15 -0
- package/dist/src/utilities/dom/getGroupLabelId.js +15 -0
- package/dist/styled-system/recipes/base-main-navigation-slot-recipe.d.ts +1 -1
- package/dist/styled-system/recipes/base-main-navigation-slot-recipe.js +8 -0
- package/dist/styled-system/recipes/data-table-cell-slot-recipe.d.ts +1 -0
- package/dist/styled-system/recipes/data-table-cell-slot-recipe.js +4 -0
- package/dist/styles.css +15 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +9 -9
|
@@ -78,6 +78,8 @@ export declare const DataTable: import("react").ForwardRefExoticComponent<Omit<D
|
|
|
78
78
|
* This component should be used inside the `DataTable` component.
|
|
79
79
|
*
|
|
80
80
|
* It renders and extends the props of `td` element.
|
|
81
|
+
* When the `isRowHeader` prop is provided, it renders `th scope="row"` instead,
|
|
82
|
+
* which exposes the cell as the row header to assistive technology.
|
|
81
83
|
*
|
|
82
84
|
* When the `onClickCell` prop is provided, the cell is rendered as an interactive cell.
|
|
83
85
|
* Otherwise, the cell is rendered as a default cell.
|
|
@@ -92,6 +94,8 @@ export declare const DataTable: import("react").ForwardRefExoticComponent<Omit<D
|
|
|
92
94
|
*
|
|
93
95
|
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td
|
|
94
96
|
*
|
|
97
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/th
|
|
98
|
+
*
|
|
95
99
|
* @see https://react.dev/reference/rules/rules-of-hooks
|
|
96
100
|
*
|
|
97
101
|
* @example
|
|
@@ -123,6 +123,8 @@ export const DataTable = Object.assign(Base, {
|
|
|
123
123
|
* This component should be used inside the `DataTable` component.
|
|
124
124
|
*
|
|
125
125
|
* It renders and extends the props of `td` element.
|
|
126
|
+
* When the `isRowHeader` prop is provided, it renders `th scope="row"` instead,
|
|
127
|
+
* which exposes the cell as the row header to assistive technology.
|
|
126
128
|
*
|
|
127
129
|
* When the `onClickCell` prop is provided, the cell is rendered as an interactive cell.
|
|
128
130
|
* Otherwise, the cell is rendered as a default cell.
|
|
@@ -137,6 +139,8 @@ export const DataTable = Object.assign(Base, {
|
|
|
137
139
|
*
|
|
138
140
|
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/td
|
|
139
141
|
*
|
|
142
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/th
|
|
143
|
+
*
|
|
140
144
|
* @see https://react.dev/reference/rules/rules-of-hooks
|
|
141
145
|
*
|
|
142
146
|
* @example
|
|
@@ -10,7 +10,7 @@ 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
|
-
export const DataTableCell = forwardRef(({ onClickCell, children, type = 'text', className, isRowHighlighted, disabled = false, depth = 0, columnIndex, disclosureIconProps: nestingIconProps, style, ...props }, ref) => {
|
|
13
|
+
export const DataTableCell = forwardRef(({ onClickCell, children, type = 'text', className, isRowHighlighted, isRowHeader = false, disabled = false, depth = 0, columnIndex, disclosureIconProps: nestingIconProps, style, ...props }, ref) => {
|
|
14
14
|
const { size, leftFixedColumnIndex, rightFixedColumnIndex, loading, enableRowSelection, showVerticalBorder, layout, } = useDataTableContext();
|
|
15
15
|
const { cellRef, isFixedColumn, isEdgeFixedColumn, fixedPositionStyles } = useFixedColumns({
|
|
16
16
|
enableRowSelection,
|
|
@@ -27,6 +27,11 @@ export const DataTableCell = forwardRef(({ onClickCell, children, type = 'text',
|
|
|
27
27
|
isCheckboxCell: enableRowSelection && columnIndex === 0,
|
|
28
28
|
verticalBorder: showVerticalBorder,
|
|
29
29
|
layout,
|
|
30
|
+
// Passing `false` would emit a class for the unused `false` variant value, changing the
|
|
31
|
+
// class list of every existing cell. `undefined` keeps normal cells byte identical.
|
|
32
|
+
// The variants above deliberately keep passing `false`: retrofitting the same guard would
|
|
33
|
+
// itself change the class list of already shipped cells, which is what this guard avoids.
|
|
34
|
+
isRowHeader: isRowHeader || undefined,
|
|
30
35
|
});
|
|
31
36
|
const handleClick = (event) => {
|
|
32
37
|
onClickCell?.(event);
|
|
@@ -48,7 +53,10 @@ export const DataTableCell = forwardRef(({ onClickCell, children, type = 'text',
|
|
|
48
53
|
return (_jsx("div", { className: cx(classes.defaultCell, 'mfui-DataTableCell__defaultCell',
|
|
49
54
|
/* TODO: Remove in v5.0.0 */ 'mfui-DataGridCell__defaultCell'), children: _jsx(CellContent, { depth: depth, disclosureIconProps: nestingIconProps, classes: classes, children: children }) }));
|
|
50
55
|
})();
|
|
51
|
-
|
|
56
|
+
// A row header identifies its row, so it must be a `th` for assistive technology.
|
|
57
|
+
// `scope` is only interpreted on `th`, which is why it cannot be passed to a plain cell.
|
|
58
|
+
const Tag = isRowHeader ? 'th' : 'td';
|
|
59
|
+
return (_jsx(Tag, { ref: mergeRefs(ref, cellRef), scope: isRowHeader ? 'row' : undefined, className: cx(classes.root, 'mfui-DataTableCell__root',
|
|
52
60
|
/* TODO: Remove in v5.0.0 */ 'mfui-DataGridCell__root', className), style: { ...style, ...fixedPositionStyles, zIndex: isFixedColumn ? 1 : undefined }, ...props, children: cellContent }));
|
|
53
61
|
});
|
|
54
62
|
DataTableCell.displayName = 'DataTableCell';
|
|
@@ -34,6 +34,38 @@ export type DataTableCellProps = {
|
|
|
34
34
|
* @default undefined
|
|
35
35
|
*/
|
|
36
36
|
onClickCell?: (event?: React.MouseEvent<HTMLButtonElement>) => void;
|
|
37
|
+
/**
|
|
38
|
+
* Whether the cell is the row header, which identifies the row it belongs to.
|
|
39
|
+
*
|
|
40
|
+
* If `true`, the cell is rendered as `<th scope="row">` instead of `<td>`.
|
|
41
|
+
* This lets assistive technology announce the identifying cell of the row
|
|
42
|
+
* alongside every other cell in that row, so screen reader users navigating
|
|
43
|
+
* a wide table cell-by-cell can still tell which row they are in
|
|
44
|
+
* (e.g. 'Amount: ¥125,000, invoice_2026.pdf').
|
|
45
|
+
*
|
|
46
|
+
* Unlike `isRowHighlighted`, **this prop is intended to be passed by you.**
|
|
47
|
+
*
|
|
48
|
+
* The appearance is identical to a normal cell.
|
|
49
|
+
*
|
|
50
|
+
* Use it only when a column actually identifies the row, such as a file name or an
|
|
51
|
+
* invoice number. Set it on at most one cell per row. When every column holds an
|
|
52
|
+
* equally weighted attribute, there is no row header and marking one only makes
|
|
53
|
+
* the announcement verbose. The checkbox cell rendered by `enableRowSelection` is
|
|
54
|
+
* never the row header, so the row header is the second cell in that case.
|
|
55
|
+
*
|
|
56
|
+
* @default false
|
|
57
|
+
*
|
|
58
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/th
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```tsx
|
|
62
|
+
* <DataTable.Row>
|
|
63
|
+
* <DataTable.Cell isRowHeader>invoice_2026.pdf</DataTable.Cell>
|
|
64
|
+
* <DataTable.Cell type="amount">¥125,000</DataTable.Cell>
|
|
65
|
+
* </DataTable.Row>
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
isRowHeader?: boolean;
|
|
37
69
|
/**
|
|
38
70
|
* **Do not pass this prop manually.**
|
|
39
71
|
*
|
|
@@ -57,7 +57,11 @@ export const DataTableHeaderCell = forwardRef(({ children, type = 'text', sortBu
|
|
|
57
57
|
// Actions are rendered as siblings of the sort button (never inside it)
|
|
58
58
|
// to avoid nesting interactive elements.
|
|
59
59
|
const cellContentWithActions = actions ? (_jsxs("div", { className: cx(classes.container, 'mfui-DataTableHeaderCell__container'), children: [cellContent, _jsx("div", { className: cx(classes.actionsContainer, 'mfui-DataTableHeaderCell__actionsContainer'), children: actions })] })) : (cellContent);
|
|
60
|
-
return (_jsx("th", { ref: mergeRefs(ref, cellRef),
|
|
60
|
+
return (_jsx("th", { ref: mergeRefs(ref, cellRef),
|
|
61
|
+
// `th` also appears in the body now that `DataTable.Cell` supports `isRowHeader`, so the
|
|
62
|
+
// scope is stated explicitly rather than left to the auto-scope algorithm, whose result
|
|
63
|
+
// is harder to predict with `colSpan` / `rowSpan` and fixed columns.
|
|
64
|
+
scope: "col", className: cx(classes.root, 'mfui-DataTableHeaderCell__root',
|
|
61
65
|
/* TODO: Remove in v5.0.0 */ 'mfui-DataGridHeaderCell__root', className), "aria-sort": sortOrder, style: fixedPositionStyles, ...props, children: cellContentWithActions }));
|
|
62
66
|
});
|
|
63
67
|
DataTableHeaderCell.displayName = 'DataTableHeaderCell';
|
|
@@ -47,6 +47,13 @@ export type DataTableRowProps = {
|
|
|
47
47
|
* This prop will be passed to the `onClick` and `onKeyDown` of the `tr` element.
|
|
48
48
|
* When the element is clicked, the event is passed to the `onClickRow` prop, but event isn't passed when the Enter or Space key is pressed.
|
|
49
49
|
*
|
|
50
|
+
* Note that this callback is NOT called when the click originates from an interactive
|
|
51
|
+
* descendant inside the row — e.g. a button, link, input (including `Checkbox`), select,
|
|
52
|
+
* label, or an element with an interactive ARIA role such as `checkbox` or `menuitem`.
|
|
53
|
+
* Handle those interactions on the descendant itself (e.g. the `Checkbox`'s `onChange`).
|
|
54
|
+
* Likewise, Enter or Space triggers this callback only while the row itself is focused,
|
|
55
|
+
* not while a focusable descendant is focused.
|
|
56
|
+
*
|
|
50
57
|
* @default undefined
|
|
51
58
|
*/
|
|
52
59
|
onClickRow?: (event: React.MouseEvent<HTMLTableRowElement> | React.KeyboardEvent<HTMLTableRowElement>) => void;
|
|
@@ -3,4 +3,4 @@ import { type MainNavigationProps } from './MainNavigation.types';
|
|
|
3
3
|
* The component for the main navigation on the normal viewport.
|
|
4
4
|
* This component must only be used in MainNavigation.tsx, do not export it.
|
|
5
5
|
*/
|
|
6
|
-
export declare function BaseMainNavigation({ className, featureShortcut, navigationItems, enableCollapsible, width, customLinkComponent, disableResponsive, isCollapsed: controlledIsCollapsed, onCollapsedChange, collapseButtonProps, expandButtonProps, }: MainNavigationProps): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
export declare function BaseMainNavigation({ className, featureShortcut, navigationItems, enableCollapsible, nestedNavigationDisplay, width, customLinkComponent, disableResponsive, isCollapsed: controlledIsCollapsed, onCollapsedChange, collapseButtonProps, expandButtonProps, }: MainNavigationProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { DisclosureBasicCollapsed, DisclosureBasicExpanded, Lock, PaneExpansionToLeft, PaneExpansionToRight, } from '@moneyforward/mfui-icons-react';
|
|
3
|
+
import { DisclosureBasicCollapsed, DisclosureBasicExpanded, DisclosurePopoverCollapsed, DisclosurePopoverExpanded, Lock, PaneExpansionToLeft, PaneExpansionToRight, } from '@moneyforward/mfui-icons-react';
|
|
4
4
|
import { useId, useState, useRef } from 'react';
|
|
5
5
|
import { cx } from '../../styled-system/css';
|
|
6
6
|
import { FocusIndicator } from '../FocusIndicator';
|
|
@@ -13,7 +13,7 @@ import { Popover } from '../Popover';
|
|
|
13
13
|
* The component for the main navigation on the normal viewport.
|
|
14
14
|
* This component must only be used in MainNavigation.tsx, do not export it.
|
|
15
15
|
*/
|
|
16
|
-
export function BaseMainNavigation({ className, featureShortcut, navigationItems, enableCollapsible = false, width = 'normal', customLinkComponent, disableResponsive, isCollapsed: controlledIsCollapsed, onCollapsedChange, collapseButtonProps, expandButtonProps, }) {
|
|
16
|
+
export function BaseMainNavigation({ className, featureShortcut, navigationItems, enableCollapsible = false, nestedNavigationDisplay = 'accordion', width = 'normal', customLinkComponent, disableResponsive, isCollapsed: controlledIsCollapsed, onCollapsedChange, collapseButtonProps, expandButtonProps, }) {
|
|
17
17
|
const [localIsCollapsed, setLocalIsCollapsed] = useState(controlledIsCollapsed ?? false);
|
|
18
18
|
const [openPopoverIndex, setOpenPopoverIndex] = useState(null);
|
|
19
19
|
const isControlled = controlledIsCollapsed !== undefined;
|
|
@@ -38,7 +38,7 @@ export function BaseMainNavigation({ className, featureShortcut, navigationItems
|
|
|
38
38
|
return null;
|
|
39
39
|
}
|
|
40
40
|
if (navigation.children) {
|
|
41
|
-
return (_jsx(NestedNavigationItemRenderer, { navigation: navigation, originalIndex: index, classes: classes, InternalLinkTag: InternalLinkTag, isCollapsed: isCollapsed, openPopoverIndex: openPopoverIndex, setOpenPopoverIndex: setOpenPopoverIndex }, index));
|
|
41
|
+
return (_jsx(NestedNavigationItemRenderer, { navigation: navigation, originalIndex: index, navId: navId, classes: classes, InternalLinkTag: InternalLinkTag, isCollapsed: isCollapsed, nestedNavigationDisplay: nestedNavigationDisplay, openPopoverIndex: openPopoverIndex, setOpenPopoverIndex: setOpenPopoverIndex }, index));
|
|
42
42
|
}
|
|
43
43
|
return (_jsx("li", { className: cx(classes.listItem, 'mfui-BaseMainNavigation__listItem'), children: _jsx(Tooltip, { className: cx(classes.listItemTooltip, 'mfui-BaseMainNavigation__listItemTooltip'), content: navigation.label, disabled: !isCollapsed, placement: "right", children: _jsx(FocusIndicator, { position: "inside", children: _jsx(NavigationLink, { tag: InternalLinkTag, isExternal: navigation.isExternal, href: navigation.href, className: cx(classes.listItemAnchor, 'mfui-BaseMainNavigation__listItemAnchor'), "aria-label": isCollapsed ? navigation.label : undefined, "aria-current": navigation.isCurrent ? 'page' : undefined, children: _jsxs("div", { className: cx(classes.labelGroup, 'mfui-BaseMainNavigation__labelGroup'), children: [_jsxs("div", { className: cx(classes.iconAndLabel, 'mfui-BaseMainNavigation__iconAndLabel'), children: [_jsx("div", { "aria-hidden": "true", className: cx(classes.listItemAnchorIcon, 'mfui-BaseMainNavigation__listItemAnchorIcon'), children: navigation.icon }), isCollapsed ? null : _jsx(Typography, { variant: "controlLabel", children: navigation.label })] }), navigation.locked ? (_jsx(Lock, { className: cx(classes.lockIcon, 'mfui-BaseMainNavigation__lockIcon'), "aria-label": navigation.lockIconProps?.['aria-label'] ?? 'ロックされています' })) : null, !isCollapsed && navigation.statusSlot && navigation.locked !== true
|
|
44
44
|
? navigation.statusSlot
|
|
@@ -47,7 +47,7 @@ export function BaseMainNavigation({ className, featureShortcut, navigationItems
|
|
|
47
47
|
if (!('isGroupMenu' in navigation && navigation.isGroupMenu)) {
|
|
48
48
|
return null;
|
|
49
49
|
}
|
|
50
|
-
return (_jsx(NestedNavigationItemRenderer, { navigation: navigation, originalIndex: index, classes: classes, InternalLinkTag: InternalLinkTag, isCollapsed: isCollapsed, openPopoverIndex: openPopoverIndex, setOpenPopoverIndex: setOpenPopoverIndex }, index));
|
|
50
|
+
return (_jsx(NestedNavigationItemRenderer, { navigation: navigation, originalIndex: index, navId: navId, classes: classes, InternalLinkTag: InternalLinkTag, isCollapsed: isCollapsed, nestedNavigationDisplay: nestedNavigationDisplay, openPopoverIndex: openPopoverIndex, setOpenPopoverIndex: setOpenPopoverIndex }, index));
|
|
51
51
|
}) })), !isCollapsed &&
|
|
52
52
|
navigationItems.map((navigation, index) => {
|
|
53
53
|
const isGroupLabel = navigation.children && 'isGroupLabel' in navigation && navigation.isGroupLabel;
|
|
@@ -60,21 +60,23 @@ export function BaseMainNavigation({ className, featureShortcut, navigationItems
|
|
|
60
60
|
}
|
|
61
61
|
/**
|
|
62
62
|
* The component for rendering a parent navigation item with children.
|
|
63
|
-
* Handles both
|
|
63
|
+
* Handles both popover and details/summary display modes.
|
|
64
64
|
* This component must only be used in BaseMainNavigation.tsx, do not export it.
|
|
65
65
|
*/
|
|
66
|
-
function NestedNavigationItemRenderer({ navigation, originalIndex, classes, InternalLinkTag, isCollapsed, openPopoverIndex, setOpenPopoverIndex, }) {
|
|
67
|
-
|
|
66
|
+
function NestedNavigationItemRenderer({ navigation, originalIndex, navId, classes, InternalLinkTag, isCollapsed, nestedNavigationDisplay, openPopoverIndex, setOpenPopoverIndex, }) {
|
|
67
|
+
// Scope the ID to this navigation instance so multiple MainNavigation components
|
|
68
|
+
// on the same page (e.g. the Storybook docs page) don't portal into each other.
|
|
69
|
+
const popoverPortalTargetId = `${navId}-nested-popover-portal-target-${String(originalIndex)}`;
|
|
68
70
|
const hasCurrentChildren = navigation.children.some((child) => child.isCurrent);
|
|
69
|
-
return (_jsx("li", { className: cx(classes.listItem, 'mfui-BaseMainNavigation__listItem'), "data-mfui-popover-portal-target-id": popoverPortalTargetId, children: isCollapsed ? (_jsx(
|
|
71
|
+
return (_jsx("li", { className: cx(classes.listItem, 'mfui-BaseMainNavigation__listItem'), "data-mfui-popover-portal-target-id": popoverPortalTargetId, children: isCollapsed || nestedNavigationDisplay === 'popover' ? (_jsx(PopoverNestedNavigationItem, { navigation: navigation, classes: classes, InternalLinkTag: InternalLinkTag, isCollapsed: isCollapsed, hasCurrentChildren: hasCurrentChildren, popoverPortalTargetId: popoverPortalTargetId, isOpen: openPopoverIndex === originalIndex, onOpenChange: (isOpen) => {
|
|
70
72
|
setOpenPopoverIndex(isOpen ? originalIndex : null);
|
|
71
73
|
} })) : (_jsxs("details", { open: ('isOpenByDefault' in navigation && navigation.isOpenByDefault) || hasCurrentChildren, className: cx(classes.parentDetails, 'mfui-BaseMainNavigation__parentDetails'), children: [_jsx(FocusIndicator, { position: "inside", children: _jsxs("summary", { className: cx(classes.parentSummary, 'mfui-BaseMainNavigation__parentSummary'), children: [_jsx("div", { "aria-hidden": "true", className: cx(classes.parentSummaryIcon, 'mfui-BaseMainNavigation__parentSummaryIcon'), children: navigation.icon }), _jsx(Typography, { variant: "controlLabel", children: navigation.label }), _jsx(DisclosureBasicCollapsed, { className: "mfui-BaseMainNavigation__icon_collapsed", "aria-hidden": "true", "data-mfui-content": "main-navigation-icon-collapsed" }), _jsx(DisclosureBasicExpanded, { className: "mfui-BaseMainNavigation__icon_expanded", "aria-hidden": "true", "data-mfui-content": "main-navigation-icon-expanded" })] }) }), _jsx("ul", { className: cx(classes.childrenList, 'mfui-BaseMainNavigation__childrenList'), children: navigation.children.map((child, childIndex) => (_jsx("li", { className: cx(classes.childrenListItem, 'mfui-BaseMainNavigation__childrenListItem'), children: _jsx(FocusIndicator, { position: "inside", children: _jsx(NavigationLink, { tag: InternalLinkTag, isExternal: child.isExternal, href: child.href, className: cx(classes.childrenListItemAnchor, 'mfui-BaseMainNavigation__childrenListItemAnchor'), "aria-current": child.isCurrent ? 'page' : undefined, children: _jsx(Typography, { variant: "controlLabel", children: child.label }) }) }) }, childIndex))) })] })) }));
|
|
72
74
|
}
|
|
73
75
|
/**
|
|
74
|
-
* The component for displaying nested navigation
|
|
76
|
+
* The component for displaying nested navigation with a popover.
|
|
75
77
|
* This component must only be used in BaseMainNavigation.tsx, do not export it.
|
|
76
78
|
*/
|
|
77
|
-
function
|
|
79
|
+
function PopoverNestedNavigationItem({ navigation, classes, InternalLinkTag, isCollapsed, hasCurrentChildren, popoverPortalTargetId, isOpen, onOpenChange, }) {
|
|
78
80
|
const popoverContentRef = useRef(null);
|
|
79
81
|
const handleKeyDown = (event) => {
|
|
80
82
|
if (event.key === 'Enter' || event.key === ' ') {
|
|
@@ -87,9 +89,18 @@ function CollapsedNestedNavigationItem({ navigation, classes, InternalLinkTag, h
|
|
|
87
89
|
const handleClick = () => {
|
|
88
90
|
onOpenChange(!isOpen);
|
|
89
91
|
};
|
|
90
|
-
|
|
92
|
+
// The expanded trigger shows the label and the disclosure chevron inline,
|
|
93
|
+
// while the collapsed trigger is icon-only and exposes the label via aria-label and a tooltip.
|
|
94
|
+
const isExpanded = !isCollapsed;
|
|
95
|
+
const DisclosureIcon = isOpen ? DisclosurePopoverExpanded : DisclosurePopoverCollapsed;
|
|
96
|
+
const popoverContentId = `${popoverPortalTargetId}-content`;
|
|
97
|
+
return (_jsx(Popover, { enableAutoFocusOnPopover: false, enableAutoFocusOnTrigger: false, open: isOpen,
|
|
98
|
+
// `document` is unavailable during server-side prerendering.
|
|
99
|
+
targetDOMNode: typeof document === 'undefined'
|
|
100
|
+
? undefined
|
|
101
|
+
: document.querySelector(`[data-mfui-popover-portal-target-id="${popoverPortalTargetId}"]`), allowedPlacements: ['right-start', 'right', 'right-end'], contentProps: {
|
|
91
102
|
tabIndex: -1,
|
|
92
|
-
}, renderTrigger: ({ setTriggerRef }) => (_jsx(Tooltip, { className: cx(classes.listItemTooltip, 'mfui-BaseMainNavigation__listItemTooltip'), content: navigation.label, disabled: isOpen, placement: "right", children: _jsx(FocusIndicator, { position: "inside", children: _jsx("button", { ref: setTriggerRef, type: "button", className: cx(classes.listItemAnchor, 'mfui-BaseMainNavigation__listItemAnchor'), "data-mfui-current": hasCurrentChildren ? 'page' : undefined, "aria-current": hasCurrentChildren ? true : undefined, "aria-label": navigation.label, onClick: handleClick, onKeyDown: handleKeyDown, children:
|
|
103
|
+
}, renderTrigger: ({ setTriggerRef }) => (_jsx(Tooltip, { className: cx(classes.listItemTooltip, 'mfui-BaseMainNavigation__listItemTooltip'), content: navigation.label, disabled: isExpanded || isOpen, placement: "right", children: _jsx(FocusIndicator, { position: "inside", children: _jsx("button", { ref: setTriggerRef, type: "button", className: cx(classes.listItemAnchor, classes.popoverTrigger, 'mfui-BaseMainNavigation__listItemAnchor', 'mfui-BaseMainNavigation__popoverTrigger'), "data-mfui-current": hasCurrentChildren ? 'page' : undefined, "aria-current": hasCurrentChildren ? true : undefined, "aria-expanded": isOpen, "aria-controls": isOpen ? popoverContentId : undefined, "aria-label": isExpanded ? undefined : navigation.label, onClick: handleClick, onKeyDown: handleKeyDown, children: _jsxs("div", { className: cx(classes.labelGroup, 'mfui-BaseMainNavigation__labelGroup'), children: [_jsxs("div", { className: cx(classes.iconAndLabel, 'mfui-BaseMainNavigation__iconAndLabel'), children: [_jsx("div", { "aria-hidden": "true", className: cx(classes.listItemAnchorIcon, 'mfui-BaseMainNavigation__listItemAnchorIcon'), children: navigation.icon }), isExpanded ? _jsx(Typography, { variant: "controlLabel", children: navigation.label }) : null] }), isExpanded ? (_jsx(DisclosureIcon, { "aria-hidden": "true", className: cx(classes.popoverTriggerDisclosureIcon, 'mfui-BaseMainNavigation__popoverTriggerDisclosureIcon') })) : null] }) }) }) })), renderContent: () => (_jsx("div", { id: popoverContentId, ref: popoverContentRef, className: cx(classes.popoverChildrenList, 'mfui-BaseMainNavigation__popoverChildrenList'), tabIndex: -1, children: navigation.children.map((child, childIndex) => (_jsx("div", { className: cx(classes.childrenListItem, 'mfui-BaseMainNavigation__childrenListItem'), children: _jsx(FocusIndicator, { position: "inside", children: _jsx(NavigationLink, { tag: InternalLinkTag, isExternal: child.isExternal, href: child.href, className: cx(classes.childrenListItemAnchor, 'mfui-BaseMainNavigation__childrenListItemAnchor'), "aria-current": child.isCurrent ? 'page' : undefined, onClick: () => {
|
|
93
104
|
onOpenChange(false);
|
|
94
105
|
}, children: _jsx(Typography, { variant: "controlLabel", children: child.label }) }) }) }, childIndex))) })), onOpenStateChanged: onOpenChange }));
|
|
95
106
|
}
|
|
@@ -9,7 +9,7 @@ import { NarrowViewportMainNavigation } from './NarrowViewportMainNavigation';
|
|
|
9
9
|
* When the viewport is narrow, the navigation will be displayed as a drawer.
|
|
10
10
|
* When the viewport is not narrow, the navigation will be displayed as a sidebar.
|
|
11
11
|
*/
|
|
12
|
-
export const MainNavigation = forwardRef(({ className, featureShortcut, navigationItems, disableResponsive = false, width = 'normal', customLinkComponent, enableCollapsible, isCollapsed, onCollapsedChange, collapseButtonProps, expandButtonProps, }, ref) => {
|
|
12
|
+
export const MainNavigation = forwardRef(({ className, featureShortcut, navigationItems, disableResponsive = false, width = 'normal', customLinkComponent, enableCollapsible, nestedNavigationDisplay, isCollapsed, onCollapsedChange, collapseButtonProps, expandButtonProps, }, ref) => {
|
|
13
13
|
const commonProps = {
|
|
14
14
|
className,
|
|
15
15
|
featureShortcut,
|
|
@@ -17,5 +17,5 @@ export const MainNavigation = forwardRef(({ className, featureShortcut, navigati
|
|
|
17
17
|
width,
|
|
18
18
|
customLinkComponent,
|
|
19
19
|
};
|
|
20
|
-
return (_jsxs(_Fragment, { children: [_jsx(BaseMainNavigation, { ...commonProps, enableCollapsible: enableCollapsible, disableResponsive: disableResponsive, isCollapsed: isCollapsed, onCollapsedChange: onCollapsedChange, collapseButtonProps: collapseButtonProps, expandButtonProps: expandButtonProps }), !disableResponsive && _jsx(NarrowViewportMainNavigation, { ...commonProps, ref: ref })] }));
|
|
20
|
+
return (_jsxs(_Fragment, { children: [_jsx(BaseMainNavigation, { ...commonProps, enableCollapsible: enableCollapsible, nestedNavigationDisplay: nestedNavigationDisplay, disableResponsive: disableResponsive, isCollapsed: isCollapsed, onCollapsedChange: onCollapsedChange, collapseButtonProps: collapseButtonProps, expandButtonProps: expandButtonProps }), !disableResponsive && _jsx(NarrowViewportMainNavigation, { ...commonProps, ref: ref })] }));
|
|
21
21
|
});
|
|
@@ -85,6 +85,8 @@ type ParentNavigationItem = {
|
|
|
85
85
|
* Whether the nested navigation is open by default.
|
|
86
86
|
* When set to true, the parent navigation item is expanded on initial render,
|
|
87
87
|
* regardless of whether any child has `isCurrent: true`.
|
|
88
|
+
* This property has no effect when the nested navigation is shown in a popover —
|
|
89
|
+
* i.e. when `MainNavigation` has `nestedNavigationDisplay="popover"`, or while the navigation is collapsed.
|
|
88
90
|
*
|
|
89
91
|
* @default false
|
|
90
92
|
*/
|
|
@@ -193,6 +195,20 @@ export type MainNavigationProps = {
|
|
|
193
195
|
* @default false
|
|
194
196
|
*/
|
|
195
197
|
enableCollapsible?: boolean;
|
|
198
|
+
/**
|
|
199
|
+
* How nested navigation items display their children in the normal viewport navigation.
|
|
200
|
+
*
|
|
201
|
+
* - `'accordion'`: Children expand below the parent item while the navigation is expanded.
|
|
202
|
+
* When the navigation is collapsed, children are always shown in a popover.
|
|
203
|
+
* - `'popover'`: Children are shown in a popover to the right in both expanded and collapsed states.
|
|
204
|
+
*
|
|
205
|
+
* Prefer `'accordion'` while the navigation is expanded unless the design requires a popover.
|
|
206
|
+
* When set to `'popover'`, `isOpenByDefault` on navigation items has no effect.
|
|
207
|
+
* The narrow viewport navigation always displays nested items as an accordion regardless of this property.
|
|
208
|
+
*
|
|
209
|
+
* @default 'accordion'
|
|
210
|
+
*/
|
|
211
|
+
nestedNavigationDisplay?: 'accordion' | 'popover';
|
|
196
212
|
/**
|
|
197
213
|
* The width of the navigation.
|
|
198
214
|
*
|
|
@@ -265,5 +281,5 @@ export type MainNavigationRef = {
|
|
|
265
281
|
openMainNavigation: () => void;
|
|
266
282
|
closeMainNavigation: () => void;
|
|
267
283
|
};
|
|
268
|
-
export type NarrowViewportMainNavigationProps = Omit<MainNavigationProps, 'enableCollapsible'>;
|
|
284
|
+
export type NarrowViewportMainNavigationProps = Omit<MainNavigationProps, 'enableCollapsible' | 'nestedNavigationDisplay'>;
|
|
269
285
|
export {};
|
|
@@ -24,6 +24,8 @@ import { useApplyControls } from './hooks/useApplyControls';
|
|
|
24
24
|
import { useCreateOption } from './hooks/useCreateOption';
|
|
25
25
|
import { cx } from '../../styled-system/css';
|
|
26
26
|
import { flattenOptions } from './utils/flattenOptions';
|
|
27
|
+
import { getGroupLabelId } from '../utilities/dom/getGroupLabelId';
|
|
28
|
+
import { OptionGroupLabelDescriptions } from '../shared/OptionGroupLabelDescriptions';
|
|
27
29
|
import { isSelectableOption, isOptionDisabled, isOptionSelected } from './utils/isSelectableOption';
|
|
28
30
|
import { useVirtualizedMultipleSelectBoxOptions } from './hooks/useVirtualizedMultipleSelectBoxOptions';
|
|
29
31
|
import { useInfiniteScroll } from '../utilities/dom/useInfiniteScroll';
|
|
@@ -80,11 +82,50 @@ placeholder, emptyMessage, disabled, invalid, required, caution, targetDOMNode,
|
|
|
80
82
|
});
|
|
81
83
|
// Determine if virtualization should be enabled
|
|
82
84
|
const shouldVirtualize = enableVirtualization && !loading && flattenedOptions.length > 0;
|
|
85
|
+
// The options reference their group label with `aria-describedby`. A virtualized option list can
|
|
86
|
+
// unmount the visible label row, so there the referenced element is rendered outside the list
|
|
87
|
+
// instead - see `OptionGroupLabelDescriptions`. Only one of the two ever carries the id.
|
|
88
|
+
const rendersHiddenGroupLabels = shouldVirtualize;
|
|
89
|
+
const optionGroupLabels = useMemo(() =>
|
|
90
|
+
// Same check as `isSelectableOption`, inlined because its type guard narrows the negative branch
|
|
91
|
+
// to `never` here. A group label is any entry without a value, including one that carries no
|
|
92
|
+
// nested options - `flattenOptions` keeps those.
|
|
93
|
+
filteredOptions.flatMap((option, optionIndex) => 'value' in option ? [] : [{ id: getGroupLabelId(listBoxId, optionIndex), label: option.label }]), [filteredOptions, listBoxId]);
|
|
94
|
+
// Create the row structure for virtualization: one row per entry of the already flat option list,
|
|
95
|
+
// group labels included. The nested options of a group entry are not expanded here - `filteredOptions`
|
|
96
|
+
// is the output of `flattenOptions`, so they are already present as their own entries.
|
|
97
|
+
const createVirtualizedItems = useCallback(() => {
|
|
98
|
+
if (!shouldVirtualize)
|
|
99
|
+
return [];
|
|
100
|
+
const items = [];
|
|
101
|
+
// An option is described by the group label it is rendered under, which is the last group label
|
|
102
|
+
// rendered before it.
|
|
103
|
+
let currentGroupLabelId;
|
|
104
|
+
filteredOptions.forEach((option, optionIndex) => {
|
|
105
|
+
if ('options' in option && Array.isArray(option.options)) {
|
|
106
|
+
currentGroupLabelId = getGroupLabelId(listBoxId, optionIndex);
|
|
107
|
+
items.push({
|
|
108
|
+
type: 'group',
|
|
109
|
+
option,
|
|
110
|
+
groupLabelId: currentGroupLabelId,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
items.push({
|
|
115
|
+
type: 'option',
|
|
116
|
+
option,
|
|
117
|
+
groupLabelId: currentGroupLabelId,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
return items;
|
|
122
|
+
}, [filteredOptions, shouldVirtualize, listBoxId]);
|
|
123
|
+
const virtualizedItems = useMemo(() => createVirtualizedItems(), [createVirtualizedItems]);
|
|
83
124
|
// Initialize virtualization
|
|
84
125
|
const { virtualizer, virtualItems, totalSize, isVirtualized } = useVirtualizedMultipleSelectBoxOptions({
|
|
85
126
|
enabled: shouldVirtualize,
|
|
86
127
|
parentRef: scrollWrapperRef,
|
|
87
|
-
|
|
128
|
+
rowCount: virtualizedItems.length,
|
|
88
129
|
estimateSize: virtualizationOptions?.estimateSize,
|
|
89
130
|
overscan: virtualizationOptions?.overscan,
|
|
90
131
|
});
|
|
@@ -152,37 +193,6 @@ placeholder, emptyMessage, disabled, invalid, required, caution, targetDOMNode,
|
|
|
152
193
|
onSelectOption: handleSelectOption,
|
|
153
194
|
onClosePanel: closeOptionPanel,
|
|
154
195
|
});
|
|
155
|
-
// Create a flat structure for virtualization that includes both group headers and options
|
|
156
|
-
const createVirtualizedItems = useCallback(() => {
|
|
157
|
-
if (!shouldVirtualize)
|
|
158
|
-
return [];
|
|
159
|
-
const items = [];
|
|
160
|
-
filteredOptions.forEach((option) => {
|
|
161
|
-
if ('options' in option && Array.isArray(option.options)) {
|
|
162
|
-
// Add group header
|
|
163
|
-
items.push({
|
|
164
|
-
type: 'group',
|
|
165
|
-
option,
|
|
166
|
-
});
|
|
167
|
-
// Add group options
|
|
168
|
-
option.options.forEach((nestedOption) => {
|
|
169
|
-
items.push({
|
|
170
|
-
type: 'option',
|
|
171
|
-
option: nestedOption,
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
else {
|
|
176
|
-
// Regular option
|
|
177
|
-
items.push({
|
|
178
|
-
type: 'option',
|
|
179
|
-
option,
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
return items;
|
|
184
|
-
}, [filteredOptions, shouldVirtualize]);
|
|
185
|
-
const virtualizedItems = useMemo(() => createVirtualizedItems(), [createVirtualizedItems]);
|
|
186
196
|
// Render virtualized items - extracted for better maintainability
|
|
187
197
|
const renderVirtualizedItems = useCallback(() => {
|
|
188
198
|
let focusableIndex = 0;
|
|
@@ -190,7 +200,7 @@ placeholder, emptyMessage, disabled, invalid, required, caution, targetDOMNode,
|
|
|
190
200
|
const virtualizedItem = virtualizedItems[virtualItem.index];
|
|
191
201
|
if (!virtualizedItem)
|
|
192
202
|
return null;
|
|
193
|
-
const { type, option } = virtualizedItem;
|
|
203
|
+
const { type, option, groupLabelId } = virtualizedItem;
|
|
194
204
|
if (type === 'group') {
|
|
195
205
|
return (_jsx("div", { ref: virtualizer?.measureElement, "data-index": virtualItem.index, style: {
|
|
196
206
|
position: 'absolute',
|
|
@@ -198,7 +208,7 @@ placeholder, emptyMessage, disabled, invalid, required, caution, targetDOMNode,
|
|
|
198
208
|
left: 0,
|
|
199
209
|
width: '100%',
|
|
200
210
|
transform: `translateY(${String(virtualItem.start)}px)`,
|
|
201
|
-
}, children: _jsx("li", { className: cx(classes.listItem, 'mfui-MultipleSelectBox__listItem'), "data-mfui-group-label": "true",
|
|
211
|
+
}, children: _jsx("li", { role: "presentation", className: cx(classes.listItem, 'mfui-MultipleSelectBox__listItem'), "data-mfui-group-label": "true", children: _jsx(Typography, { variant: "label", children: option.label }) }) }, virtualItem.key));
|
|
202
212
|
}
|
|
203
213
|
if (isSelectableOption(option) && option.value != null) {
|
|
204
214
|
const { value, label } = option;
|
|
@@ -220,7 +230,7 @@ placeholder, emptyMessage, disabled, invalid, required, caution, targetDOMNode,
|
|
|
220
230
|
left: 0,
|
|
221
231
|
width: '100%',
|
|
222
232
|
transform: `translateY(${String(virtualItem.start)}px)`,
|
|
223
|
-
}, children: _jsx(FocusIndicator, { position: "inside", children: _jsx("li", { role: "option", "data-focusable": !isDisabled, "data-value": value, className: cx(classes.listItem, 'mfui-MultipleSelectBox__listItem'), "aria-selected": isSelected, "aria-disabled": isDisabled, tabIndex: isDisabled ? -1 : currentFocusableIndex === tabbableOptionIndex ? 0 : -1, onFocus: () => {
|
|
233
|
+
}, children: _jsx(FocusIndicator, { position: "inside", children: _jsx("li", { role: "option", "data-focusable": !isDisabled, "data-value": value, className: cx(classes.listItem, 'mfui-MultipleSelectBox__listItem'), "aria-selected": isSelected, "aria-disabled": isDisabled, "aria-describedby": groupLabelId, tabIndex: isDisabled ? -1 : currentFocusableIndex === tabbableOptionIndex ? 0 : -1, onFocus: () => {
|
|
224
234
|
if (!isDisabled)
|
|
225
235
|
handleOptionFocus(currentFocusableIndex);
|
|
226
236
|
}, onKeyDown: createKeyDownHandler(option), onClick: () => {
|
|
@@ -251,6 +261,9 @@ placeholder, emptyMessage, disabled, invalid, required, caution, targetDOMNode,
|
|
|
251
261
|
const renderNonVirtualizedItems = useCallback(() => {
|
|
252
262
|
// Map from filteredOptions index to focusableOptions index
|
|
253
263
|
let focusableIndex = 0;
|
|
264
|
+
// An option is described by the group label it is rendered under, which is the last group label
|
|
265
|
+
// rendered before it.
|
|
266
|
+
let currentGroupLabelId;
|
|
254
267
|
return filteredOptions.map((option, optionIndex) => {
|
|
255
268
|
if (isSelectableOption(option)) {
|
|
256
269
|
// If selectable but value is null/undefined, skip rendering
|
|
@@ -268,7 +281,7 @@ placeholder, emptyMessage, disabled, invalid, required, caution, targetDOMNode,
|
|
|
268
281
|
currentFocusableIndex = focusableIndex;
|
|
269
282
|
focusableIndex += 1;
|
|
270
283
|
}
|
|
271
|
-
return (_jsx(FocusIndicator, { position: "inside", children: _jsx("li", { role: "option", "data-focusable": !isDisabled, className: cx(classes.listItem, 'mfui-MultipleSelectBox__listItem'), "aria-selected": isSelected, "aria-disabled": isDisabled, tabIndex: isDisabled ? -1 : currentFocusableIndex === tabbableOptionIndex ? 0 : -1, onFocus: () => {
|
|
284
|
+
return (_jsx(FocusIndicator, { position: "inside", children: _jsx("li", { role: "option", "data-focusable": !isDisabled, className: cx(classes.listItem, 'mfui-MultipleSelectBox__listItem'), "aria-selected": isSelected, "aria-disabled": isDisabled, "aria-describedby": currentGroupLabelId, tabIndex: isDisabled ? -1 : currentFocusableIndex === tabbableOptionIndex ? 0 : -1, onFocus: () => {
|
|
272
285
|
if (!isDisabled)
|
|
273
286
|
handleOptionFocus(currentFocusableIndex);
|
|
274
287
|
}, onKeyDown: createKeyDownHandler(option), onClick: () => {
|
|
@@ -280,7 +293,8 @@ placeholder, emptyMessage, disabled, invalid, required, caution, targetDOMNode,
|
|
|
280
293
|
}
|
|
281
294
|
// Only render group label if truly a group label (no value property at all)
|
|
282
295
|
const { label } = option;
|
|
283
|
-
|
|
296
|
+
currentGroupLabelId = getGroupLabelId(listBoxId, optionIndex);
|
|
297
|
+
return (_jsx("li", { id: rendersHiddenGroupLabels ? undefined : currentGroupLabelId, role: "presentation", className: classes.listItem, "data-mfui-group-label": "true", children: _jsx(Typography, { variant: "label", children: label }) }, `group-label-${String(optionIndex)}`));
|
|
284
298
|
});
|
|
285
299
|
}, [
|
|
286
300
|
filteredOptions,
|
|
@@ -293,6 +307,8 @@ placeholder, emptyMessage, disabled, invalid, required, caution, targetDOMNode,
|
|
|
293
307
|
createKeyDownHandler,
|
|
294
308
|
handleOptionFocus,
|
|
295
309
|
tabbableOptionIndex,
|
|
310
|
+
listBoxId,
|
|
311
|
+
rendersHiddenGroupLabels,
|
|
296
312
|
]);
|
|
297
313
|
// Render infinite scroll error message with retry button
|
|
298
314
|
const renderInfiniteScrollError = useCallback((as = 'li') => {
|
|
@@ -345,7 +361,7 @@ placeholder, emptyMessage, disabled, invalid, required, caution, targetDOMNode,
|
|
|
345
361
|
? resolvedNotFoundMessage
|
|
346
362
|
: emptyMessage }) }, "empty"),
|
|
347
363
|
!isVirtualized && renderInfiniteScrollError(),
|
|
348
|
-
].filter(Boolean) }), isVirtualized ? renderInfiniteScrollLoading() : null, isVirtualized ? renderInfiniteScrollError('div') : null] }));
|
|
364
|
+
].filter(Boolean) }), isVirtualized ? renderInfiniteScrollLoading() : null, isVirtualized ? renderInfiniteScrollError('div') : null, _jsx(OptionGroupLabelDescriptions, { groups: rendersHiddenGroupLabels ? optionGroupLabels : [] })] }));
|
|
349
365
|
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()) ??
|
|
350
366
|
`${searchText.trim()}を追加できませんでした。` }) })) : null] })) : null;
|
|
351
367
|
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,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
2
2
|
import { type RefObject } from 'react';
|
|
3
|
-
import { type
|
|
4
|
-
export type UseVirtualizedMultipleSelectBoxOptionsParams
|
|
3
|
+
import { type VirtualOptionTypes } from '../MultipleSelectBox.types';
|
|
4
|
+
export type UseVirtualizedMultipleSelectBoxOptionsParams = {
|
|
5
5
|
/**
|
|
6
6
|
* Enable virtualization for the option list
|
|
7
7
|
*/
|
|
@@ -11,9 +11,10 @@ export type UseVirtualizedMultipleSelectBoxOptionsParams<T extends string | numb
|
|
|
11
11
|
*/
|
|
12
12
|
parentRef: RefObject<HTMLElement | null>;
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
14
|
+
* Number of rows the option list renders. Group labels are rows too, so this is not the number of
|
|
15
|
+
* options: sizing the scroll area by the option count alone makes the last rows unreachable.
|
|
15
16
|
*/
|
|
16
|
-
|
|
17
|
+
rowCount: number;
|
|
17
18
|
} & Partial<VirtualOptionTypes>;
|
|
18
19
|
export type UseVirtualizedMultipleSelectBoxOptionsReturn = {
|
|
19
20
|
/**
|
|
@@ -54,10 +55,10 @@ export type UseVirtualizedMultipleSelectBoxOptionsReturn = {
|
|
|
54
55
|
* const { virtualItems, totalSize, scrollToIndex, isVirtualized } = useVirtualizedMultipleSelectBoxOptions({
|
|
55
56
|
* enabled: true,
|
|
56
57
|
* parentRef: optionPanelRef,
|
|
57
|
-
*
|
|
58
|
+
* rowCount: virtualizedItems.length,
|
|
58
59
|
* estimateSize: () => 36, // or (index) => index < 10 ? 40 : 36 for dynamic sizing
|
|
59
60
|
* overscan: 5,
|
|
60
61
|
* });
|
|
61
62
|
* ```
|
|
62
63
|
*/
|
|
63
|
-
export declare function useVirtualizedMultipleSelectBoxOptions
|
|
64
|
+
export declare function useVirtualizedMultipleSelectBoxOptions({ enabled, parentRef, rowCount, estimateSize, overscan, }: UseVirtualizedMultipleSelectBoxOptionsParams): UseVirtualizedMultipleSelectBoxOptionsReturn;
|
|
@@ -13,17 +13,17 @@ import { useCallback } from 'react';
|
|
|
13
13
|
* const { virtualItems, totalSize, scrollToIndex, isVirtualized } = useVirtualizedMultipleSelectBoxOptions({
|
|
14
14
|
* enabled: true,
|
|
15
15
|
* parentRef: optionPanelRef,
|
|
16
|
-
*
|
|
16
|
+
* rowCount: virtualizedItems.length,
|
|
17
17
|
* estimateSize: () => 36, // or (index) => index < 10 ? 40 : 36 for dynamic sizing
|
|
18
18
|
* overscan: 5,
|
|
19
19
|
* });
|
|
20
20
|
* ```
|
|
21
21
|
*/
|
|
22
|
-
export function useVirtualizedMultipleSelectBoxOptions({ enabled, parentRef,
|
|
22
|
+
export function useVirtualizedMultipleSelectBoxOptions({ enabled, parentRef, rowCount, estimateSize = () => 36, overscan = 5, }) {
|
|
23
23
|
const shouldVirtualize = enabled;
|
|
24
24
|
// Always call useVirtualizer to maintain hook order, but with 0 count when disabled
|
|
25
25
|
const virtualizer = useVirtualizer({
|
|
26
|
-
count: shouldVirtualize ?
|
|
26
|
+
count: shouldVirtualize ? rowCount : 0,
|
|
27
27
|
getScrollElement: () =>
|
|
28
28
|
// The parentRef itself is the scrollable element (option panel div)
|
|
29
29
|
parentRef.current ?? null,
|