@moneyforward/mfui-components 4.7.0 → 4.8.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 (34) hide show
  1. package/dist/src/DataTable/DataTable.js +38 -5
  2. package/dist/src/DataTable/DataTable.types.d.ts +101 -0
  3. package/dist/src/DataTable/DataTableHeaderCell/DataTableHeaderCell.js +5 -2
  4. package/dist/src/DataTable/DataTableHeaderRow/DataTableHeaderRow.js +31 -15
  5. package/dist/src/DataTable/DataTableProvider.d.ts +19 -4
  6. package/dist/src/DataTable/DataTableProvider.js +11 -3
  7. package/dist/src/DataTable/DataTableRow/DataTableRow.js +21 -12
  8. package/dist/src/DataTable/hooks/useVirtualizedColumns.d.ts +73 -0
  9. package/dist/src/DataTable/hooks/useVirtualizedColumns.js +109 -0
  10. package/dist/src/DataTable/index.d.ts +1 -1
  11. package/dist/src/DataTable/shared/commonStyles.d.ts +11 -0
  12. package/dist/src/DataTable/shared/commonStyles.js +11 -0
  13. package/dist/src/DataTable/utils/renderVirtualizedColumns.d.ts +42 -0
  14. package/dist/src/DataTable/utils/renderVirtualizedColumns.js +40 -0
  15. package/dist/src/DataTable/utils/resolveColumnWidths.d.ts +28 -0
  16. package/dist/src/DataTable/utils/resolveColumnWidths.js +41 -0
  17. package/dist/src/MultipleSelectBox/MultipleSelectBox.d.ts +1 -1
  18. package/dist/src/MultipleSelectBox/MultipleSelectBox.js +8 -5
  19. package/dist/src/MultipleSelectBox/MultipleSelectBox.types.d.ts +33 -0
  20. package/dist/src/SelectBox/SelectBox.js +6 -4
  21. package/dist/src/SelectBox/SelectBox.types.d.ts +33 -0
  22. package/dist/src/SelectBox/SelectBoxOption.d.ts +5 -1
  23. package/dist/src/SelectBox/SelectBoxOption.js +3 -4
  24. package/dist/src/SelectBox/SelectBoxOptionGroup.d.ts +5 -1
  25. package/dist/src/SelectBox/SelectBoxOptionGroup.js +2 -2
  26. package/dist/src/shared/OptionLabel/OptionLabel.d.ts +45 -0
  27. package/dist/src/shared/OptionLabel/OptionLabel.js +9 -0
  28. package/dist/src/shared/OptionLabel/index.d.ts +1 -0
  29. package/dist/src/shared/OptionLabel/index.js +1 -0
  30. package/dist/styled-system/recipes/data-table-slot-recipe.d.ts +5 -1
  31. package/dist/styled-system/recipes/data-table-slot-recipe.js +19 -1
  32. package/dist/styles.css +31 -0
  33. package/dist/tsconfig.build.tsbuildinfo +1 -1
  34. package/package.json +7 -7
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The width of the selection checkbox column.
3
+ *
4
+ * The checkbox cell is always the table's start-edge cell, so its border-box has to hold the
5
+ * start-edge padding from `_first` as well as the inner cell's padding around the checkbox.
6
+ * Otherwise the checkbox overflows into the next column under `table-layout: fixed`.
7
+ *
8
+ * The header cell, the body cell and the `<col>` that `DataTable` renders for column virtualization
9
+ * all take the width from here, so that they cannot disagree.
10
+ */
11
+ export const SELECTION_COLUMN_WIDTH = '[calc({sizes.mfui.size.dimension.icon.square.comfort} + {spacing.mfui.size.padding.data-table-cell.horizontal.comfort} * 2 + {spacing.mfui.size.padding.container.horizontal.comfort})]';
@@ -0,0 +1,42 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type DataTableColumnWindow } from '../hooks/useVirtualizedColumns';
3
+ type RenderVirtualizedColumnsParams = {
4
+ /**
5
+ * The cells of the row, flattened, without the selection checkbox cell the row renders itself.
6
+ */
7
+ cells: ReactNode[];
8
+ /**
9
+ * The grid column of the first cell, which is 1 when the row renders a selection checkbox cell.
10
+ */
11
+ columnOffset: number;
12
+ /**
13
+ * The columns to mount, or `null` to mount every cell.
14
+ */
15
+ columnWindow: DataTableColumnWindow | null;
16
+ /**
17
+ * The class name of the spacer cells.
18
+ */
19
+ spacerClassName: string;
20
+ /**
21
+ * Renders a mounted cell.
22
+ *
23
+ * @param cell - The cell to render.
24
+ * @param columnIndex - The grid column of the cell.
25
+ */
26
+ renderCell: (cell: ReactNode, columnIndex: number) => ReactNode;
27
+ };
28
+ /**
29
+ * Applies the column window to the cells of a row. The header row and the rows share it, so that
30
+ * they cannot disagree about which columns are mounted.
31
+ *
32
+ * Each gap left by the columns that are not mounted is filled with a single spacer cell spanning
33
+ * them. Under `table-layout: fixed` the spacer takes exactly the width of the `<col>` elements it
34
+ * spans, so the mounted cells stay in their columns and the sticky offsets of the fixed columns,
35
+ * which are summed over the cells of the row, stay what they are without virtualization.
36
+ *
37
+ * @param params - The cells of the row and the window to apply.
38
+ *
39
+ * @returns The cells to mount, with the spacer cells in place of the columns that are not mounted.
40
+ */
41
+ export declare function renderVirtualizedColumns({ cells, columnOffset, columnWindow, spacerClassName, renderCell, }: RenderVirtualizedColumnsParams): ReactNode[];
42
+ export {};
@@ -0,0 +1,40 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Applies the column window to the cells of a row. The header row and the rows share it, so that
4
+ * they cannot disagree about which columns are mounted.
5
+ *
6
+ * Each gap left by the columns that are not mounted is filled with a single spacer cell spanning
7
+ * them. Under `table-layout: fixed` the spacer takes exactly the width of the `<col>` elements it
8
+ * spans, so the mounted cells stay in their columns and the sticky offsets of the fixed columns,
9
+ * which are summed over the cells of the row, stay what they are without virtualization.
10
+ *
11
+ * @param params - The cells of the row and the window to apply.
12
+ *
13
+ * @returns The cells to mount, with the spacer cells in place of the columns that are not mounted.
14
+ */
15
+ export function renderVirtualizedColumns({ cells, columnOffset, columnWindow, spacerClassName, renderCell, }) {
16
+ if (columnWindow === null) {
17
+ return cells.map((cell, index) => renderCell(cell, index + columnOffset));
18
+ }
19
+ const rowColumnCount = cells.length + columnOffset;
20
+ // The bounds are clamped to the row, so a row with fewer cells than the header still lays out.
21
+ const suffixStart = Math.min(columnWindow.suffixStart ?? rowColumnCount, rowColumnCount);
22
+ const prefixEnd = Math.min(columnWindow.prefixEnd, suffixStart);
23
+ const windowStart = Math.min(Math.max(columnWindow.windowStart, prefixEnd), suffixStart);
24
+ const windowEnd = Math.min(Math.max(columnWindow.windowEnd, windowStart), suffixStart);
25
+ const renderRange = (start, end) => cells
26
+ .slice(Math.max(start - columnOffset, 0), Math.max(end - columnOffset, 0))
27
+ .map((cell, index) => renderCell(cell, Math.max(start, columnOffset) + index));
28
+ // The spacer stands in for the columns that are not mounted, so it reserves their width without
29
+ // being announced as a cell of the table.
30
+ const renderSpacer = (position, colSpan) => (_jsx("td", {
31
+ // oxlint-disable-next-line jsx-a11y/no-interactive-element-to-noninteractive-role -- A `td` is not interactive; the role only keeps this empty layout cell from being announced as a cell.
32
+ role: "presentation", colSpan: colSpan, className: spacerClassName, "data-mfui-virtual-spacer": position }, `mfui-virtual-column-spacer-${position}`));
33
+ return [
34
+ ...renderRange(0, prefixEnd),
35
+ windowStart > prefixEnd ? renderSpacer('start', windowStart - prefixEnd) : null,
36
+ ...renderRange(windowStart, windowEnd),
37
+ suffixStart > windowEnd ? renderSpacer('end', suffixStart - windowEnd) : null,
38
+ ...renderRange(suffixStart, rowColumnCount),
39
+ ];
40
+ }
@@ -0,0 +1,28 @@
1
+ import { type CSSProperties } from 'react';
2
+ import { type DataTableColumnVirtualizationOptions } from '../DataTable.types';
3
+ /**
4
+ * The width of a column in pixels when neither `getColumnWidth` nor its header cell declares one.
5
+ */
6
+ export declare const DEFAULT_COLUMN_WIDTH = 160;
7
+ /**
8
+ * The `style.width` of a header cell, which the header row reports for each column.
9
+ */
10
+ export type DeclaredColumnWidth = CSSProperties['width'];
11
+ /**
12
+ * Resolves the width of every consumer column, in pixels, for column virtualization.
13
+ *
14
+ * Each column takes the first of:
15
+ *
16
+ * 1. `getColumnWidth(index)`, when it returns a width.
17
+ * 2. The `style.width` of the column's `DataTable.HeaderCell`, when it is a number or a `px` string.
18
+ * 3. {@link DEFAULT_COLUMN_WIDTH}.
19
+ *
20
+ * @param declaredColumnWidths - The `style.width` of each header cell, which also gives the number
21
+ * of consumer columns.
22
+ *
23
+ * @param getColumnWidth - The consumer's width callback, which receives the column index counted as
24
+ * `leftFixedColumnIndex` is, without the selection column.
25
+ *
26
+ * @returns The width of each consumer column in pixels.
27
+ */
28
+ export declare function resolveColumnWidths(declaredColumnWidths: readonly DeclaredColumnWidth[], getColumnWidth: DataTableColumnVirtualizationOptions['getColumnWidth']): number[];
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The width of a column in pixels when neither `getColumnWidth` nor its header cell declares one.
3
+ */
4
+ export const DEFAULT_COLUMN_WIDTH = 160;
5
+ /**
6
+ * Converts the `style.width` of a header cell into pixels.
7
+ * Only a number or a `px` string is understood, because the column virtualizer needs the width in
8
+ * pixels before anything is laid out. Any other value counts as not declared.
9
+ *
10
+ * @param width - The declared width.
11
+ *
12
+ * @returns The width in pixels, or `undefined` when it cannot be resolved without layout.
13
+ */
14
+ function toPixels(width) {
15
+ if (typeof width === 'number')
16
+ return Number.isFinite(width) && width > 0 ? width : undefined;
17
+ if (typeof width !== 'string')
18
+ return undefined;
19
+ const match = /^\s*(\d+(?:\.\d+)?)px\s*$/u.exec(width);
20
+ return match?.[1] === undefined ? undefined : Number(match[1]);
21
+ }
22
+ /**
23
+ * Resolves the width of every consumer column, in pixels, for column virtualization.
24
+ *
25
+ * Each column takes the first of:
26
+ *
27
+ * 1. `getColumnWidth(index)`, when it returns a width.
28
+ * 2. The `style.width` of the column's `DataTable.HeaderCell`, when it is a number or a `px` string.
29
+ * 3. {@link DEFAULT_COLUMN_WIDTH}.
30
+ *
31
+ * @param declaredColumnWidths - The `style.width` of each header cell, which also gives the number
32
+ * of consumer columns.
33
+ *
34
+ * @param getColumnWidth - The consumer's width callback, which receives the column index counted as
35
+ * `leftFixedColumnIndex` is, without the selection column.
36
+ *
37
+ * @returns The width of each consumer column in pixels.
38
+ */
39
+ export function resolveColumnWidths(declaredColumnWidths, getColumnWidth) {
40
+ return declaredColumnWidths.map((declaredWidth, index) => getColumnWidth?.(index) ?? toPixels(declaredWidth) ?? DEFAULT_COLUMN_WIDTH);
41
+ }
@@ -4,4 +4,4 @@ import { type MultipleSelectBoxProps, type AllowedValueTypes } from './MultipleS
4
4
  * This component is separated from the SelectBox component because it has a different behavior.
