@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.
Files changed (33) hide show
  1. package/dist/src/DataTable/DataTable.d.ts +4 -0
  2. package/dist/src/DataTable/DataTable.js +4 -0
  3. package/dist/src/DataTable/DataTableCell/DataTableCell.js +10 -2
  4. package/dist/src/DataTable/DataTableCell/DataTableCell.types.d.ts +32 -0
  5. package/dist/src/DataTable/DataTableHeaderCell/DataTableHeaderCell.js +5 -1
  6. package/dist/src/DataTable/DataTableRow/DataTableRow.types.d.ts +7 -0
  7. package/dist/src/MainNavigation/BaseMainNavigation.d.ts +1 -1
  8. package/dist/src/MainNavigation/BaseMainNavigation.js +23 -12
  9. package/dist/src/MainNavigation/MainNavigation.js +2 -2
  10. package/dist/src/MainNavigation/MainNavigation.types.d.ts +17 -1
  11. package/dist/src/MultipleSelectBox/MultipleSelectBox.js +54 -38
  12. package/dist/src/MultipleSelectBox/hooks/useVirtualizedMultipleSelectBoxOptions.d.ts +7 -6
  13. package/dist/src/MultipleSelectBox/hooks/useVirtualizedMultipleSelectBoxOptions.js +3 -3
  14. package/dist/src/SelectBox/SelectBox.js +67 -28
  15. package/dist/src/SelectBox/SelectBoxOption.d.ts +5 -0
  16. package/dist/src/SelectBox/SelectBoxOption.js +2 -2
  17. package/dist/src/SelectBox/SelectBoxOptionGroup.d.ts +9 -0
  18. package/dist/src/SelectBox/SelectBoxOptionGroup.js +6 -3
  19. package/dist/src/SelectBox/hooks/useVirtualizedOptions.d.ts +7 -6
  20. package/dist/src/SelectBox/hooks/useVirtualizedOptions.js +3 -3
  21. package/dist/src/shared/OptionGroupLabelDescriptions/OptionGroupLabelDescriptions.d.ts +37 -0
  22. package/dist/src/shared/OptionGroupLabelDescriptions/OptionGroupLabelDescriptions.js +32 -0
  23. package/dist/src/shared/OptionGroupLabelDescriptions/index.d.ts +1 -0
  24. package/dist/src/shared/OptionGroupLabelDescriptions/index.js +1 -0
  25. package/dist/src/utilities/dom/getGroupLabelId.d.ts +15 -0
  26. package/dist/src/utilities/dom/getGroupLabelId.js +15 -0
  27. package/dist/styled-system/recipes/base-main-navigation-slot-recipe.d.ts +1 -1
  28. package/dist/styled-system/recipes/base-main-navigation-slot-recipe.js +8 -0
  29. package/dist/styled-system/recipes/data-table-cell-slot-recipe.d.ts +1 -0
  30. package/dist/styled-system/recipes/data-table-cell-slot-recipe.js +4 -0
  31. package/dist/styles.css +15 -0
  32. package/dist/tsconfig.build.tsbuildinfo +1 -1
  33. package/package.json +9 -9
@@ -25,6 +25,8 @@ import { SelectBoxOptionGroup } from './SelectBoxOptionGroup';
25
25
  import { isOptionDisabled } from './utils/isSelectableOption';
26
26
  import { useVirtualizedOptions } from './hooks/useVirtualizedOptions';
27
27
  import { useInfiniteScroll } from '../utilities/dom/useInfiniteScroll';
