@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
|
@@ -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
|
-
|
|
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
|
-
|
|
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",
|
|
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(() =>
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
} }
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
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
|
|
4
|
-
export type UseVirtualizedOptionsParams
|
|
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
|
-
*
|
|
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 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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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,
|
|
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 ?
|
|
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"
|
|
@@ -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;
|