5
5
  * This component switches the variants of looks and behaviors depends on the props: size, invalid, disabled.
6
6
  */
7
- export declare function MultipleSelectBox<T extends AllowedValueTypes = string, AdditionalProps extends Record<string, unknown> = Record<string, never>>({ id, triggerProps, triggerWrapperProps, size, options, defaultValue, placeholder, emptyMessage, disabled, invalid, required, caution, targetDOMNode, name, onChange, value, showDisplayValueAsTag, renderDisplayValue, enableSearchOptions, notFoundMessage, searchBoxProps, loading, onSearchOptions, clearButtonProps, disableClearButton, optionPanelProps, popoverWrapperProps, enableAllOptionsControls, enableApplyControls, showSelectedCount, selectAllButtonProps, clearAllButtonProps, selectedCountProps, cancelButtonProps, applyButtonProps, renderOption, renderPopoverHeader, renderOptionList, renderPopoverFooter, onOpenStateChanged, enableAutoUnmount, onBlur, showGroupOptionDivider, enableVirtualization, virtualizationOptions, infiniteScroll, enableCreateOption, onCreateOption, createOptionProps, }: MultipleSelectBoxProps<T, AdditionalProps>): import("react/jsx-runtime").JSX.Element;
7
+ export declare function MultipleSelectBox<T extends AllowedValueTypes = string, AdditionalProps extends Record<string, unknown> = Record<string, never>>({ id, triggerProps, triggerWrapperProps, size, options, defaultValue, placeholder, emptyMessage, disabled, invalid, required, caution, targetDOMNode, name, onChange, value, showDisplayValueAsTag, renderDisplayValue, enableSearchOptions, notFoundMessage, searchBoxProps, loading, onSearchOptions, clearButtonProps, disableClearButton, optionPanelProps, popoverWrapperProps, enableAllOptionsControls, enableApplyControls, showSelectedCount, selectAllButtonProps, clearAllButtonProps, selectedCountProps, cancelButtonProps, applyButtonProps, renderOption, renderOptionLabelSlot, renderPopoverHeader, renderOptionList, renderPopoverFooter, onOpenStateChanged, enableAutoUnmount, onBlur, showGroupOptionDivider, enableVirtualization, virtualizationOptions, infiniteScroll, enableCreateOption, onCreateOption, createOptionProps, }: MultipleSelectBoxProps<T, AdditionalProps>): import("react/jsx-runtime").JSX.Element;
@@ -26,6 +26,7 @@ import { cx } from '../../styled-system/css';
26
26
  import { flattenOptions } from './utils/flattenOptions';