28
+ import { getGroupLabelId } from '../utilities/dom/getGroupLabelId';
29
+ import { OptionGroupLabelDescriptions } from '../shared/OptionGroupLabelDescriptions';
28
30
  const DEFAULT_SCROLL_OPTIONS = {
29
31
  behavior: 'auto',
30
32
  block: 'center',
@@ -85,48 +87,79 @@ export const SelectBox = forwardRef((props, ref) => {
85
87
  // Create a flat structure for virtualization that includes both group headers and options
86
88
  const createVirtualizedItems = useCallback(() => {
87
89
  if (!shouldVirtualize)
88
- return [];
90
+ return { items: [], rowIndexByOptionIndex: new Map() };
89
91
  const items = [];
90
- filteredOptions.forEach((option) => {
92
+ // An option is described by the group label it is rendered under, which is the last group label
93
+ // rendered before it. Options nested in a group and options that follow a group without being
94
+ // nested in it are indistinguishable in the rendered list, so they are treated the same way.
95
+ let currentGroupLabelId;
96
+ // Group labels occupy a row of their own, so the index of an option in `flatOptions` is not the
97
+ // index of its row. Callers that scroll to an option know the former and need the latter.
98
+ const rowIndexByOptionIndex = new Map();
99
+ // `findFlatOptionIndexByValue` returns -1 for an option without a value. Those are not registered,
100
+ // so scrolling to them stays out of bounds and is rejected, as it was before this mapping existed.
101
+ const registerRowIndex = (flatOptionIndex) => {
102
+ if (flatOptionIndex >= 0)
103
+ rowIndexByOptionIndex.set(flatOptionIndex, items.length);
104
+ };
105
+ filteredOptions.forEach((option, optionIndex) => {
91
106
  if (!option)
92
107
  return;
93
108
  if ('options' in option && Array.isArray(option.options)) {
109
+ currentGroupLabelId = getGroupLabelId(listBoxId, optionIndex);
94
110
  // Add group header
95
111
  items.push({
96
112
  type: 'group',
97
113
  option,
114
+ groupLabelId: currentGroupLabelId,
98
115
  });
99
116
  // Add group options
100
117
  option.options.forEach((nestedOption) => {
101
118
  const flatOptionIndex = findFlatOptionIndexByValue(valueToIndexMap, nestedOption);
119
+ registerRowIndex(flatOptionIndex);
102
120
  items.push({
103
121
  type: 'option',
104
122
  option: nestedOption,
105
123
  originalIndex: flatOptionIndex,
124
+ groupLabelId: currentGroupLabelId,
106
125
  });
107
126
  });
108
127
  }
109
128
  else {
110
129
  // Regular option
111
130
  const flatOptionIndex = findFlatOptionIndexByValue(valueToIndexMap, option);
131
+ registerRowIndex(flatOptionIndex);
112
132
  items.push({
113
133
  type: 'option',
114
134
  option,
115
135
  originalIndex: flatOptionIndex,
136
+ groupLabelId: currentGroupLabelId,
116
137
  });
117
138
  }
118
139
  });
119
- return items;
120
- }, [filteredOptions, shouldVirtualize, valueToIndexMap]);
121
- const virtualizedItems = useMemo(() => createVirtualizedItems(), [createVirtualizedItems]);
140
+ return { items, rowIndexByOptionIndex };
141
+ }, [filteredOptions, shouldVirtualize, valueToIndexMap, listBoxId]);
142
+ const { items: virtualizedItems, rowIndexByOptionIndex } = useMemo(() => createVirtualizedItems(), [createVirtualizedItems]);
143
+ // The options reference their group label with `aria-describedby`. A virtualized option list can
144
+ // unmount the visible label row, so there the referenced element is rendered outside the list
145
+ // instead - see `OptionGroupLabelDescriptions`. Only one of the two ever carries the id.
146
+ const rendersHiddenGroupLabels = shouldVirtualize;
147
+ const optionGroupLabels = useMemo(() => filteredOptions.flatMap((option, optionIndex) => option && 'options' in option && Array.isArray(option.options)
148
+ ? [{ id: getGroupLabelId(listBoxId, optionIndex), label: option.label }]
149
+ : []), [filteredOptions, listBoxId]);
122
150
  // Initialize virtualization
123
151
  const { virtualizer, virtualItems, totalSize, scrollToIndex, isVirtualized } = useVirtualizedOptions({
124
152
  enabled: shouldVirtualize,
125
153
  parentRef: scrollWrapperRef,
126
- flatOptions,
154
+ rowCount: virtualizedItems.length,
127
155
  estimateSize: virtualizationOptions?.estimateSize,
128
156
  overscan: virtualizationOptions?.overscan,
129
157
  });
158
+ // Keyboard navigation and type-ahead know the index of an option in `flatOptions`, which is not
159
+ // the index of its row once group labels are rendered.
160
+ const scrollToOptionIndex = useCallback((optionIndex, options) => {
161
+ scrollToIndex(rowIndexByOptionIndex.get(optionIndex) ?? optionIndex, options);
162
+ }, [scrollToIndex, rowIndexByOptionIndex]);
130
163
  // Extract infinite scroll configuration with defaults
131
164
  const baseEnabledInfiniteScroll = infiniteScroll?.enabled ?? false;
132
165
  const onLoadMore = infiniteScroll?.onLoadMore;
@@ -155,7 +188,7 @@ export const SelectBox = forwardRef((props, ref) => {
155
188
  }, [onChange, closeOptionPanel]);
156
189
  const { handleTypeAhead, cleanup } = useTypeAhead({
157
190
  options: flatOptions,
158
- scrollToIndex,
191
+ scrollToIndex: scrollToOptionIndex,
159
192
  isVirtualized,
160
193
  onSelectOption: (selectedOption) => {
161
194
  if (selectedOption.value !== undefined) {
@@ -195,7 +228,7 @@ export const SelectBox = forwardRef((props, ref) => {
195
228
  onCloseOptionPanel: closeOptionPanel,
196
229
  optionPanelRef,
197
230
  searchInputRef,
198
- scrollToIndex,
231
+ scrollToIndex: scrollToOptionIndex,
199
232
  isVirtualized,
200
233
  });
201
234
  const { handleOnKeyDown: handleKeyDownMenu } = useFocusTrap({
@@ -221,7 +254,7 @@ export const SelectBox = forwardRef((props, ref) => {
221
254
  const virtualizedItem = virtualizedItems[virtualItem.index];
222
255
  if (!virtualizedItem)
223
256
  return null;
224
- const { type, option, originalIndex } = virtualizedItem;
257
+ const { type, option, originalIndex, groupLabelId } = virtualizedItem;
225
258
  if (type === 'group') {
226
259
  return (_jsx("div", { ref: virtualizer?.measureElement, "data-index": virtualItem.index, style: {
227
260
  position: 'absolute',
@@ -229,7 +262,7 @@ export const SelectBox = forwardRef((props, ref) => {
229
262
  left: 0,
230
263
  width: '100%',
231
264
  transform: `translateY(${String(virtualItem.start)}px)`,
232
- }, children: _jsx("li", { className: cx(classes.listItem, 'mfui-SelectBox__listItem'), "data-mfui-group-label": "true", "aria-hidden": "true", children: _jsx(Typography, { variant: "label", children: option.label }) }) }, virtualItem.key));
265
+ }, children: _jsx("li", { role: "presentation", className: cx(classes.listItem, 'mfui-SelectBox__listItem'), "data-mfui-group-label": "true", children: _jsx(Typography, { variant: "label", children: option.label }) }) }, virtualItem.key));
233
266
  }
234
267
  return (_jsx("div", { ref: virtualizer?.measureElement, "data-index": virtualItem.index, style: {
235
268
  position: 'absolute',
@@ -237,7 +270,7 @@ export const SelectBox = forwardRef((props, ref) => {
237
270
  left: 0,
238
271
  width: '100%',
239
272
  transform: `translateY(${String(virtualItem.start)}px)`,
240
- }, children: _jsx(SelectBoxOptionComponent, { option: option, isSelected: localSelectedOption?.value === option.value, tabbableOptionIndex: tabbableOptionIndex, optionIndex: originalIndex ?? virtualItem.index, classes: classes, renderOption: renderOption, onFocus: handleOptionFocus, onClick: handleSelectOption, onKeyDown: (event) => {
273
+ }, children: _jsx(SelectBoxOptionComponent, { option: option, isSelected: localSelectedOption?.value === option.value, tabbableOptionIndex: tabbableOptionIndex, optionIndex: originalIndex ?? virtualItem.index, classes: classes, groupLabelId: groupLabelId, renderOption: renderOption, onFocus: handleOptionFocus, onClick: handleSelectOption, onKeyDown: (event) => {
241
274
  handleTypeAhead(event.nativeEvent);
242
275
  createKeyDownHandler(option)(event);
243
276
  } }) }, virtualItem.key));
@@ -255,29 +288,35 @@ export const SelectBox = forwardRef((props, ref) => {
255
288
  virtualizer,
256
289
  ]);
257
290
  // Render non-virtualized items - extracted for better maintainability
258
- const renderNonVirtualizedItems = useCallback(() => filteredOptions.map((option, optionIndex) => {
259
- if (!option)
260
- return null;
261
- if ('options' in option && Array.isArray(option.options)) {
262
- return (_jsx(React.Fragment, { children: _jsx(SelectBoxOptionGroup, { groupOption: option, selectedOption: localSelectedOption, valueToIndexMap: valueToIndexMap, tabbableOptionIndex: tabbableOptionIndex, classes: classes, renderOption: renderOption, findFlatOptionIndexByValue: (_, option) => findFlatOptionIndexByValue(valueToIndexMap, option), onOptionFocus: handleOptionFocus, onOptionClick: handleSelectOption, onOptionKeyDown: (option) => (event) => {
291
+ const renderNonVirtualizedItems = useCallback(() => {
292
+ let currentGroupLabelId;
293
+ return filteredOptions.map((option, optionIndex) => {
294
+ if (!option)
295
+ return null;
296
+ if ('options' in option && Array.isArray(option.options)) {
297
+ currentGroupLabelId = getGroupLabelId(listBoxId, optionIndex);
298
+ return (_jsx(React.Fragment, { children: _jsx(SelectBoxOptionGroup, { groupOption: option, selectedOption: localSelectedOption, valueToIndexMap: valueToIndexMap, tabbableOptionIndex: tabbableOptionIndex, classes: classes, groupLabelId: currentGroupLabelId, hasHiddenGroupLabel: rendersHiddenGroupLabels, renderOption: renderOption, findFlatOptionIndexByValue: (_, option) => findFlatOptionIndexByValue(valueToIndexMap, option), onOptionFocus: handleOptionFocus, onOptionClick: handleSelectOption, onOptionKeyDown: (option) => (event) => {
299
+ handleTypeAhead(event.nativeEvent);
300
+ createKeyDownHandler(option)(event);
301
+ } }) }, `group-label-${option.label}-${String(optionIndex)}`));
302
+ }
303
+ if (!('options' in option)) {
304
+ const flatOptionIndex = findFlatOptionIndexByValue(valueToIndexMap, option);
305
+ return (_jsx(SelectBoxOptionComponent, { option: option, isSelected: localSelectedOption?.value === option.value, tabbableOptionIndex: tabbableOptionIndex, optionIndex: flatOptionIndex, classes: classes, groupLabelId: currentGroupLabelId, renderOption: renderOption, onFocus: handleOptionFocus, onClick: handleSelectOption, onKeyDown: (event) => {
263
306
  handleTypeAhead(event.nativeEvent);
264
307
  createKeyDownHandler(option)(event);
265
- } }) }, `group-label-${option.label}-${String(optionIndex)}`));
266
- }
267
- if (!('options' in option)) {
268
- const flatOptionIndex = findFlatOptionIndexByValue(valueToIndexMap, option);
269
- return (_jsx(SelectBoxOptionComponent, { option: option, isSelected: localSelectedOption?.value === option.value, tabbableOptionIndex: tabbableOptionIndex, optionIndex: flatOptionIndex, classes: classes, renderOption: renderOption, onFocus: handleOptionFocus, onClick: handleSelectOption, onKeyDown: (event) => {
270
- handleTypeAhead(event.nativeEvent);
271
- createKeyDownHandler(option)(event);
272
- } }, option.value ?? `option-${option.label}-${String(optionIndex)}`));
273
- }
274
- return null;
275
- }), [
308
+ } }, option.value ?? `option-${option.label}-${String(optionIndex)}`));
309
+ }
310
+ return null;
311
+ });
312
+ }, [
276
313
  filteredOptions,
277
314
  localSelectedOption,
278
315
  valueToIndexMap,
279
316
  tabbableOptionIndex,
280
317
  classes,
318
+ listBoxId,
319
+ rendersHiddenGroupLabels,
281
320
  renderOption,
282
321
  handleOptionFocus,
283
322
  handleSelectOption,
@@ -329,7 +368,7 @@ export const SelectBox = forwardRef((props, ref) => {
329
368
  !isVirtualized && renderInfiniteScrollLoading(),
330
369
  !isVirtualized && renderInfiniteScrollError(),
331
370
  ].filter(Boolean)
332
- : [renderEmptyMessage(), !isVirtualized && renderInfiniteScrollError()].filter(Boolean) }), isVirtualized ? renderInfiniteScrollLoading() : null, isVirtualized ? renderInfiniteScrollError('div') : null] }));
371
+ : [renderEmptyMessage(), !isVirtualized && renderInfiniteScrollError()].filter(Boolean) }), isVirtualized ? renderInfiniteScrollLoading() : null, isVirtualized ? renderInfiniteScrollError('div') : null, _jsx(OptionGroupLabelDescriptions, { groups: rendersHiddenGroupLabels ? optionGroupLabels : [] })] }));
333
372
  const handleClearValue = () => {
334
373
  setLocalSelectedOption(null);
335
374
  onChange?.(null);
@@ -23,6 +23,11 @@ type SelectBoxOptionProps<T extends string | number | undefined = string, Additi
23
23
  classes: {
24
24
  listItem: string;
25
25
  };
26
+ /**
27
+ * Id of the hidden element that carries the label of the option group this option is rendered
28
+ * under. See `OptionGroupLabelDescriptions`.
29
+ */
30
+ groupLabelId?: string;
26
31
  /**
27
32
  * Custom render function for the option.
28
33
  */
@@ -10,7 +10,7 @@ import { isOptionDisabled } from './utils/isSelectableOption';
10
10
  * Individual option component for SelectBox.
11
11
  * Memoized to prevent unnecessary re-renders when other options change.
12
12
  */
13
- function SelectBoxOptionComponentInner({ option, isSelected, tabbableOptionIndex, optionIndex, classes, renderOption, onFocus, onClick, onKeyDown, }) {
13
+ function SelectBoxOptionComponentInner({ option, isSelected, tabbableOptionIndex, optionIndex, classes, groupLabelId, renderOption, onFocus, onClick, onKeyDown, }) {
14
14
  const isDisabled = isOptionDisabled(option);
15
15
  const tabIndex = useMemo(() => (isDisabled ? -1 : optionIndex === tabbableOptionIndex ? 0 : -1), [optionIndex, tabbableOptionIndex, isDisabled]);
16
16
  const handleFocus = useCallback(() => {
@@ -26,7 +26,7 @@ function SelectBoxOptionComponentInner({ option, isSelected, tabbableOptionIndex
26
26
  // Type-safe property access to satisfy eslint rules
27
27
  const optionValue = 'value' in option && option.value !== undefined ? String(option.value) : undefined;
28
28
  const optionLabel = option.label;
29
- return (_jsx(FocusIndicator, { position: "inside", children: _jsx("li", { role: "option", "data-value": optionValue, className: cx(classes.listItem, 'mfui-SelectBox__listItem'), "aria-selected": isSelected, "aria-disabled": isDisabled, tabIndex: tabIndex, onFocus: handleFocus, onKeyDown: onKeyDown, onClick: handleClick, children: renderOption ? (renderOption(option, isSelected)) : (_jsxs(_Fragment, { children: [_jsx(Current, { "aria-hidden": true }), _jsx(Typography, { variant: "body", children: optionLabel })] })) }) }));
29
+ return (_jsx(FocusIndicator, { position: "inside", children: _jsx("li", { role: "option", "data-value": optionValue, className: cx(classes.listItem, 'mfui-SelectBox__listItem'), "aria-selected": isSelected, "aria-disabled": isDisabled, "aria-describedby": groupLabelId, tabIndex: tabIndex, onFocus: handleFocus, onKeyDown: onKeyDown, onClick: handleClick, children: renderOption ? (renderOption(option, isSelected)) : (_jsxs(_Fragment, { children: [_jsx(Current, { "aria-hidden": true }), _jsx(Typography, { variant: "body", children: optionLabel })] })) }) }));
30
30
  }
31
31
  export const SelectBoxOptionComponent = memo(SelectBoxOptionComponentInner);
32
32
  SelectBoxOptionComponent.displayName = 'SelectBoxOptionComponent';
@@ -25,6 +25,15 @@ type SelectBoxOptionGroupProps<T extends string | number | undefined = string, A
25
25
  classes: {
26
26
  listItem: string;
27
27
  };
28
+ /**
29
+ * Id of the element that carries the label of this group, referenced by its options.
30
+ */
31
+ groupLabelId: string;
32
+ /**
33
+ * Whether that element is rendered outside the option list. When it is not, the label row below
34
+ * carries the id itself. See `OptionGroupLabelDescriptions`.
35
+ */
36
+ hasHiddenGroupLabel: boolean;
28
37
  /**
29
38
  * Custom render function for options.
30
39
  */
@@ -8,11 +8,14 @@ import { SelectBoxOptionComponent } from './SelectBoxOption';
8
8
  * Renders a group label followed by its nested options.
9
9
  * Memoized to prevent unnecessary re-renders.
10
10
  */
11
- function SelectBoxOptionGroupInner({ groupOption, selectedOption, valueToIndexMap, tabbableOptionIndex, classes, renderOption, onOptionFocus, onOptionClick, onOptionKeyDown, findFlatOptionIndexByValue, }) {
11
+ function SelectBoxOptionGroupInner({ groupOption, selectedOption, valueToIndexMap, tabbableOptionIndex, classes, groupLabelId, hasHiddenGroupLabel, renderOption, onOptionFocus, onOptionClick, onOptionKeyDown, findFlatOptionIndexByValue, }) {
12
12
  // Type-safe property access to satisfy eslint rules
13
13
  const groupLabel = groupOption.label;
14
14
  const nestedOptions = groupOption.options;
15
- return (_jsxs(_Fragment, { children: [_jsx("li", { className: classes.listItem, "data-mfui-group-label": "true", "aria-hidden": "true", children: _jsx(Typography, { variant: "label", children: groupLabel }) }), nestedOptions.map((nestedOption, index) => {
15
+ // See `OptionGroupLabelDescriptions` for why this row is exposed to assistive technology, why
16
+ // `role="presentation"` replaces the `aria-hidden` it used to carry, and when the id the options
17
+ // reference lives on a separate element instead of on this row.
18
+ return (_jsxs(_Fragment, { children: [_jsx("li", { id: hasHiddenGroupLabel ? undefined : groupLabelId, role: "presentation", className: classes.listItem, "data-mfui-group-label": "true", children: _jsx(Typography, { variant: "label", children: groupLabel }) }), nestedOptions.map((nestedOption, index) => {
16
19
  if (!nestedOption)
17
20
  return null;
18
21
  const flatOptionIndex = findFlatOptionIndexByValue(valueToIndexMap, nestedOption);
@@ -20,7 +23,7 @@ function SelectBoxOptionGroupInner({ groupOption, selectedOption, valueToIndexMa
20
23
  const optionLabel = nestedOption.label;
21
24
  const keyValue = optionValue !== undefined ? String(optionValue) : `${optionLabel}-${String(index)}`;
22
25
  const isSelected = selectedOption && 'value' in selectedOption ? selectedOption.value === optionValue : false;
23
- return (_jsx(SelectBoxOptionComponent, { option: nestedOption, isSelected: isSelected, tabbableOptionIndex: tabbableOptionIndex, optionIndex: flatOptionIndex, classes: classes, renderOption: renderOption, onFocus: onOptionFocus, onClick: onOptionClick, onKeyDown: onOptionKeyDown(nestedOption) }, keyValue));
26
+ return (_jsx(SelectBoxOptionComponent, { option: nestedOption, isSelected: isSelected, tabbableOptionIndex: tabbableOptionIndex, optionIndex: flatOptionIndex, classes: classes, groupLabelId: groupLabelId, renderOption: renderOption, onFocus: onOptionFocus, onClick: onOptionClick, onKeyDown: onOptionKeyDown(nestedOption) }, keyValue));
24
27
  })] }));
25
28
  }
26
29
  export const SelectBoxOptionGroup = memo(SelectBoxOptionGroupInner);
@@ -1,7 +1,7 @@
1
1
  import { useVirtualizer } from '@tanstack/react-virtual';
2
2
  import { type RefObject } from 'react';
3
- import { type BasedAdditionalProps, type FlatOption, type VirtualOptionTypes } from '../SelectBox.types';
4
- export type UseVirtualizedOptionsParams<T extends string | number | undefined = string, AdditionalProps extends BasedAdditionalProps = BasedAdditionalProps> = {
3
+ import { type VirtualOptionTypes } from '../SelectBox.types';
4
+ export type UseVirtualizedOptionsParams = {
5
5
  /**
6
6
  * Enable virtualization for the option list
7
7
  */
@@ -11,9 +11,10 @@ export type UseVirtualizedOptionsParams<T extends string | number | undefined =
11
11
  */
12
12
  parentRef: RefObject<HTMLElement | null>;
13
13
  /**
14
- * Flat array of all options
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
- flatOptions: FlatOption<T, AdditionalProps>[];
17
+ rowCount: number;
17
18
  } & Partial<VirtualOptionTypes>;
18
19
  export type UseVirtualizedOptionsReturn = {
19
20
  /**
@@ -54,10 +55,10 @@ export type UseVirtualizedOptionsReturn = {
54
55
  * const { virtualItems, totalSize, scrollToIndex, isVirtualized } = useVirtualizedOptions({
55
56
  * enabled: true,
56
57
  * parentRef: optionPanelRef,
57
- * flatOptions,
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 useVirtualizedOptions<T extends string | number | undefined = string, AdditionalProps extends BasedAdditionalProps = BasedAdditionalProps>({ enabled, parentRef, flatOptions, estimateSize, overscan, }: UseVirtualizedOptionsParams<T, AdditionalProps>): UseVirtualizedOptionsReturn;
64
+ export declare function useVirtualizedOptions({ enabled, parentRef, rowCount, estimateSize, overscan, }: UseVirtualizedOptionsParams): UseVirtualizedOptionsReturn;
@@ -13,16 +13,16 @@ import { useCallback } from 'react';
13
13
  * const { virtualItems, totalSize, scrollToIndex, isVirtualized } = useVirtualizedOptions({
14
14
  * enabled: true,
15
15
  * parentRef: optionPanelRef,
16
- * flatOptions,
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 useVirtualizedOptions({ enabled, parentRef, flatOptions, estimateSize = () => 36, overscan = 5, }) {
22
+ export function useVirtualizedOptions({ enabled, parentRef, rowCount, estimateSize = () => 36, overscan = 5, }) {
23
23
  // Always call useVirtualizer to maintain hook order, but with 0 count when disabled
24
24
  const virtualizer = useVirtualizer({
25
- count: enabled ? flatOptions.length : 0,
25
+ count: enabled ? rowCount : 0,
26
26
  getScrollElement: () =>
27
27
  // parentRef points to scrollWrapper, which is always the scroll container
28
28
  // Structure: Popover root > optionPanel > scrollWrapper (parentRef) > listBox
@@ -0,0 +1,37 @@
1
+ type OptionGroupLabelDescriptionsProps = {
2
+ /**
3
+ * The option groups of the currently rendered option list, in the order they are rendered.
4
+ */
5
+ groups: {
6
+ id: string;
7
+ label: string;
8
+ }[];
9
+ };
10
+ /**
11
+ * Renders the option group labels of a virtualized listbox as hidden elements that the options of each
12
+ * group reference with `aria-describedby`, so that assistive technology can tell which group an option
13
+ * belongs to.
14
+ *
15
+ * The visible group label is a row of the option list, and a virtualized option list unmounts the rows
16
+ * that are scrolled out of view. Referencing the visible row directly would therefore leave the options
17
+ * of a long group pointing at an element that is no longer in the document. These elements live outside
18
+ * the option list instead, so the reference always resolves. A hidden element that is referenced directly
19
+ * by `aria-describedby` still contributes its text to the accessible description, which is what makes
20
+ * this work.
21
+ *
22
+ * A non-virtualized option list renders every row, so there the visible label row carries the id itself
23
+ * and this component renders nothing. Exactly one element carries a given group label id either way.
24
+ *
25
+ * These elements are hidden with `hidden`, not visually hidden like the closed `Tooltip` content is. A
26
+ * visually hidden element stays in the accessibility tree, and unlike a closed tooltip the visible group
27
+ * label row is still rendered next to it, so the group label would be announced twice when reading
28
+ * through the option list. `hidden` keeps the label out of the accessibility tree while the accessible
29
+ * description computation still reads it, because it is referenced directly.
30
+ *
31
+ * The visible label row keeps `role="presentation"` and is not hidden from assistive technology in both
32
+ * cases, so the heading itself is still announced when navigating through the option list.
33
+ *
34
+ * @see https://www.w3.org/TR/accname-1.2/#comp_labelledby
35
+ */
36
+ export declare const OptionGroupLabelDescriptions: ({ groups }: OptionGroupLabelDescriptionsProps) => import("react/jsx-runtime").JSX.Element | null;
37
+ export {};
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Renders the option group labels of a virtualized listbox as hidden elements that the options of each
4
+ * group reference with `aria-describedby`, so that assistive technology can tell which group an option
5
+ * belongs to.
6
+ *
7
+ * The visible group label is a row of the option list, and a virtualized option list unmounts the rows
8
+ * that are scrolled out of view. Referencing the visible row directly would therefore leave the options
9
+ * of a long group pointing at an element that is no longer in the document. These elements live outside
10
+ * the option list instead, so the reference always resolves. A hidden element that is referenced directly
11
+ * by `aria-describedby` still contributes its text to the accessible description, which is what makes
12
+ * this work.
13
+ *
14
+ * A non-virtualized option list renders every row, so there the visible label row carries the id itself
15
+ * and this component renders nothing. Exactly one element carries a given group label id either way.
16
+ *
17
+ * These elements are hidden with `hidden`, not visually hidden like the closed `Tooltip` content is. A
18
+ * visually hidden element stays in the accessibility tree, and unlike a closed tooltip the visible group
19
+ * label row is still rendered next to it, so the group label would be announced twice when reading
20
+ * through the option list. `hidden` keeps the label out of the accessibility tree while the accessible
21
+ * description computation still reads it, because it is referenced directly.
22
+ *
23
+ * The visible label row keeps `role="presentation"` and is not hidden from assistive technology in both
24
+ * cases, so the heading itself is still announced when navigating through the option list.
25
+ *
26
+ * @see https://www.w3.org/TR/accname-1.2/#comp_labelledby
27
+ */
28
+ export const OptionGroupLabelDescriptions = ({ groups }) => {
29
+ if (groups.length === 0)
30
+ return null;
31
+ return (_jsx("div", { hidden: true, children: groups.map(({ id, label }) => (_jsx("span", { id: id, children: label }, id))) }));
32
+ };
@@ -0,0 +1 @@
1
+ export * from './OptionGroupLabelDescriptions';
@@ -0,0 +1 @@
1
+ export * from './OptionGroupLabelDescriptions';
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Build the id of the element that carries the label of an option group of a listbox.
3
+ *
4
+ * The id is derived from the listbox id and the position of the group, so it stays the same across
5
+ * re-renders. It does not identify the visible group label row, which a virtualized option list
6
+ * unmounts once it is scrolled out of view, but the hidden element rendered outside the option list
7
+ * by `OptionGroupLabelDescriptions` - see that component for why the two are separate.
8
+ *
9
+ * @param listBoxId - The id of the listbox that owns the option group.
10
+ *
11
+ * @param groupIndex - The index of the option group in the rendered option list.
12
+ *
13
+ * @returns The id of the element the options of the group reference with `aria-describedby`.
14
+ */
15
+ export declare const getGroupLabelId: (listBoxId: string, groupIndex: number) => string;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Build the id of the element that carries the label of an option group of a listbox.
3
+ *
4
+ * The id is derived from the listbox id and the position of the group, so it stays the same across
5
+ * re-renders. It does not identify the visible group label row, which a virtualized option list
6
+ * unmounts once it is scrolled out of view, but the hidden element rendered outside the option list
7
+ * by `OptionGroupLabelDescriptions` - see that component for why the two are separate.
8
+ *
9
+ * @param listBoxId - The id of the listbox that owns the option group.
10
+ *
11
+ * @param groupIndex - The index of the option group in the rendered option list.
12
+ *
13
+ * @returns The id of the element the options of the group reference with `aria-describedby`.
14
+ */
15
+ export const getGroupLabelId = (listBoxId, groupIndex) => `${listBoxId}-group-${String(groupIndex)}`;
@@ -11,7 +11,7 @@ type BaseMainNavigationSlotRecipeVariantMap = {
11
11
  [key in keyof BaseMainNavigationSlotRecipeVariant]: Array<BaseMainNavigationSlotRecipeVariant[key]>
12
12
  }
13
13
 
14
- type BaseMainNavigationSlotRecipeSlot = "root" | "nav" | "list" | "featureShortcut" | "featureShortcutAnchor" | "labelGroup" | "iconAndLabel" | "lockIcon" | "listItem" | "listItemTooltip" | "listItemAnchor" | "listItemAnchorIcon" | "parentDetails" | "parentSummary" | "parentSummaryIcon" | "groupMenuList" | "groupLabelSection" | "groupLabel" | "groupLabelText" | "childrenList" | "childrenListItem" | "childrenListItemAnchor" | "popoverChildrenList" | "footer" | "toggleButton" | "toggleButtonIcon"
14
+ type BaseMainNavigationSlotRecipeSlot = "root" | "nav" | "list" | "featureShortcut" | "featureShortcutAnchor" | "labelGroup" | "iconAndLabel" | "lockIcon" | "listItem" | "listItemTooltip" | "listItemAnchor" | "listItemAnchorIcon" | "parentDetails" | "parentSummary" | "parentSummaryIcon" | "groupMenuList" | "groupLabelSection" | "groupLabel" | "groupLabelText" | "childrenList" | "childrenListItem" | "childrenListItemAnchor" | "popoverChildrenList" | "popoverTrigger" | "popoverTriggerDisclosureIcon" | "footer" | "toggleButton" | "toggleButtonIcon"
15
15
 
16
16
  export type BaseMainNavigationSlotRecipeVariantProps = {
17
17
  [key in keyof BaseMainNavigationSlotRecipeVariant]?: ConditionalValue<BaseMainNavigationSlotRecipeVariant[key]> | undefined
@@ -95,6 +95,14 @@ const baseMainNavigationSlotRecipeSlotNames = [
95
95
  "popoverChildrenList",
96
96
  "BaseMainNavigation__popoverChildrenList"
97
97
  ],
98
+ [
99
+ "popoverTrigger",
100
+ "BaseMainNavigation__popoverTrigger"
101
+ ],
102
+ [
103
+ "popoverTriggerDisclosureIcon",
104
+ "BaseMainNavigation__popoverTriggerDisclosureIcon"
105
+ ],
98
106
  [
99
107
  "footer",
100
108
  "BaseMainNavigation__footer"
@@ -10,6 +10,7 @@ fixedColumn: boolean
10
10
  verticalBorder: boolean
11
11
  edgeFixedColumn: "left" | "right"
12
12
  layout: "default" | "edge-to-edge"
13
+ isRowHeader: boolean
13
14
  type: "text" | "amount"
14
15
  }
15
16
 
@@ -55,6 +55,7 @@ const dataTableCellSlotRecipeVariantKeys = [
55
55
  "verticalBorder",
56
56
  "edgeFixedColumn",
57
57
  "layout",
58
+ "isRowHeader",
58
59
  "type"
59
60
  ];
60
61
  const getVariantProps = (variants) => ({ ...dataTableCellSlotRecipeDefaultVariants, ...compact(variants) });
@@ -89,6 +90,9 @@ export const dataTableCellSlotRecipe = /* @__PURE__ */ Object.assign(dataTableCe
89
90
  "default",
90
91
  "edge-to-edge"
91
92
  ],
93
+ "isRowHeader": [
94
+ "true"
95
+ ],
92
96
  "type": [
93
97
  "text",
94
98
  "amount"
package/dist/styles.css CHANGED
@@ -2015,6 +2015,16 @@
2015
2015
  padding-left: var(--mfui-spacing-mfui\.size\.padding\.container\.horizontal\.comfort);
2016
2016
  }
2017
2017
 
2018
+ .mfui-dqtPcW {
2019
+ width: 100%;
2020
+ }
2021
+
2022
+ .mfui-datOaS {
2023
+ flex-shrink: 0;
2024
+ width: var(--mfui-sizes-mfui\.size\.dimension\.icon\.square\.comfort);
2025
+ height: 24px;
2026
+ }
2027
+
2018
2028
  .mfui-jlehrW {
2019
2029
  padding-inline: var(--mfui-spacing-mfui\.size\.padding\.container\.horizontal\.comfort);
2020
2030
  border-block-start: 1px solid;
@@ -7605,6 +7615,11 @@ li:last-child > .mfui-cXGJls {
7605
7615
  border-left-color: var(--mfui-colors-mfui\.color\.neutral\.sub-border\.none);
7606
7616
  }
7607
7617
 
7618
+ .mfui-VBccL {
7619
+ font-weight: inherit;
7620
+ text-align: inherit;
7621
+ }
7622
+
7608
7623
  .mfui-cyVYhU,.mfui-ePuaEb {
7609
7624
  justify-content: flex-start;
7610
7625
  text-align: left;