@moneyforward/mfui-components 4.7.0 → 4.9.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.js +38 -5
- package/dist/src/DataTable/DataTable.types.d.ts +101 -0
- package/dist/src/DataTable/DataTableHeaderCell/DataTableHeaderCell.js +5 -2
- package/dist/src/DataTable/DataTableHeaderRow/DataTableHeaderRow.js +31 -15
- package/dist/src/DataTable/DataTableProvider.d.ts +19 -4
- package/dist/src/DataTable/DataTableProvider.js +11 -3
- package/dist/src/DataTable/DataTableRow/DataTableRow.js +21 -12
- package/dist/src/DataTable/hooks/useVirtualizedColumns.d.ts +73 -0
- package/dist/src/DataTable/hooks/useVirtualizedColumns.js +109 -0
- package/dist/src/DataTable/index.d.ts +1 -1
- package/dist/src/DataTable/shared/commonStyles.d.ts +11 -0
- package/dist/src/DataTable/shared/commonStyles.js +11 -0
- package/dist/src/DataTable/utils/renderVirtualizedColumns.d.ts +42 -0
- package/dist/src/DataTable/utils/renderVirtualizedColumns.js +40 -0
- package/dist/src/DataTable/utils/resolveColumnWidths.d.ts +28 -0
- package/dist/src/DataTable/utils/resolveColumnWidths.js +41 -0
- package/dist/src/MultipleSelectBox/MultipleSelectBox.d.ts +1 -1
- package/dist/src/MultipleSelectBox/MultipleSelectBox.js +10 -7
- package/dist/src/MultipleSelectBox/MultipleSelectBox.types.d.ts +33 -0
- package/dist/src/Popover/Popover.d.ts +7 -0
- package/dist/src/Popover/Popover.js +7 -0
- package/dist/src/SelectBox/SelectBox.js +6 -4
- package/dist/src/SelectBox/SelectBox.types.d.ts +33 -0
- package/dist/src/SelectBox/SelectBoxOption.d.ts +5 -1
- package/dist/src/SelectBox/SelectBoxOption.js +3 -4
- package/dist/src/SelectBox/SelectBoxOptionGroup.d.ts +5 -1
- package/dist/src/SelectBox/SelectBoxOptionGroup.js +2 -2
- package/dist/src/shared/OptionLabel/OptionLabel.d.ts +45 -0
- package/dist/src/shared/OptionLabel/OptionLabel.js +9 -0
- package/dist/src/shared/OptionLabel/index.d.ts +1 -0
- package/dist/src/shared/OptionLabel/index.js +1 -0
- package/dist/styled-system/recipes/data-table-slot-recipe.d.ts +5 -1
- package/dist/styled-system/recipes/data-table-slot-recipe.js +19 -1
- package/dist/styled-system/tokens/index.js +4 -0
- package/dist/styled-system/tokens/tokens.d.ts +1 -1
- package/dist/styles.css +33 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- 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
|
|
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(
|
|
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
|
|
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(
|
|
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,
|
|
@@ -321,7 +324,7 @@ placeholder, emptyMessage, disabled, invalid, required, caution, targetDOMNode,
|
|
|
321
324
|
return (_jsxs(Tag, { className: cx(classes.infiniteScrollError, 'mfui-MultipleSelectBox__infiniteScrollError'), role: "alert", children: [_jsxs("div", { className: cx(classes.infiniteScrollErrorMessage, 'mfui-MultipleSelectBox__infiniteScrollErrorMessage'), "aria-live": "polite", children: [_jsx(Error, { "aria-hidden": true, className: cx(classes.infiniteScrollErrorIcon, 'mfui-MultipleSelectBox__infiniteScrollErrorIcon') }), _jsx(Typography, { variant: "body", children: infiniteScrollErrorMessage })] }), _jsx("div", { className: cx(classes.infiniteScrollErrorButton, 'mfui-MultipleSelectBox__infiniteScrollErrorButton'), children: _jsx(Button, { size: "small", onClick: (event) => {
|
|
322
325
|
event.stopPropagation();
|
|
323
326
|
retryInfiniteScroll();
|
|
324
|
-
}, children: infiniteScrollRetryButtonText }) })] }));
|
|
327
|
+
}, children: infiniteScrollRetryButtonText }) })] }, "infinite-scroll-error"));
|
|
325
328
|
}, [
|
|
326
329
|
infiniteScrollError,
|
|
327
330
|
classes.infiniteScrollError,
|
|
@@ -333,7 +336,7 @@ placeholder, emptyMessage, disabled, invalid, required, caution, targetDOMNode,
|
|
|
333
336
|
infiniteScrollRetryButtonText,
|
|
334
337
|
]);
|
|
335
338
|
// Render infinite scroll loading indicator
|
|
336
|
-
const renderInfiniteScrollLoading = useCallback(() => isInfiniteScrollLoading && enableInfiniteScroll ? (_jsx("div", { className: cx(classes.infiniteScrollLoading, 'mfui-MultipleSelectBox__infiniteScrollLoading'), children: _jsx(ProgressIndicator, {}) })) : null, [isInfiniteScrollLoading, enableInfiniteScroll, classes.infiniteScrollLoading]);
|
|
339
|
+
const renderInfiniteScrollLoading = useCallback(() => isInfiniteScrollLoading && enableInfiniteScroll ? (_jsx("div", { className: cx(classes.infiniteScrollLoading, 'mfui-MultipleSelectBox__infiniteScrollLoading'), children: _jsx(ProgressIndicator, {}) }, "infinite-scroll-loading")) : null, [isInfiniteScrollLoading, enableInfiniteScroll, classes.infiniteScrollLoading]);
|
|
337
340
|
const searchNode = enableSearchOptions ? (_jsx(SearchBox, { ref: searchInputRef, enableClearButton: true, value: searchText, textBoxSize: "small", autoComplete: "off", "aria-label": "\u691C\u7D22\u3059\u308B", onChange: onSearchTextChange, ...searchBoxProps })) : null;
|
|
338
341
|
const allOptionsControlsNode = enableAllOptionsControls ? (_jsxs(HStack, { justifyContent: "space-between", children: [_jsx(Button, { ref: selectAllButtonRef, size: "small", onClick: handleSelectAll, children: selectAllButtonProps?.label ?? 'すべて選択' }), _jsx(Button, { ref: clearAllButtonRef, size: "small", onClick: handleClearAll, children: clearAllButtonProps?.label ?? 'すべて解除' })] })) : null;
|
|
339
342
|
const selectedCountNode = enableApplyControls || showSelectedCount ? (_jsx(Typography, { variant: "body", color: "neutral.600", children: enableApplyControls
|
|
@@ -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
|
*
|
|
@@ -5,5 +5,12 @@ import { type PopoverProps } from './Popover.types';
|
|
|
5
5
|
*
|
|
6
6
|
* You can control the open state of the Popover by passing the `open` and `onOpenStateChanged` props
|
|
7
7
|
* Or just leave it as an uncontrolled popover, it will automatically handle by itself.
|
|
8
|
+
*
|
|
9
|
+
* ## Layering
|
|
10
|
+
*
|
|
11
|
+
* The popover content is drawn on the `--mfui-z-index-popover` layer (`6000`), above `Dialog` and `SidePane`
|
|
12
|
+
* and below `Toast` and `Tooltip`.
|
|
13
|
+
* To place it above an application overlay with a higher `z-index`, set `z-index` through `contentProps.className`.
|
|
14
|
+
* See the "Layering" page in Storybook for the full layer order.
|
|
8
15
|
*/
|
|
9
16
|
export declare function Popover({ renderTrigger, renderContent, open, onOpenStateChanged, onOpen, onClose, enableAutoFocusOnTrigger, enableAutoFocusOnPopover, minWidth, maxHeight, enableConstrainedContentWidth, contentProps, targetDOMNode, enableAutoUnmount, onBlur, allowedPlacements, enableViewportConstraint, enableAutomaticPortalTargetResolution, enableCenteredLayoutOnNarrowViewport, }: PopoverProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -18,6 +18,13 @@ import { cx } from '../../styled-system/css';
|
|
|
18
18
|
*
|
|
19
19
|
* You can control the open state of the Popover by passing the `open` and `onOpenStateChanged` props
|
|
20
20
|
* Or just leave it as an uncontrolled popover, it will automatically handle by itself.
|
|
21
|
+
*
|
|
22
|
+
* ## Layering
|
|
23
|
+
*
|
|
24
|
+
* The popover content is drawn on the `--mfui-z-index-popover` layer (`6000`), above `Dialog` and `SidePane`
|
|
25
|
+
* and below `Toast` and `Tooltip`.
|
|
26
|
+
* To place it above an application overlay with a higher `z-index`, set `z-index` through `contentProps.className`.
|
|
27
|
+
* See the "Layering" page in Storybook for the full layer order.
|
|
21
28
|
*/
|
|
22
29
|
export function Popover({ renderTrigger, renderContent, open, onOpenStateChanged, onOpen, onClose, enableAutoFocusOnTrigger = true, enableAutoFocusOnPopover = true, minWidth, maxHeight, enableConstrainedContentWidth = false, contentProps = {}, targetDOMNode, enableAutoUnmount = true, onBlur, allowedPlacements, enableViewportConstraint = true, enableAutomaticPortalTargetResolution = false, enableCenteredLayoutOnNarrowViewport = false, }) {
|
|
23
30
|
// Toggle event is the event triggered by open / close the Popover
|
|
@@ -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 {
|
|
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
|
-
|
|
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
|