27
27
  import { getGroupLabelId } from '../utilities/dom/getGroupLabelId';
28
28
  import { OptionGroupLabelDescriptions } from '../shared/OptionGroupLabelDescriptions';
29
+ import { OptionLabel } from '../shared/OptionLabel';
29
30
  import { isSelectableOption, isOptionDisabled, isOptionSelected } from './utils/isSelectableOption';
30
31
  import { useVirtualizedMultipleSelectBoxOptions } from './hooks/useVirtualizedMultipleSelectBoxOptions';
31
32
  import { useInfiniteScroll } from '../utilities/dom/useInfiniteScroll';
@@ -41,7 +42,7 @@ const EMPTY_OPTIONS = [];
41
42
  */
42
43
  export function MultipleSelectBox({ id, triggerProps, triggerWrapperProps, size, options = EMPTY_OPTIONS, defaultValue,
43
44
  // eslint-disable-next-line @typescript-eslint/no-deprecated
44
- placeholder, emptyMessage, disabled, invalid, required, caution, targetDOMNode, name, onChange, value, showDisplayValueAsTag, renderDisplayValue, enableSearchOptions = false, notFoundMessage, searchBoxProps, loading = false, onSearchOptions, clearButtonProps, disableClearButton = false, optionPanelProps, popoverWrapperProps, enableAllOptionsControls = false, enableApplyControls = false, showSelectedCount = false, selectAllButtonProps, clearAllButtonProps, selectedCountProps, cancelButtonProps, applyButtonProps, renderOption, renderPopoverHeader, renderOptionList, renderPopoverFooter, onOpenStateChanged, enableAutoUnmount = true, onBlur, showGroupOptionDivider, enableVirtualization = false, virtualizationOptions, infiniteScroll, enableCreateOption = false, onCreateOption, createOptionProps, }) {
45
+ placeholder, emptyMessage, disabled, invalid, required, caution, targetDOMNode, name, onChange, value, showDisplayValueAsTag, renderDisplayValue, enableSearchOptions = false, notFoundMessage, searchBoxProps, loading = false, onSearchOptions, clearButtonProps, disableClearButton = false, optionPanelProps, popoverWrapperProps, enableAllOptionsControls = false, enableApplyControls = false, showSelectedCount = false, selectAllButtonProps, clearAllButtonProps, selectedCountProps, cancelButtonProps, applyButtonProps, renderOption, renderOptionLabelSlot, renderPopoverHeader, renderOptionList, renderPopoverFooter, onOpenStateChanged, enableAutoUnmount = true, onBlur, showGroupOptionDivider, enableVirtualization = false, virtualizationOptions, infiniteScroll, enableCreateOption = false, onCreateOption, createOptionProps, }) {
45
46
  const classes = multipleSelectBoxSlotRecipe({ showGroupOptionDivider });
46
47
  const triggerRef = useRef(null);
47
48
  const listBoxId = useId();
@@ -214,7 +215,7 @@ placeholder, emptyMessage, disabled, invalid, required, caution, targetDOMNode,
214
215
  }, 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));
215
216
  }
216
217
  if (isSelectableOption(option) && option.value != null) {
217
- const { value, label } = option;
218
+ const { value } = option;
218
219
  const isDisabled = isOptionDisabled(option);
219
220
  const selectedSet = enableApplyControls ? temporaryValues : localSelectedValuesSet;
220
221
  const isSelected = isOptionSelected(option, selectedSet);
@@ -241,7 +242,7 @@ placeholder, emptyMessage, disabled, invalid, required, caution, targetDOMNode,
241
242
  return;
242
243
  }
243
244
  handleSelectOption(option);
244
- }, children: renderOption ? (renderOption(option, isSelected)) : (_jsxs(_Fragment, { children: [isSelected ? _jsx(CheckboxChecked, { "aria-hidden": "true" }) : _jsx(CheckboxUnchecked, { "aria-hidden": "true" }), _jsx(Typography, { variant: "body", children: label })] })) }) }) }, virtualItem.key));
245
+ }, children: renderOption ? (renderOption(option, isSelected)) : (_jsxs(_Fragment, { children: [isSelected ? _jsx(CheckboxChecked, { "aria-hidden": "true" }) : _jsx(CheckboxUnchecked, { "aria-hidden": "true" }), _jsx(OptionLabel, { option: option, isSelected: isSelected, renderOptionLabelSlot: renderOptionLabelSlot, slotContentName: "multiple-select-box-option-label-slot" })] })) }) }) }, virtualItem.key));
245
246
  }
246
247
  return null;
247
248
  });
@@ -254,6 +255,7 @@ placeholder, emptyMessage, disabled, invalid, required, caution, targetDOMNode,
254
255
  classes,
255
256
  handleSelectOption,
256
257
  renderOption,
258
+ renderOptionLabelSlot,
257
259
  createKeyDownHandler,
258
260
  handleOptionFocus,
259
261
  tabbableOptionIndex,
@@ -274,7 +276,7 @@ placeholder, emptyMessage, disabled, invalid, required, caution, targetDOMNode,
274
276
  return null;
275
277
  }
276
278
  // Only reaches here if isSelectableOption(option) is true and value is not null/undefined
277
- const { value, label } = option;
279
+ const { value } = option;
278
280
  const isDisabled = isOptionDisabled(option);
279
281
  const selectedSet = enableApplyControls ? temporaryValues : localSelectedValuesSet;
280
282
  const isSelected = isOptionSelected(option, selectedSet);
@@ -292,7 +294,7 @@ placeholder, emptyMessage, disabled, invalid, required, caution, targetDOMNode,
292
294
  return;
293
295
  }
294
296
  handleSelectOption(option);
295
- }, children: renderOption ? (renderOption(option, isSelected)) : (_jsxs(_Fragment, { children: [isSelected ? _jsx(CheckboxChecked, { "aria-hidden": "true" }) : _jsx(CheckboxUnchecked, { "aria-hidden": "true" }), _jsx(Typography, { variant: "body", children: label })] })) }) }, String(value)));
297
+ }, children: renderOption ? (renderOption(option, isSelected)) : (_jsxs(_Fragment, { children: [isSelected ? _jsx(CheckboxChecked, { "aria-hidden": "true" }) : _jsx(CheckboxUnchecked, { "aria-hidden": "true" }), _jsx(OptionLabel, { option: option, isSelected: isSelected, renderOptionLabelSlot: renderOptionLabelSlot, slotContentName: "multiple-select-box-option-label-slot" })] })) }) }, String(value)));
296
298
  }
297
299
  // Only render group label if truly a group label (no value property at all)
298
300
  const { label } = option;
@@ -307,6 +309,7 @@ placeholder, emptyMessage, disabled, invalid, required, caution, targetDOMNode,
307
309
  classes,
308
310
  handleSelectOption,
309
311
  renderOption,
312
+ renderOptionLabelSlot,
310
313
  createKeyDownHandler,
311
314
  handleOptionFocus,
312
315
  tabbableOptionIndex,
@@ -4,6 +4,7 @@ import { type SearchBoxProps } from '../SearchBox';
4
4
  import { type MultipleSelectBoxTriggerProps, type PassedProps } from './MultipleSelectBoxTrigger/MultipleSelectBoxTrigger.types';
5
5
  import { type PopoverProps } from '../Popover';
6
6
  import { type InfiniteScrollDirection } from '../utilities/dom/useInfiniteScroll';
7
+ import { type RenderOptionLabelSlot } from '../shared/OptionLabel';
7
8
  export type AllowedValueTypes = string | number | undefined;
8
9
  export type VirtualOptionTypes = Pick<VirtualizerOptions<HTMLElement, Element>, 'estimateSize' | 'overscan'>;
9
10
  export type InfiniteScrollConfig = {
@@ -313,6 +314,38 @@ export type MultipleSelectBoxProps<T extends AllowedValueTypes = string, Additio
313
314
  * ```
314
315
  */
315
316
  renderOption?: (option: MultipleSelectBoxOption<T, AdditionalProps>, isSelected: boolean) => React.ReactNode;
317
+ /**
318
+ * The function to customize the label area of each option in the list, such as adding an icon or tags.
319
+ *
320
+ * Unlike `renderOption`, only the label text is replaced. The checkbox and the
321
+ * hover, focus, pressed, and disabled styles of the option are kept.
322
+ *
323
+ * - The `label` of the option is still required and is used for search and the trigger.
324
+ * - The slot content becomes the accessible name of the option. Include the same words as `label`,
325
+ * as text or as the accessible name of an image, so that what users read matches what they can type.
326
+ * - Return only non-interactive content, because the option itself is the interactive element.
327
+ * - Ignored when `renderOption` is provided.
328
+ * - Define the function outside the component or wrap it in `useCallback`, so that the options are
329
+ * not re-rendered every time the parent renders.
330
+ *
331
+ * @param option - The option object being rendered (includes additional properties if defined)
332
+ *
333
+ * @param state.isSelected - Whether the option is currently selected
334
+ *
335
+ * @example
336
+ * ```tsx
337
+ * <MultipleSelectBox
338
+ * options={options}
339
+ * renderOptionLabelSlot={(option) => (
340
+ * <>
341
+ * <Icon aria-hidden />
342
+ * <span>{option.label}</span>
343
+ * </>
344
+ * )}
345
+ * />
346
+ * ```
347
+ */
348
+ renderOptionLabelSlot?: RenderOptionLabelSlot<MultipleSelectBoxOption<T, AdditionalProps>>;
316
349
  /**
317
350
  * Customize the popover header content.
318
351
  *
@@ -43,7 +43,7 @@ const EMPTY_OPTIONS = [];
43
43
  export const SelectBox = forwardRef((props, ref) => {
44
44
  const { id, triggerProps, clearButtonProps, enableClearButton = false, size, options = EMPTY_OPTIONS, defaultValue,
45
45
  // eslint-disable-next-line @typescript-eslint/no-deprecated
46
- placeholder, emptyMessage, disabled, invalid, caution = false, required, targetDOMNode, name, onChange, onBlur, value, enableSearchOptions = false, notFoundMessage, searchBoxProps, loading = false, onSearchOptions, renderDisplayValue, renderOption, renderPopoverContent, enableAutoScrollToSelectedOption, popoverContentProps, popoverWrapperProps, onOpenStateChanged, showGroupOptionDivider, triggerWrapperProps, enableAutoUnmount = true, enableConstrainedPopoverWidth = false, enableVirtualization = false, virtualizationOptions, infiniteScroll, } = props;
46
+ placeholder, emptyMessage, disabled, invalid, caution = false, required, targetDOMNode, name, onChange, onBlur, value, enableSearchOptions = false, notFoundMessage, searchBoxProps, loading = false, onSearchOptions, renderDisplayValue, renderOption, renderOptionLabelSlot, renderPopoverContent, enableAutoScrollToSelectedOption, popoverContentProps, popoverWrapperProps, onOpenStateChanged, showGroupOptionDivider, triggerWrapperProps, enableAutoUnmount = true, enableConstrainedPopoverWidth = false, enableVirtualization = false, virtualizationOptions, infiniteScroll, } = props;
47
47
  const classes = selectBoxSlotRecipe({ size, showGroupOptionDivider });
48
48
  const triggerRef = useRef(null);
49
49
  const listBoxId = useId();
@@ -272,7 +272,7 @@ export const SelectBox = forwardRef((props, ref) => {
272
272
  left: 0,
273
273
  width: '100%',
274
274
  transform: `translateY(${String(virtualItem.start)}px)`,
275
- }, 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) => {
275
+ }, children: _jsx(SelectBoxOptionComponent, { option: option, isSelected: localSelectedOption?.value === option.value, tabbableOptionIndex: tabbableOptionIndex, optionIndex: originalIndex ?? virtualItem.index, classes: classes, groupLabelId: groupLabelId, renderOption: renderOption, renderOptionLabelSlot: renderOptionLabelSlot, onFocus: handleOptionFocus, onClick: handleSelectOption, onKeyDown: (event) => {
276
276
  handleTypeAhead(event.nativeEvent);
277
277
  createKeyDownHandler(option)(event);
278
278
  } }) }, virtualItem.key));
@@ -283,6 +283,7 @@ export const SelectBox = forwardRef((props, ref) => {
283
283
  tabbableOptionIndex,
284
284
  classes,
285
285
  renderOption,
286
+ renderOptionLabelSlot,
286
287
  handleOptionFocus,
287
288
  handleSelectOption,
288
289
  handleTypeAhead,
@@ -297,14 +298,14 @@ export const SelectBox = forwardRef((props, ref) => {
297
298
  return null;
298
299
  if ('options' in option && Array.isArray(option.options)) {
299
300
  currentGroupLabelId = getGroupLabelId(listBoxId, optionIndex);
300
- 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) => {
301
+ return (_jsx(React.Fragment, { children: _jsx(SelectBoxOptionGroup, { groupOption: option, selectedOption: localSelectedOption, valueToIndexMap: valueToIndexMap, tabbableOptionIndex: tabbableOptionIndex, classes: classes, groupLabelId: currentGroupLabelId, hasHiddenGroupLabel: rendersHiddenGroupLabels, renderOption: renderOption, renderOptionLabelSlot: renderOptionLabelSlot, findFlatOptionIndexByValue: (_, option) => findFlatOptionIndexByValue(valueToIndexMap, option), onOptionFocus: handleOptionFocus, onOptionClick: handleSelectOption, onOptionKeyDown: (option) => (event) => {
301
302
  handleTypeAhead(event.nativeEvent);
302
303
  createKeyDownHandler(option)(event);
303
304
  } }) }, `group-label-${option.label}-${String(optionIndex)}`));
304
305
  }
305
306
  if (!('options' in option)) {
306
307
  const flatOptionIndex = findFlatOptionIndexByValue(valueToIndexMap, option);
307
- 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) => {
308
+ return (_jsx(SelectBoxOptionComponent, { option: option, isSelected: localSelectedOption?.value === option.value, tabbableOptionIndex: tabbableOptionIndex, optionIndex: flatOptionIndex, classes: classes, groupLabelId: currentGroupLabelId, renderOption: renderOption, renderOptionLabelSlot: renderOptionLabelSlot, onFocus: handleOptionFocus, onClick: handleSelectOption, onKeyDown: (event) => {
308
309
  handleTypeAhead(event.nativeEvent);
309
310
  createKeyDownHandler(option)(event);
310
311
  } }, option.value ?? `option-${option.label}-${String(optionIndex)}`));
@@ -320,6 +321,7 @@ export const SelectBox = forwardRef((props, ref) => {
320
321
  listBoxId,
321
322
  rendersHiddenGroupLabels,
322
323
  renderOption,
324
+ renderOptionLabelSlot,
323
325
  handleOptionFocus,
324
326
  handleSelectOption,
325
327
  handleTypeAhead,
@@ -4,6 +4,7 @@ import { type SelectBoxSlotRecipeVariant } from '../../styled-system/recipes';
4
4
  import { type SearchBoxProps } from '../SearchBox';
5
5
  import { type PopoverProps } from '../Popover';
6
6
  import { type InfiniteScrollDirection } from '../utilities/dom/useInfiniteScroll';
7
+ import { type RenderOptionLabelSlot } from '../shared/OptionLabel';
7
8
  type AllowedValueTypes = string | number | undefined;
8
9
  export type VirtualOptionTypes = Pick<VirtualizerOptions<HTMLElement, Element>, 'estimateSize' | 'overscan'>;
9
10
  export type InfiniteScrollConfig = {
@@ -349,6 +350,38 @@ export type SelectBoxProps<T extends AllowedValueTypes = string, AdditionalProps
349
350
  * ```
350
351
  */
351
352
  renderOption?: (option: SelectBoxOption<T, AdditionalProps>, isSelected: boolean) => React.ReactNode;
353
+ /**
354
+ * The function to customize the label area of each option in the list, such as adding an icon or tags.
355
+ *
356
+ * Unlike `renderOption`, only the label text is replaced. The selected mark and the
357
+ * hover, focus, pressed, and disabled styles of the option are kept.
358
+ *
359
+ * - The `label` of the option is still required and is used for search, type-ahead, and the trigger.
360
+ * - The slot content becomes the accessible name of the option. Include the same words as `label`,
361
+ * as text or as the accessible name of an image, so that what users read matches what they can type.
362
+ * - Return only non-interactive content, because the option itself is the interactive element.
363
+ * - Ignored when `renderOption` is provided.
364
+ * - Define the function outside the component or wrap it in `useCallback`, so that the options are
365
+ * not re-rendered every time the parent renders.
366
+ *
367
+ * @param option - The option object being rendered (includes additional properties if defined)
368
+ *
369
+ * @param state.isSelected - Whether the option is currently selected
370
+ *
371
+ * @example
372
+ * ```tsx
373
+ * <SelectBox
374
+ * options={options}
375
+ * renderOptionLabelSlot={(option) => (
376
+ * <>
377
+ * <Icon aria-hidden />
378
+ * <span>{option.label}</span>
379
+ * </>
380
+ * )}
381
+ * />
382
+ * ```
383
+ */
384
+ renderOptionLabelSlot?: RenderOptionLabelSlot<SelectBoxOption<T, AdditionalProps>>;
352
385
  /**
353
386
  * The function to customize how the popover is rendered.
354
387
  * - `searchNode` is the search input element. It's available only when `enableSearchOptions` is `true`.
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { type BasedAdditionalProps, type SelectBoxOption } from './SelectBox.types';
2
+ import { type BasedAdditionalProps, type SelectBoxOption, type SelectBoxProps } from './SelectBox.types';
3
3
  type SelectBoxOptionProps<T extends string | number | undefined = string, AdditionalProps extends BasedAdditionalProps = BasedAdditionalProps> = {
4
4
  /**
5
5
  * The option data to render.
@@ -32,6 +32,10 @@ type SelectBoxOptionProps<T extends string | number | undefined = string, Additi
32
32
  * Custom render function for the option.
33
33
  */
34
34
  renderOption?: (option: SelectBoxOption<T, AdditionalProps>, isSelected: boolean) => React.ReactNode;
35
+ /**
36
+ * Custom render function for the label area of the option.
37
+ */
38
+ renderOptionLabelSlot?: SelectBoxProps<T, AdditionalProps>['renderOptionLabelSlot'];
35
39
  /**
36
40
  * Handler called when option receives focus.
37
41
  */
@@ -3,14 +3,14 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
3
3
  import { memo, useMemo, useCallback } from 'react';
4
4
  import { Current } from '@moneyforward/mfui-icons-react';
5
5
  import { FocusIndicator } from '../FocusIndicator';
6
- import { Typography } from '../Typography';
6
+ import { OptionLabel } from '../shared/OptionLabel';
7
7
  import { cx } from '../../styled-system/css';
8
8
  import { isOptionDisabled } from './utils/isSelectableOption';
9
9
  /**
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, groupLabelId, renderOption, onFocus, onClick, onKeyDown, }) {
13
+ function SelectBoxOptionComponentInner({ option, isSelected, tabbableOptionIndex, optionIndex, classes, groupLabelId, renderOption, renderOptionLabelSlot, 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(() => {
@@ -25,8 +25,7 @@ function SelectBoxOptionComponentInner({ option, isSelected, tabbableOptionIndex
25
25
  }, [onClick, option, isDisabled]);
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
- 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, "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 })] })) }) }));
28
+ 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(OptionLabel, { option: option, isSelected: isSelected, renderOptionLabelSlot: renderOptionLabelSlot, slotContentName: "select-box-option-label-slot" })] })) }) }));
30
29
  }
31
30
  export const SelectBoxOptionComponent = memo(SelectBoxOptionComponentInner);
32
31
  SelectBoxOptionComponent.displayName = 'SelectBoxOptionComponent';
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { type BasedAdditionalProps, type SelectBoxOption } from './SelectBox.types';
2
+ import { type BasedAdditionalProps, type SelectBoxOption, type SelectBoxProps } from './SelectBox.types';
3
3
  type SelectBoxOptionGroupProps<T extends string | number | undefined = string, AdditionalProps extends BasedAdditionalProps = BasedAdditionalProps> = {
4
4
  /**
5
5
  * The group option with nested options.
@@ -38,6 +38,10 @@ type SelectBoxOptionGroupProps<T extends string | number | undefined = string, A
38
38
  * Custom render function for options.
39
39
  */
40
40
  renderOption?: (option: SelectBoxOption<T, AdditionalProps>, isSelected: boolean) => React.ReactNode;
41
+ /**
42
+ * Custom render function for the label area of options.
43
+ */
44
+ renderOptionLabelSlot?: SelectBoxProps<T, AdditionalProps>['renderOptionLabelSlot'];
41
45
  /**
42
46
  * Handler called when option receives focus.
43
47
  */
@@ -8,7 +8,7 @@ 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, groupLabelId, hasHiddenGroupLabel, renderOption, onOptionFocus, onOptionClick, onOptionKeyDown, findFlatOptionIndexByValue, }) {
11
+ function SelectBoxOptionGroupInner({ groupOption, selectedOption, valueToIndexMap, tabbableOptionIndex, classes, groupLabelId, hasHiddenGroupLabel, renderOption, renderOptionLabelSlot, 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;
@@ -23,7 +23,7 @@ function SelectBoxOptionGroupInner({ groupOption, selectedOption, valueToIndexMa
23
23
  const optionLabel = nestedOption.label;
24
24
  const keyValue = optionValue !== undefined ? String(optionValue) : `${optionLabel}-${String(index)}`;
25
25
  const isSelected = selectedOption && 'value' in selectedOption ? selectedOption.value === optionValue : false;
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));
26
+ return (_jsx(SelectBoxOptionComponent, { option: nestedOption, isSelected: isSelected, tabbableOptionIndex: tabbableOptionIndex, optionIndex: flatOptionIndex, classes: classes, groupLabelId: groupLabelId, renderOption: renderOption, renderOptionLabelSlot: renderOptionLabelSlot, onFocus: onOptionFocus, onClick: onOptionClick, onKeyDown: onOptionKeyDown(nestedOption) }, keyValue));
27
27
  })] }));
28
28
  }
29
29
  export const SelectBoxOptionGroup = memo(SelectBoxOptionGroupInner);
@@ -0,0 +1,45 @@
1
+ import { type ReactNode } from 'react';
2
+ /**
3
+ * The state of an option passed to the function that renders its label slot.
4
+ */
5
+ export type OptionLabelSlotState = {
6
+ /**
7
+ * Whether the option is currently selected.
8
+ */
9
+ isSelected: boolean;
10
+ };
11
+ /**
12
+ * The function that renders the label slot of an option, replacing its label text.
13
+ */
14
+ export type RenderOptionLabelSlot<Option> = (option: Option, state: OptionLabelSlotState) => ReactNode;
15
+ type OptionLabelProps<Option extends {
16
+ label: string;
17
+ }> = {
18
+ /**
19
+ * The option whose label is rendered.
20
+ */
21
+ option: Option;
22
+ /**
23
+ * Whether the option is currently selected.
24
+ */
25
+ isSelected: boolean;
26
+ /**
27
+ * The function that renders the label slot. The label text is rendered when it is omitted.
28
+ */
29
+ renderOptionLabelSlot?: RenderOptionLabelSlot<Option>;
30
+ /**
31
+ * The `data-mfui-content` value of the slot wrapper, prefixed with the component name so that the
32
+ * component styles can target it.
33
+ */
34
+ slotContentName: string;
35
+ };
36
+ /**
37
+ * Renders the label of an option in an option list, or the label slot that replaces it.
38
+ *
39
+ * The slot is wrapped so that it keeps the default text style and an icon returned by it is not
40
+ * styled as the selected mark of the option (`& > svg`). A `div` because the slot may return block content.
41
+ */
42
+ export declare const OptionLabel: <Option extends {
43
+ label: string;
44
+ }>({ option, isSelected, renderOptionLabelSlot, slotContentName, }: OptionLabelProps<Option>) => import("react/jsx-runtime").JSX.Element;
45
+ export {};
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Typography } from '../../Typography';
3
+ /**
4
+ * Renders the label of an option in an option list, or the label slot that replaces it.
5
+ *
6
+ * The slot is wrapped so that it keeps the default text style and an icon returned by it is not
7
+ * styled as the selected mark of the option (`& > svg`). A `div` because the slot may return block content.
8
+ */
9
+ export const OptionLabel = ({ option, isSelected, renderOptionLabelSlot, slotContentName, }) => renderOptionLabelSlot ? (_jsx(Typography, { as: "div", variant: "body", "data-mfui-content": slotContentName, children: renderOptionLabelSlot(option, { isSelected }) })) : (_jsx(Typography, { variant: "body", children: option.label }));
@@ -0,0 +1 @@
1
+ export * from './OptionLabel';
@@ -0,0 +1 @@
1
+ export * from './OptionLabel';
@@ -4,6 +4,10 @@ import type { DistributiveOmit, Pretty } from '../types/system-types';
4
4
 
5
5
  interface DataTableSlotRecipeVariant {
6
6
  /**
7
+ * @default false
8
+ */
9
+ columnVirtualization: boolean
10
+ /**
7
11
  * @default "default"
8
12
  */
9
13
  layout: "default" | "edge-to-edge"
@@ -13,7 +17,7 @@ type DataTableSlotRecipeVariantMap = {
13
17
  [key in keyof DataTableSlotRecipeVariant]: Array<DataTableSlotRecipeVariant[key]>
14
18
  }
15
19
 
16
- type DataTableSlotRecipeSlot = "wrapper" | "table" | "emptyState"
20
+ type DataTableSlotRecipeSlot = "wrapper" | "table" | "emptyState" | "columnSpacer" | "headerColumnSpacer" | "selectionColumn"
17
21
 
18
22
  export type DataTableSlotRecipeVariantProps = {
19
23
  [key in keyof DataTableSlotRecipeVariant]?: ConditionalValue<DataTableSlotRecipeVariant[key]> | undefined
@@ -1,7 +1,8 @@
1
1
  import { compact, getSlotCompoundVariant, memo, splitProps } from '../helpers.js';
2
2
  import { createRecipe } from './create-recipe.js';
3
3
  const dataTableSlotRecipeDefaultVariants = {
4
- "layout": "default"
4
+ "layout": "default",
5
+ "columnVirtualization": false
5
6
  };
6
7
  const dataTableSlotRecipeCompoundVariants = [];
7
8
  const dataTableSlotRecipeSlotNames = [
@@ -16,6 +17,18 @@ const dataTableSlotRecipeSlotNames = [
16
17
  [
17
18
  "emptyState",
18
19
  "DataTable__emptyState"
20
+ ],
21
+ [
22
+ "columnSpacer",
23
+ "DataTable__columnSpacer"
24
+ ],
25
+ [
26
+ "headerColumnSpacer",
27
+ "DataTable__headerColumnSpacer"
28
+ ],
29
+ [
30
+ "selectionColumn",
31
+ "DataTable__selectionColumn"
19
32
  ]
20
33
  ];
21
34
  const dataTableSlotRecipeSlotFns = /* @__PURE__ */ dataTableSlotRecipeSlotNames.map(([slotName, slotKey]) => [slotName, createRecipe(slotKey, dataTableSlotRecipeDefaultVariants, getSlotCompoundVariant(dataTableSlotRecipeCompoundVariants, slotName))]);
@@ -23,6 +36,7 @@ const dataTableSlotRecipeFn = memo((props = {}) => {
23
36
  return Object.fromEntries(dataTableSlotRecipeSlotFns.map(([slotName, slotFn]) => [slotName, slotFn.recipeFn(props)]));
24
37
  });
25
38
  const dataTableSlotRecipeVariantKeys = [
39
+ "columnVirtualization",
26
40
  "layout"
27
41
  ];
28
42
  const getVariantProps = (variants) => ({ ...dataTableSlotRecipeDefaultVariants, ...compact(variants) });
@@ -33,6 +47,10 @@ export const dataTableSlotRecipe = /* @__PURE__ */ Object.assign(dataTableSlotRe
33
47
  classNameMap: {},
34
48
  variantKeys: dataTableSlotRecipeVariantKeys,
35
49
  variantMap: {
50
+ "columnVirtualization": [
51
+ "true",
52
+ "false"
53
+ ],
36
54
  "layout": [
37
55
  "default",
38
56
  "edge-to-edge"