@moneyforward/mfui-components 3.1.0 → 3.3.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 (48) hide show
  1. package/dist/src/DateTimeSelection/shared/BasePicker/BasePicker.d.ts +1 -1
  2. package/dist/src/DateTimeSelection/shared/BasePicker/BasePicker.js +6 -6
  3. package/dist/src/DateTimeSelection/shared/BasePicker/BasePicker.types.d.ts +1 -1
  4. package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePicker.d.ts +1 -1
  5. package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePicker.js +4 -4
  6. package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePicker.types.d.ts +8 -0
  7. package/dist/src/DateTimeSelection/shared/CalendarGrid/CalendarGrid.js +3 -1
  8. package/dist/src/DisplayTable/DisplayTable.d.ts +3 -2
  9. package/dist/src/DisplayTable/DisplayTable.js +4 -3
  10. package/dist/src/DisplayTable/DisplayTable.types.d.ts +7 -0
  11. package/dist/src/DisplayTable/DisplayTableBody/DisplayTableBody.js +15 -0
  12. package/dist/src/DisplayTable/DisplayTableCell/DisplayTableCell.js +9 -2
  13. package/dist/src/DisplayTable/DisplayTableHeaderRow/DisplayTableHeaderRow.js +12 -1
  14. package/dist/src/DisplayTable/DisplayTableProvider.d.ts +8 -4
  15. package/dist/src/DisplayTable/DisplayTableProvider.js +12 -3
  16. package/dist/src/MultipleSelectBox/MultipleSelectBox.d.ts +1 -1
  17. package/dist/src/MultipleSelectBox/MultipleSelectBox.js +65 -15
  18. package/dist/src/MultipleSelectBox/MultipleSelectBox.types.d.ts +86 -0
  19. package/dist/src/SelectBox/SelectBox.js +49 -11
  20. package/dist/src/SelectBox/SelectBox.types.d.ts +80 -1
  21. package/dist/src/Tag/Tag.js +1 -1
  22. package/dist/src/ToggleSwitch/ToggleSwitch.d.ts +9 -0
  23. package/dist/src/ToggleSwitch/ToggleSwitch.js +32 -0
  24. package/dist/src/ToggleSwitch/ToggleSwitch.types.d.ts +6 -0
  25. package/dist/src/ToggleSwitch/ToggleSwitch.types.js +1 -0
  26. package/dist/src/ToggleSwitch/index.d.ts +2 -0
  27. package/dist/src/ToggleSwitch/index.js +2 -0
  28. package/dist/src/index.d.ts +1 -0
  29. package/dist/src/index.js +1 -0
  30. package/dist/src/utilities/dom/useFixedColumns.js +36 -10
  31. package/dist/src/utilities/dom/useInfiniteScroll.d.ts +22 -0
  32. package/dist/src/utilities/dom/useInfiniteScroll.js +65 -0
  33. package/dist/styled-system/css/conditions.js +1 -1
  34. package/dist/styled-system/jsx/is-valid-prop.js +1 -1
  35. package/dist/styled-system/recipes/display-table-cell-slot-recipe.d.ts +1 -1
  36. package/dist/styled-system/recipes/display-table-cell-slot-recipe.js +4 -0
  37. package/dist/styled-system/recipes/index.d.ts +2 -1
  38. package/dist/styled-system/recipes/index.js +1 -0
  39. package/dist/styled-system/recipes/multiple-select-box-slot-recipe.d.ts +1 -1
  40. package/dist/styled-system/recipes/multiple-select-box-slot-recipe.js +24 -0
  41. package/dist/styled-system/recipes/select-box-slot-recipe.d.ts +2 -2
  42. package/dist/styled-system/recipes/select-box-slot-recipe.js +30 -1
  43. package/dist/styled-system/recipes/toggle-switch-slot-recipe.d.ts +33 -0
  44. package/dist/styled-system/recipes/toggle-switch-slot-recipe.js +36 -0
  45. package/dist/styled-system/types/conditions.d.ts +10 -0
  46. package/dist/styles.css +298 -34
  47. package/dist/tsconfig.build.tsbuildinfo +1 -1
  48. package/package.json +2 -2
@@ -4,4 +4,4 @@ import { type BasePickerProps } from './BasePicker.types';
4
4
  * This component uses the composition component of the TextBox component. Please refer to the TextBox documentation for their usage.
5
5
  * This component extends the props of TextBox component.
6
6
  */
7
- export declare function BasePicker({ targetDOMNode, enableAutoUnmount, open, onOpenStateChanged, disableAutoOpen, format, baseFormat, customFormatValue, value, defaultValue, onChange, calendarIconButtonProps, renderPopoverContent, onBlur, allowedPlacements, calendarLocale, disabled, ...textBoxProps }: BasePickerProps): import("react/jsx-runtime").JSX.Element;
7
+ export declare function BasePicker({ targetDOMNode, enableAutoUnmount, open, onOpenStateChanged, disableAutoOpen, format, baseFormat, customFormatValue, value, defaultValue, onChange, calendarIconButtonProps, renderPopoverContent, onBlur, allowedPlacements, enableViewportConstraint, calendarLocale, disabled, ...textBoxProps }: BasePickerProps): import("react/jsx-runtime").JSX.Element;
@@ -18,7 +18,7 @@ import { getBasePickerLabel } from './constants';
18
18
  * This component uses the composition component of the TextBox component. Please refer to the TextBox documentation for their usage.
19
19
  * This component extends the props of TextBox component.
20
20
  */
21
- export function BasePicker({ targetDOMNode, enableAutoUnmount = true, open = false, onOpenStateChanged, disableAutoOpen = false, format = 'YYYY/MM/DD', baseFormat = 'YYYY-MM-DD', customFormatValue, value, defaultValue, onChange, calendarIconButtonProps, renderPopoverContent, onBlur, allowedPlacements, calendarLocale = 'ja', disabled, ...textBoxProps }) {
21
+ export function BasePicker({ targetDOMNode, enableAutoUnmount = true, open = false, onOpenStateChanged, disableAutoOpen = false, format = 'YYYY/MM/DD', baseFormat = 'YYYY-MM-DD', customFormatValue, value, defaultValue, onChange, calendarIconButtonProps, renderPopoverContent, onBlur, allowedPlacements, enableViewportConstraint, calendarLocale = 'ja', disabled, ...textBoxProps }) {
22
22
  const textBoxRef = useRef(null);
23
23
  const basePickerPopoverWrapperRef = useRef(null);
24
24
  const triggerRef = useRef(null);
@@ -83,17 +83,17 @@ export function BasePicker({ targetDOMNode, enableAutoUnmount = true, open = fal
83
83
  textBoxProps.wrapperProps?.onKeyDown?.(event);
84
84
  handleTriggerKeyDown(event);
85
85
  },
86
- onClick: !disableAutoOpen ? handleWrapperClick(openPopover) : undefined,
86
+ onClick: !disableAutoOpen && !disabled ? handleWrapperClick(openPopover) : undefined,
87
87
  style: {
88
88
  ...textBoxProps.wrapperProps?.style,
89
- cursor: 'text',
89
+ cursor: disabled ? 'not-allowed' : 'text',
90
90
  },
91
91
  }, onBlur: (event) => {
92
92
  // First call the original input onBlur handler (contains onChange logic)
93
93
  inputValueProps.onBlur(event);
94
94
  // Then call the Popover's smart blur detection
95
95
  handleTriggerBlur(event);
96
- } })), open: isBasePickerOpen, targetDOMNode: targetDOMNode, enableAutoUnmount: enableAutoUnmount, minWidth: "min-content", value: value, defaultValue: defaultValue, baseFormat: baseFormat, renderPopoverContent: renderPopoverContent, textBoxRef: textBoxRef, triggerRef: triggerRef, basePickerPopoverWrapperRef: basePickerPopoverWrapperRef, handleOnKeyDown: handleOnKeyDown, pickerPopoverProps: pickerPopoverProps, allowedPlacements: allowedPlacements, onOpenStateChanged: toggleBasePicker, onBlur: onBlur }) }));
96
+ } })), open: isBasePickerOpen, targetDOMNode: targetDOMNode, enableAutoUnmount: enableAutoUnmount, minWidth: "min-content", value: value, defaultValue: defaultValue, baseFormat: baseFormat, renderPopoverContent: renderPopoverContent, textBoxRef: textBoxRef, triggerRef: triggerRef, basePickerPopoverWrapperRef: basePickerPopoverWrapperRef, handleOnKeyDown: handleOnKeyDown, pickerPopoverProps: pickerPopoverProps, allowedPlacements: allowedPlacements, enableViewportConstraint: enableViewportConstraint, onOpenStateChanged: toggleBasePicker, onBlur: onBlur }) }));
97
97
  }
98
98
  /**
99
99
  * Internal Popover component that needs access to BasePickerContext.
@@ -101,7 +101,7 @@ export function BasePicker({ targetDOMNode, enableAutoUnmount = true, open = fal
101
101
  * which requires being within a BasePickerProvider. Creating it as an internal
102
102
  * component ensures proper context access while keeping the logic encapsulated.
103
103
  */
104
- function InternalPopover({ value, defaultValue, textBoxRef, triggerRef, baseFormat = 'YYYY-MM-DD', renderPopoverContent, basePickerPopoverWrapperRef, handleOnKeyDown, pickerPopoverProps, onOpenStateChanged, allowedPlacements, ...popoverProps }) {
104
+ function InternalPopover({ value, defaultValue, textBoxRef, triggerRef, baseFormat = 'YYYY-MM-DD', renderPopoverContent, basePickerPopoverWrapperRef, handleOnKeyDown, pickerPopoverProps, onOpenStateChanged, allowedPlacements, enableViewportConstraint, ...popoverProps }) {
105
105
  // Always call the hook at the top level to ensure consistent hook order
106
106
  const { viewingValue, setViewingValue, setPendingFocusDate } = useBasePickerContext();
107
107
  const handlePopoverOpen = useCallback((event) => {
@@ -129,7 +129,7 @@ function InternalPopover({ value, defaultValue, textBoxRef, triggerRef, baseForm
129
129
  // Set the pending focus date to ensure proper focus after month change
130
130
  setPendingFocusDate(formattedDate);
131
131
  }, [textBoxRef, triggerRef, value, defaultValue, baseFormat, setViewingValue, setPendingFocusDate]);
132
- return (_jsx(Popover, { enableAutomaticPortalTargetResolution: true, enableViewportConstraint: false, enableAutoFocusOnPopover: false, ...popoverProps, renderContent: () => (_jsx("div", { ref: basePickerPopoverWrapperRef, className: "mfui-BasePicker__popoverWrapper", onKeyDown: handleOnKeyDown, children: renderPopoverContent({
132
+ return (_jsx(Popover, { enableAutomaticPortalTargetResolution: true, enableViewportConstraint: enableViewportConstraint ?? false, enableAutoFocusOnPopover: false, ...popoverProps, renderContent: () => (_jsx("div", { ref: basePickerPopoverWrapperRef, className: "mfui-BasePicker__popoverWrapper", onKeyDown: handleOnKeyDown, children: renderPopoverContent({
133
133
  viewingValue,
134
134
  setViewingValue,
135
135
  value: pickerPopoverProps.value,
@@ -118,4 +118,4 @@ export type BasePickerProps = {
118
118
  * @see https://en.wikipedia.org/wiki/ISO_8601
119
119
  */
120
120
  baseFormat?: string;
121
- } & Omit<TextBoxProps, 'value' | 'defaultValue' | 'onChange'> & Pick<PopoverProps, 'allowedPlacements'>;
121
+ } & Omit<TextBoxProps, 'value' | 'defaultValue' | 'onChange'> & Pick<PopoverProps, 'allowedPlacements' | 'enableViewportConstraint'>;
@@ -3,4 +3,4 @@ import { type BaseRangePickerProps } from './BaseRangePicker.types';
3
3
  * BaseRangePicker component
4
4
  * A generic component for selecting a range of dates with configurable format
5
5
  */
6
- export declare function BaseRangePicker({ value, defaultValue, disabled, invalid, targetDOMNode, onChange, format, onOpenStateChanged, disableAutoOpen, onBlur, allowedPlacements, enableClearButton, clearButtonProps, minDate, maxDate, enableAutoUnmount, minWidth, renderPopoverContent, startInputProps, endInputProps, calendarLocale, }: BaseRangePickerProps): import("react/jsx-runtime").JSX.Element;
6
+ export declare function BaseRangePicker({ value, defaultValue, disabled, invalid, targetDOMNode, onChange, format, onOpenStateChanged, disableAutoOpen, onBlur, allowedPlacements, enableViewportConstraint, enableClearButton, clearButtonProps, minDate, maxDate, enableAutoUnmount, minWidth, renderPopoverContent, startInputProps, endInputProps, calendarLocale, }: BaseRangePickerProps): import("react/jsx-runtime").JSX.Element;
@@ -9,7 +9,7 @@ import { dayjs } from '../../../utilities/date/dayjs';
9
9
  /**
10
10
  * Internal component that has access to BaseRangePickerContext
11
11
  */
12
- function InternalBaseRangePicker({ startInputRef, endInputRef, disabled, invalid, format, enableClearButton, clearButtonProps, targetDOMNode, onBlur, isOpen, open, close, onOpenStateChanged, enableAutoUnmount, allowedPlacements, minWidth, renderPopoverContent, startInputProps = {}, endInputProps = {}, calendarLocale = 'ja', }) {
12
+ function InternalBaseRangePicker({ startInputRef, endInputRef, disabled, invalid, format, enableClearButton, clearButtonProps, targetDOMNode, onBlur, isOpen, open, close, onOpenStateChanged, enableAutoUnmount, allowedPlacements, enableViewportConstraint, minWidth, renderPopoverContent, startInputProps = {}, endInputProps = {}, calendarLocale = 'ja', }) {
13
13
  const { setPendingFocusDate, temporaryStart, temporaryEnd, viewingMonth, setViewingMonth } = useBaseRangePickerContext();
14
14
  // Custom onOpen handler to prevent auto-focus when clicking on input elements
15
15
  // and implement smart focus management for calendar dates
@@ -49,7 +49,7 @@ function InternalBaseRangePicker({ startInputRef, endInputRef, disabled, invalid
49
49
  setPendingFocusDate(formattedDate);
50
50
  open();
51
51
  }, [startInputRef, endInputRef, temporaryStart, temporaryEnd, setPendingFocusDate, open, format]);
52
- return (_jsx(Popover, { enableAutomaticPortalTargetResolution: true, open: isOpen, allowedPlacements: allowedPlacements, minWidth: minWidth, renderTrigger: ({ setTriggerRef, handleTriggerBlur, handleTriggerKeyDown, openPopover }) => (_jsx(BaseRangePickerTrigger, { ref: setTriggerRef, disabled: disabled, invalid: invalid, format: format, enableClearValue: enableClearButton, clearButtonProps: clearButtonProps, startInputRef: startInputRef, endInputRef: endInputRef, startInputProps: startInputProps, endInputProps: endInputProps, popoverOpenFunction: openPopover, calendarLocale: calendarLocale, onBlur: handleTriggerBlur, onKeyDown: handleTriggerKeyDown })), renderContent: () => renderPopoverContent({
52
+ return (_jsx(Popover, { enableAutomaticPortalTargetResolution: true, open: isOpen, allowedPlacements: allowedPlacements, enableViewportConstraint: enableViewportConstraint, minWidth: minWidth, renderTrigger: ({ setTriggerRef, handleTriggerBlur, handleTriggerKeyDown, openPopover }) => (_jsx(BaseRangePickerTrigger, { ref: setTriggerRef, disabled: disabled, invalid: invalid, format: format, enableClearValue: enableClearButton, clearButtonProps: clearButtonProps, startInputRef: startInputRef, endInputRef: endInputRef, startInputProps: startInputProps, endInputProps: endInputProps, popoverOpenFunction: openPopover, calendarLocale: calendarLocale, onBlur: handleTriggerBlur, onKeyDown: handleTriggerKeyDown })), renderContent: () => renderPopoverContent({
53
53
  viewingValue: viewingMonth,
54
54
  setViewingValue: setViewingMonth,
55
55
  value: [temporaryStart, temporaryEnd],
@@ -61,12 +61,12 @@ function InternalBaseRangePicker({ startInputRef, endInputRef, disabled, invalid
61
61
  * BaseRangePicker component
62
62
  * A generic component for selecting a range of dates with configurable format
63
63
  */
64
- export function BaseRangePicker({ value, defaultValue, disabled, invalid, targetDOMNode, onChange, format = 'YYYY/MM/DD', onOpenStateChanged, disableAutoOpen = false, onBlur, allowedPlacements, enableClearButton = false, clearButtonProps, minDate, maxDate, enableAutoUnmount = true, minWidth, renderPopoverContent, startInputProps, endInputProps, calendarLocale = 'ja', }) {
64
+ export function BaseRangePicker({ value, defaultValue, disabled, invalid, targetDOMNode, onChange, format = 'YYYY/MM/DD', onOpenStateChanged, disableAutoOpen = false, onBlur, allowedPlacements, enableViewportConstraint, enableClearButton = false, clearButtonProps, minDate, maxDate, enableAutoUnmount = true, minWidth, renderPopoverContent, startInputProps, endInputProps, calendarLocale = 'ja', }) {
65
65
  const { isOpen, open, close } = useDisclosure({ value: false });
66
66
  const startInputRef = useRef(null);
67
67
  const endInputRef = useRef(null);
68
68
  // Default renderPopoverContent implementation for backward compatibility
69
69
  const defaultRenderPopoverContent = useCallback(() => _jsx(BaseRangePickerPopover, { calendarLocale: calendarLocale }), [calendarLocale]);
70
70
  const finalRenderPopoverContent = renderPopoverContent ?? defaultRenderPopoverContent;
71
- return (_jsx(BaseRangePickerProvider, { value: value, defaultValue: defaultValue, disabled: disabled, format: format, isOpen: isOpen, open: open, close: close, disableAutoOpen: disableAutoOpen, minDate: minDate, maxDate: maxDate, onChange: onChange, children: _jsx(InternalBaseRangePicker, { startInputRef: startInputRef, endInputRef: endInputRef, disabled: disabled, invalid: invalid, format: format, enableClearButton: enableClearButton, clearButtonProps: clearButtonProps, targetDOMNode: targetDOMNode, allowedPlacements: allowedPlacements, enableAutoUnmount: enableAutoUnmount, minWidth: minWidth, startInputProps: startInputProps, endInputProps: endInputProps, isOpen: isOpen, open: open, close: close, renderPopoverContent: finalRenderPopoverContent, calendarLocale: calendarLocale, onBlur: onBlur, onOpenStateChanged: onOpenStateChanged }) }));
71
+ return (_jsx(BaseRangePickerProvider, { value: value, defaultValue: defaultValue, disabled: disabled, format: format, isOpen: isOpen, open: open, close: close, disableAutoOpen: disableAutoOpen, minDate: minDate, maxDate: maxDate, onChange: onChange, children: _jsx(InternalBaseRangePicker, { startInputRef: startInputRef, endInputRef: endInputRef, disabled: disabled, invalid: invalid, format: format, enableClearButton: enableClearButton, clearButtonProps: clearButtonProps, targetDOMNode: targetDOMNode, allowedPlacements: allowedPlacements, enableViewportConstraint: enableViewportConstraint, enableAutoUnmount: enableAutoUnmount, minWidth: minWidth, startInputProps: startInputProps, endInputProps: endInputProps, isOpen: isOpen, open: open, close: close, renderPopoverContent: finalRenderPopoverContent, calendarLocale: calendarLocale, onBlur: onBlur, onOpenStateChanged: onOpenStateChanged }) }));
72
72
  }
@@ -128,6 +128,14 @@ export type BaseRangePickerProps = {
128
128
  * ```
129
129
  */
130
130
  allowedPlacements?: PopoverProps['allowedPlacements'];
131
+ /**
132
+ * Whether to enable viewport constraint for the popover.
133
+ * When true, the popover will be constrained within the visible viewport and a maxHeight is applied.
134
+ * Set to true when the picker is inside a constrained container like SidePane.
135
+ *
136
+ * @default true
137
+ */
138
+ enableViewportConstraint?: PopoverProps['enableViewportConstraint'];
131
139
  /**
132
140
  * Whether to enable the clear button functionality.
133
141
  * When enabled, a clear button will appear when there are values in the date inputs.
@@ -12,11 +12,13 @@ export function CalendarGrid({ viewingValue, minDate, maxDate, checkDisabledDate
12
12
  const dates = useDates(viewingValue);
13
13
  const computedDataForStyles = useMemo(() => computeDataForStyling(viewingValue), [viewingValue]);
14
14
  const computedPropsForDates = useMemo(() => {
15
+ const startOfMinDate = minDate ? new Date(minDate.getFullYear(), minDate.getMonth(), minDate.getDate()) : undefined;
16
+ const startOfMaxDate = maxDate ? new Date(maxDate.getFullYear(), maxDate.getMonth(), maxDate.getDate()) : undefined;
15
17
  const newDates = dates.map((dateNumber) => {
16
18
  if (!dateNumber)
17
19
  return null;
18
20
  const date = new Date(viewingValue.getFullYear(), viewingValue.getMonth(), dateNumber);
19
- const isDisabledByProps = (minDate && date < minDate) || (maxDate && date > maxDate);
21
+ const isDisabledByProps = (startOfMinDate && date < startOfMinDate) || (startOfMaxDate && date > startOfMaxDate);
20
22
  const isDisabledByCallback = checkDisabledDate?.(date);
21
23
  const disabled = isDisabledByProps || !!isDisabledByCallback;
22
24
  const isSelected = value ? checkIsSameDate(value, date) : false;
@@ -5,7 +5,7 @@ import { DisplayTableHeader } from './DisplayTableHeader';
5
5
  import { DisplayTableHeaderCell } from './DisplayTableHeaderCell';
6
6
  import { DisplayTableHeaderRow } from './DisplayTableHeaderRow';
7
7
  import { DisplayTableRow } from './DisplayTableRow';
8
- declare function Base({ title, titleTag, lead, className, fixedHeader, leftFixedColumnIndex, rightFixedColumnIndex, wrapperProps, ...props }: DisplayTableProps): import("react/jsx-runtime").JSX.Element;
8
+ declare function Base({ title, titleTag, lead, className, fixedHeader, leftFixedColumnIndex, rightFixedColumnIndex, wrapperProps, loading, ...props }: DisplayTableProps): import("react/jsx-runtime").JSX.Element;
9
9
  /**
10
10
  * The `DisplayTable` component.
11
11
  *
@@ -17,12 +17,13 @@ declare function Base({ title, titleTag, lead, className, fixedHeader, leftFixed
17
17
  * @example
18
18
  * ```jsx
19
19
  * <DisplayTable title="My Table" lead="Table lead text">
20
+ * <DisplayTable.Header>
20
21
  * <DisplayTable.HeaderRow>
21
22
  * <DisplayTable.HeaderCell>Header 1</DisplayTable.HeaderCell>
22
23
  * <DisplayTable.HeaderCell>Header 2</DisplayTable.HeaderCell>
23
24
  * </DisplayTable.HeaderRow>
24
25
  * </DisplayTable.Header>
25
- * </DisplayTable.Body>
26
+ * <DisplayTable.Body>
26
27
  * <DisplayTable.Row>
27
28
  * <DisplayTable.Cell>Cell 1</DisplayTable.Cell>
28
29
  * <DisplayTable.Cell>Cell 2</DisplayTable.Cell>
@@ -11,9 +11,9 @@ import { DisplayTableRow } from './DisplayTableRow';
11
11
  import { cx } from '../../styled-system/css';
12
12
  import { Heading } from '../Heading';
13
13
  import { Typography } from '../Typography';
14
- function Base({ title, titleTag = 'h4', lead, className, fixedHeader = false, leftFixedColumnIndex, rightFixedColumnIndex, wrapperProps, ...props }) {
14
+ function Base({ title, titleTag = 'h4', lead, className, fixedHeader = false, leftFixedColumnIndex, rightFixedColumnIndex, wrapperProps, loading = false, ...props }) {
15
15
  const classes = displayTableSlotRecipe({ fixedHeader });
16
- return (_jsx(DisplayTableProvider, { fixedHeader: fixedHeader, leftFixedColumnIndex: leftFixedColumnIndex, rightFixedColumnIndex: rightFixedColumnIndex, children: _jsxs("div", { ...wrapperProps, className: cx(classes.wrapper, 'mfui-DisplayTable__wrapper', wrapperProps?.className), children: [_jsxs("div", { className: cx(classes.header, 'mfui-DisplayTable__header'), children: [_jsx(Heading, { variant: "sectionHeading2", tag: titleTag, className: cx(classes.title, 'mfui-DisplayTable__title'), children: title }), lead ? (_jsx(Typography, { tag: "p", className: cx(classes.lead, 'mfui-DisplayTable__lead'), children: lead })) : null] }), _jsx("div", { className: cx(classes.body, 'mfui-DisplayTable__body'), children: _jsx("table", { className: cx(classes.table, 'mfui-DisplayTable__table', className), ...props }) })] }) }));
16
+ return (_jsx(DisplayTableProvider, { fixedHeader: fixedHeader, leftFixedColumnIndex: leftFixedColumnIndex, rightFixedColumnIndex: rightFixedColumnIndex, loading: loading, children: _jsxs("div", { ...wrapperProps, className: cx(classes.wrapper, 'mfui-DisplayTable__wrapper', wrapperProps?.className), children: [_jsxs("div", { className: cx(classes.header, 'mfui-DisplayTable__header'), children: [_jsx(Heading, { variant: "sectionHeading2", tag: titleTag, className: cx(classes.title, 'mfui-DisplayTable__title'), children: title }), lead ? (_jsx(Typography, { tag: "p", className: cx(classes.lead, 'mfui-DisplayTable__lead'), children: lead })) : null] }), _jsx("div", { className: cx(classes.body, 'mfui-DisplayTable__body'), children: _jsx("table", { className: cx(classes.table, 'mfui-DisplayTable__table', className), "aria-live": "polite", "aria-busy": loading, "aria-label": loading ? 'データを読み込み中' : undefined, ...props }) })] }) }));
17
17
  }
18
18
  /**
19
19
  * The `DisplayTable` component.
@@ -26,12 +26,13 @@ function Base({ title, titleTag = 'h4', lead, className, fixedHeader = false, le
26
26
  * @example
27
27
  * ```jsx
28
28
  * <DisplayTable title="My Table" lead="Table lead text">
29
+ * <DisplayTable.Header>
29
30
  * <DisplayTable.HeaderRow>
30
31
  * <DisplayTable.HeaderCell>Header 1</DisplayTable.HeaderCell>
31
32
  * <DisplayTable.HeaderCell>Header 2</DisplayTable.HeaderCell>
32
33
  * </DisplayTable.HeaderRow>
33
34
  * </DisplayTable.Header>
34
- * </DisplayTable.Body>
35
+ * <DisplayTable.Body>
35
36
  * <DisplayTable.Row>
36
37
  * <DisplayTable.Cell>Cell 1</DisplayTable.Cell>
37
38
  * <DisplayTable.Cell>Cell 2</DisplayTable.Cell>
@@ -74,4 +74,11 @@ export type DisplayTableProps = {
74
74
  * @property className - The class name of the wrapper element.
75
75
  */
76
76
  wrapperProps?: Pick<ComponentPropsWithoutRef<'div'>, 'className'>;
77
+ /**
78
+ * Whether the table is loading.
79
+ * If `true`, four rows with the skeleton cell will be shown instead of the table content.
80
+ *
81
+ * @default false
82
+ */
83
+ loading?: boolean;
77
84
  } & ComponentPropsWithoutRef<'table'>;
@@ -1,7 +1,22 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useMemo } from 'react';
2
3
  import { displayTableBodySlotRecipe } from '../../../styled-system/recipes';
3
4
  import { cx } from '../../../styled-system/css';
5
+ import { useDisplayTableContext } from '../DisplayTableProvider';
6
+ import { DisplayTableRow } from '../DisplayTableRow';
7
+ import { DisplayTableCell } from '../DisplayTableCell';
8
+ const NUMBER_OF_LOADING_ROWS = 4;
4
9
  export function DisplayTableBody({ className, ...props }) {
10
+ const { numberColumns, loading } = useDisplayTableContext();
5
11
  const classes = displayTableBodySlotRecipe();
12
+ // Memoize loading rows to avoid recreating arrays on every render
13
+ const loadingRows = useMemo(() => {
14
+ if (!loading)
15
+ return null;
16
+ return Array.from({ length: NUMBER_OF_LOADING_ROWS }).map((_, rowIndex) => (_jsx(DisplayTableRow, { children: Array.from({ length: numberColumns }).map((_, colIndex) => (_jsx(DisplayTableCell, { columnIndex: colIndex }, colIndex))) }, rowIndex)));
17
+ }, [loading, numberColumns]);
18
+ if (loading) {
19
+ return (_jsx("tbody", { className: cx(classes.root, 'mfui-DisplayTableBody__root', className), ...props, children: loadingRows }));
20
+ }
6
21
  return _jsx("tbody", { className: cx(classes.root, 'mfui-DisplayTableBody__root', className), ...props });
7
22
  }
@@ -1,11 +1,12 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { displayTableCellSlotRecipe } from '../../../styled-system/recipes';
3
3
  import { Typography } from '../../Typography';
4
+ import { Skeleton } from '../../Skeleton';
4
5
  import { useDisplayTableContext } from '../DisplayTableProvider';
5
6
  import { cx } from '../../../styled-system/css';
6
7
  import { useFixedColumns } from '../../utilities/dom/useFixedColumns';
7
8
  export function DisplayTableCell({ children, className, depth = '0', columnIndex, style, type = 'text', isTotal = false, ...props }) {
8
- const { leftFixedColumnIndex, rightFixedColumnIndex } = useDisplayTableContext();
9
+ const { leftFixedColumnIndex, rightFixedColumnIndex, loading } = useDisplayTableContext();
9
10
  const { cellRef, isFixedColumn, isEdgeFixedColumn, fixedPositionStyles } = useFixedColumns({
10
11
  columnIndex,
11
12
  leftFixedColumnIndex,
@@ -27,5 +28,11 @@ export function DisplayTableCell({ children, className, depth = '0', columnIndex
27
28
  // Row headers are fixed columns on the left side
28
29
  const isRowHeader = isFixedColumn && isEdgeFixedColumn === 'left';
29
30
  const Tag = isRowHeader ? 'th' : 'td';
30
- return (_jsx(Tag, { ref: cellRef, className: cx(classes.root, 'mfui-DisplayTableCell__root', className), style: { ...style, ...fixedPositionStyles }, ...props, children: _jsx(Typography, { variant: getTypographyVariant(), children: children }) }));
31
+ const cellContent = (() => {
32
+ if (loading) {
33
+ return (_jsx("div", { className: cx(classes.skeletonCell, 'mfui-DisplayTableCell__skeletonCell'), children: _jsx(Skeleton, {}) }));
34
+ }
35
+ return _jsx(Typography, { variant: getTypographyVariant(), children: children });
36
+ })();
37
+ return (_jsx(Tag, { ref: cellRef, className: cx(classes.root, 'mfui-DisplayTableCell__root', className), style: { ...style, ...fixedPositionStyles }, ...props, children: cellContent }));
31
38
  }
@@ -1,11 +1,22 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Children, cloneElement, isValidElement } from 'react';
2
+ import { Children, cloneElement, isValidElement, useMemo, useEffect } from 'react';
3
3
  import { displayTableHeaderRowSlotRecipe } from '../../../styled-system/recipes';
4
4
  import { DisplayTableHeaderCell } from '../DisplayTableHeaderCell';
5
5
  import { cx } from '../../../styled-system/css';
6
6
  import { isComponentOrWrapped } from '../../utilities/react/isComponentOrWrapped';
7
+ import { useDisplayTableContext } from '../DisplayTableProvider';
7
8
  export function DisplayTableHeaderRow({ children, className, ...props }) {
9
+ const { setNumberColumns } = useDisplayTableContext();
8
10
  const classes = displayTableHeaderRowSlotRecipe();
11
+ // Count the number of header cells
12
+ const numberOfColumns = useMemo(() => {
13
+ const headerCells = Children.toArray(children).filter((child) => isValidElement(child) && isComponentOrWrapped(child, DisplayTableHeaderCell));
14
+ return headerCells.length;
15
+ }, [children]);
16
+ // Update context with the number of columns
17
+ useEffect(() => {
18
+ setNumberColumns(numberOfColumns);
19
+ }, [numberOfColumns, setNumberColumns]);
9
20
  return (_jsx("tr", { className: cx(classes.root, 'mfui-DisplayTableHeaderRow__root', className), ...props, children: Children.map(children, (child, index) => {
10
21
  if (!isValidElement(child)) {
11
22
  return child;
@@ -3,20 +3,24 @@ import { type DisplayTableProps } from './DisplayTable.types';
3
3
  /**
4
4
  * The props for the `DisplayTableContext` component.
5
5
  */
6
- export type DisplayTableContextProps = Pick<DisplayTableProps, 'fixedHeader' | 'leftFixedColumnIndex' | 'rightFixedColumnIndex'>;
6
+ export type DisplayTableContextProps = Pick<DisplayTableProps, 'fixedHeader' | 'leftFixedColumnIndex' | 'rightFixedColumnIndex' | 'loading'>;
7
+ export type DisplayTableProviderValues = {
8
+ numberColumns: number;
9
+ setNumberColumns: (number: number) => void;
10
+ } & DisplayTableContextProps;
7
11
  /**
8
12
  * The context for the `DisplayTable` component.
9
13
  *
10
14
  * This should only be used in DisplayTable component.
11
15
  */
12
- export declare const DisplayTableContext: import("react").Context<DisplayTableContextProps>;
16
+ export declare const DisplayTableContext: import("react").Context<DisplayTableProviderValues>;
13
17
  /**
14
18
  * The hook to use the `DisplayTableContext`.
15
19
  */
16
- export declare const useDisplayTableContext: () => DisplayTableContextProps;
20
+ export declare const useDisplayTableContext: () => DisplayTableProviderValues;
17
21
  /**
18
22
  * The provider for the `DisplayTableContext`.
19
23
  *
20
24
  * This should only be used in DisplayTable component.
21
25
  */
22
- export declare function DisplayTableProvider({ children, fixedHeader, leftFixedColumnIndex, rightFixedColumnIndex, }: PropsWithChildren<DisplayTableContextProps>): import("react/jsx-runtime").JSX.Element;
26
+ export declare function DisplayTableProvider({ children, fixedHeader, leftFixedColumnIndex, rightFixedColumnIndex, loading, }: PropsWithChildren<DisplayTableContextProps>): import("react/jsx-runtime").JSX.Element;
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { createContext, useContext, useMemo } from 'react';
3
+ import { createContext, useContext, useMemo, useState } from 'react';
4
4
  /**
5
5
  * The context for the `DisplayTable` component.
6
6
  *
@@ -10,6 +10,11 @@ export const DisplayTableContext = createContext({
10
10
  fixedHeader: false,
11
11
  leftFixedColumnIndex: undefined,
12
12
  rightFixedColumnIndex: undefined,
13
+ loading: false,
14
+ numberColumns: 0,
15
+ setNumberColumns: () => {
16
+ // Do nothing
17
+ },
13
18
  });
14
19
  /**
15
20
  * The hook to use the `DisplayTableContext`.
@@ -20,11 +25,15 @@ export const useDisplayTableContext = () => useContext(DisplayTableContext);
20
25
  *
21
26
  * This should only be used in DisplayTable component.
22
27
  */
23
- export function DisplayTableProvider({ children, fixedHeader = false, leftFixedColumnIndex, rightFixedColumnIndex, }) {
28
+ export function DisplayTableProvider({ children, fixedHeader = false, leftFixedColumnIndex, rightFixedColumnIndex, loading = false, }) {
29
+ const [numberColumns, setNumberColumns] = useState(0);
24
30
  const contextValue = useMemo(() => ({
25
31
  fixedHeader,
26
32
  leftFixedColumnIndex,
27
33
  rightFixedColumnIndex,
28
- }), [fixedHeader, leftFixedColumnIndex, rightFixedColumnIndex]);
34
+ loading,
35
+ numberColumns,
36
+ setNumberColumns,
37
+ }), [fixedHeader, leftFixedColumnIndex, rightFixedColumnIndex, loading, numberColumns]);
29
38
  return _jsx(DisplayTableContext.Provider, { value: contextValue, children: children });
30
39
  }
@@ -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, targetDOMNode, name, onChange, value, showDisplayValueAsTag, renderDisplayValue, enableSearchOptions, notFoundMessage, searchBoxProps, loading, onSearchOptions, clearButtonProps, disableClearButton, optionPanelProps, enableAllOptionsControls, enableApplyControls, selectAllButtonProps, clearAllButtonProps, selectedCountProps, cancelButtonProps, applyButtonProps, renderOption, onOpenStateChanged, enableAutoUnmount, onBlur, showGroupOptionDivider, enableVirtualization, virtualizationOptions, }: 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, targetDOMNode, name, onChange, value, showDisplayValueAsTag, renderDisplayValue, enableSearchOptions, notFoundMessage, searchBoxProps, loading, onSearchOptions, clearButtonProps, disableClearButton, optionPanelProps, popoverWrapperProps, enableAllOptionsControls, enableApplyControls, selectAllButtonProps, clearAllButtonProps, selectedCountProps, cancelButtonProps, applyButtonProps, renderOption, onOpenStateChanged, enableAutoUnmount, onBlur, showGroupOptionDivider, enableVirtualization, virtualizationOptions, infiniteScroll, }: MultipleSelectBoxProps<T, AdditionalProps>): import("react/jsx-runtime").JSX.Element;
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useCallback, useId, useMemo, useRef } from 'react';
4
- import { CheckboxChecked, CheckboxUnchecked } from '@moneyforward/mfui-icons-react';
4
+ import { CheckboxChecked, CheckboxUnchecked, Error } from '@moneyforward/mfui-icons-react';
5
5
  import { FocusIndicator } from '../FocusIndicator';
6
6
  import { Typography } from '../Typography';
7
7
  import { useFocusTrap } from '../utilities/dom/useFocusTrap';
@@ -13,6 +13,7 @@ import { SearchBox } from '../SearchBox';
13
13
  import { Button } from '../Button';
14
14
  import { HStack, VStack } from '../Stack';
15
15
  import { Skeleton } from '../Skeleton';
16
+ import { ProgressIndicator } from '../ProgressIndicator';
16
17
  import { useInitialFocusOnOptionPanelOpen } from './hooks/useInitialFocusOnOptionPanelOpen';
17
18
  import { useSelectedValues } from './hooks/useSelectedValues';
18
19
  import { useOptionKeyboardNavigation } from './hooks/useOptionKeyboardNavigation';
@@ -23,6 +24,7 @@ import { cx } from '../../styled-system/css';
23
24
  import { flattenOptions } from './utils/flattenOptions';
24
25
  import { isSelectableOption, isOptionDisabled, isOptionSelected } from './utils/isSelectableOption';
25
26
  import { useVirtualizedMultipleSelectBoxOptions } from './hooks/useVirtualizedMultipleSelectBoxOptions';
27
+ import { useInfiniteScroll } from '../utilities/dom/useInfiniteScroll';
26
28
  // How many skeleton items to render while the component is loading.
27
29
  const SKELETON_ITEM_COUNT = 4;
28
30
  /**
@@ -32,12 +34,13 @@ const SKELETON_ITEM_COUNT = 4;
32
34
  */
33
35
  export function MultipleSelectBox({ id, triggerProps, triggerWrapperProps, size, options = [], defaultValue,
34
36
  // eslint-disable-next-line @typescript-eslint/no-deprecated
35
- placeholder, emptyMessage, disabled, invalid, targetDOMNode, name, onChange, value, showDisplayValueAsTag, renderDisplayValue, enableSearchOptions = false, notFoundMessage, searchBoxProps, loading = false, onSearchOptions, clearButtonProps, disableClearButton = false, optionPanelProps, enableAllOptionsControls = false, enableApplyControls = false, selectAllButtonProps, clearAllButtonProps, selectedCountProps, cancelButtonProps, applyButtonProps, renderOption, onOpenStateChanged, enableAutoUnmount = true, onBlur, showGroupOptionDivider, enableVirtualization = false, virtualizationOptions, }) {
37
+ placeholder, emptyMessage, disabled, invalid, targetDOMNode, name, onChange, value, showDisplayValueAsTag, renderDisplayValue, enableSearchOptions = false, notFoundMessage, searchBoxProps, loading = false, onSearchOptions, clearButtonProps, disableClearButton = false, optionPanelProps, popoverWrapperProps, enableAllOptionsControls = false, enableApplyControls = false, selectAllButtonProps, clearAllButtonProps, selectedCountProps, cancelButtonProps, applyButtonProps, renderOption, onOpenStateChanged, enableAutoUnmount = true, onBlur, showGroupOptionDivider, enableVirtualization = false, virtualizationOptions, infiniteScroll, }) {
36
38
  const classes = multipleSelectBoxSlotRecipe({ showGroupOptionDivider });
37
39
  const triggerRef = useRef(null);
38
40
  const listBoxId = useId();
39
41
  const optionPanelRef = useRef(null);
40
42
  const listBoxRef = useRef(null);
43
+ const scrollWrapperRef = useRef(null);
41
44
  const searchInputRef = useRef(null);
42
45
  const selectAllButtonRef = useRef(null);
43
46
  const clearAllButtonRef = useRef(null);
@@ -71,11 +74,30 @@ placeholder, emptyMessage, disabled, invalid, targetDOMNode, name, onChange, val
71
74
  // Initialize virtualization
72
75
  const { virtualizer, virtualItems, totalSize, isVirtualized } = useVirtualizedMultipleSelectBoxOptions({
73
76
  enabled: shouldVirtualize,
74
- parentRef: listBoxRef,
77
+ parentRef: scrollWrapperRef,
75
78
  flatOptions: filteredOptions.filter((option) => 'value' in option),
76
79
  estimateSize: virtualizationOptions?.estimateSize,
77
80
  overscan: virtualizationOptions?.overscan,
78
81
  });
82
+ // Extract infinite scroll configuration with defaults
83
+ const baseEnabledInfiniteScroll = infiniteScroll?.enabled ?? false;
84
+ const onLoadMore = infiniteScroll?.onLoadMore;
85
+ const hasNextPage = infiniteScroll?.hasNextPage ?? true;
86
+ const hasPreviousPage = infiniteScroll?.hasPreviousPage ?? true;
87
+ const infiniteScrollThreshold = infiniteScroll?.threshold ?? 100;
88
+ const infiniteScrollErrorMessage = infiniteScroll?.errorMessage ?? '読み込みに失敗しました';
89
+ const infiniteScrollRetryButtonText = infiniteScroll?.retryButtonText ?? '再読み込み';
90
+ // Initialize infinite scroll
91
+ const { isLoading: isInfiniteScrollLoading, error: infiniteScrollError, handleScroll: handleInfiniteScroll, retryLoad: retryInfiniteScroll, } = useInfiniteScroll({
92
+ onLoadMore,
93
+ hasNextPage,
94
+ hasPreviousPage,
95
+ }, {
96
+ enabled: baseEnabledInfiniteScroll,
97
+ threshold: infiniteScrollThreshold,
98
+ });
99
+ // Disable infinite scroll when there's an error
100
+ const enableInfiniteScroll = baseEnabledInfiniteScroll && !infiniteScrollError;
79
101
  const { temporaryValues, updateTemporaryValues, initializeTemporaryValues, handleCancelButtonClick, handleApplyButtonClick, } = useApplyControls({
80
102
  onValuesChange: updateSelectedValues,
81
103
  closeOptionPanel,
@@ -262,18 +284,46 @@ placeholder, emptyMessage, disabled, invalid, targetDOMNode, name, onChange, val
262
284
  handleOptionFocus,
263
285
  tabbableOptionIndex,
264
286
  ]);
287
+ // Render infinite scroll error message with retry button
288
+ const renderInfiniteScrollError = useCallback(() => infiniteScrollError ? (_jsxs("li", { 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) => {
289
+ event.stopPropagation();
290
+ retryInfiniteScroll();
291
+ }, children: infiniteScrollRetryButtonText }) })] })) : null, [
292
+ infiniteScrollError,
293
+ classes.infiniteScrollError,
294
+ classes.infiniteScrollErrorIcon,
295
+ classes.infiniteScrollErrorMessage,
296
+ classes.infiniteScrollErrorButton,
297
+ retryInfiniteScroll,
298
+ infiniteScrollErrorMessage,
299
+ infiniteScrollRetryButtonText,
300
+ ]);
301
+ // Render infinite scroll loading indicator
302
+ const renderInfiniteScrollLoading = useCallback(() => isInfiniteScrollLoading && enableInfiniteScroll ? (_jsx("div", { className: cx(classes.infiniteScrollLoading, 'mfui-MultipleSelectBox__infiniteScrollLoading'), children: _jsx(ProgressIndicator, {}) })) : null, [isInfiniteScrollLoading, enableInfiniteScroll, classes.infiniteScrollLoading]);
265
303
  return (_jsx(Popover, { renderTrigger: ({ setTriggerRef, togglePopover, handleTriggerKeyDown, handleTriggerBlur }) => (_jsx(MultipleSelectBoxTrigger, { ref: triggerRef, wrapperRef: setTriggerRef, selectedOptions: localSelectedOptions, id: id, disabled: disabled, triggerProps: triggerProps, triggerWrapperProps: triggerWrapperProps, name: name,
266
304
  // eslint-disable-next-line @typescript-eslint/no-deprecated
267
- placeholder: placeholder, size: size, listBoxId: listBoxId, isOptionPanelOpen: isOptionPanelOpen, invalid: invalid, showDisplayValueAsTag: showDisplayValueAsTag, renderDisplayValue: renderDisplayValue, clearButtonProps: clearButtonProps, disableClearButton: disableClearButton, updateSelectedValues: updateSelectedValues, onClick: togglePopover, onKeyDown: handleTriggerKeyDown, onBlur: handleTriggerBlur })), contentProps: { className: classes.popover }, minWidth: optionPanelProps?.minWidth, allowedPlacements: optionPanelProps?.allowedPlacements, renderContent: () => (_jsxs("div", { ref: optionPanelRef, className: cx(classes.optionPanel, 'mfui-MultipleSelectBox__optionPanel', optionPanelProps?.className), tabIndex: -1, onKeyDown: handleKeyDownMenu, children: [enableSearchOptions || enableAllOptionsControls ? (_jsxs(VStack, { className: cx(classes.menuHeader, 'mfui-MultipleSelectBox__menuHeader'), gap: "8px", children: [enableSearchOptions ? (_jsx(SearchBox, { ref: searchInputRef, enableClearButton: true, value: searchText, textBoxSize: "small", autoComplete: "off", onChange: onSearchTextChange, ...searchBoxProps })) : null, 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] })) : null, _jsx("ul", { ref: listBoxRef, role: "listbox", id: listBoxId, className: cx(classes.listBox, 'mfui-MultipleSelectBox__listBox'), tabIndex: -1, style: isVirtualized && totalSize > 0
268
- ? {
269
- height: `${String(totalSize)}px`,
270
- position: 'relative',
271
- }
272
- : undefined, children: loading ? (Array.from({ length: SKELETON_ITEM_COUNT }).map((_, index) => (_jsx("li", { className: cx(classes.skeletonItem, 'mfui-MultipleSelectBox__skeletonItem'), children: _jsx(Skeleton, {}) }, index)))) : isVirtualized && virtualItems.length > 0 ? (
273
- // Virtualized rendering with group support
274
- renderVirtualizedItems()) : filteredOptions.length > 0 ? (renderNonVirtualizedItems()) : (_jsx("li", { className: cx(classes.emptyMessage, 'mfui-MultipleSelectBox__emptyMessage'), children: _jsx(Typography, { variant: "body", children: enableSearchOptions && searchText && notFoundMessage
275
- ? notFoundMessage
276
- : options.length > 0
277
- ? notFoundMessage
278
- : emptyMessage }) })) }), enableApplyControls ? (_jsxs(HStack, { className: cx(classes.menuFooter, 'mfui-MultipleSelectBox__menuFooter'), justifyContent: "space-between", alignItems: "center", children: [_jsx(Typography, { variant: "body", color: "neutral.600", children: selectedCountProps?.render?.(temporaryValues.size) ?? `${String(temporaryValues.size)}件選択中` }), _jsxs(HStack, { gap: "horizontal.0-1of2", children: [_jsx(Button, { ref: cancelButtonRef, size: "small", priority: "secondary", onClick: handleCancelButtonClick, children: cancelButtonProps?.label ?? 'キャンセル' }), _jsx(Button, { ref: applyButtonRef, size: "small", priority: "primary", onClick: handleApplyButtonClick, children: applyButtonProps?.label ?? '適用' })] })] })) : null] })), open: isOptionPanelOpen, targetDOMNode: targetDOMNode, enableAutoUnmount: enableAutoUnmount, onOpenStateChanged: toggleOptionPanel, onBlur: onBlur }));
305
+ placeholder: placeholder, size: size, listBoxId: listBoxId, isOptionPanelOpen: isOptionPanelOpen, invalid: invalid, showDisplayValueAsTag: showDisplayValueAsTag, renderDisplayValue: renderDisplayValue, clearButtonProps: clearButtonProps, disableClearButton: disableClearButton, updateSelectedValues: updateSelectedValues, onClick: togglePopover, onKeyDown: handleTriggerKeyDown, onBlur: handleTriggerBlur })), contentProps: { className: classes.popover }, minWidth: optionPanelProps?.minWidth, maxHeight: popoverWrapperProps?.maxHeight, allowedPlacements: optionPanelProps?.allowedPlacements, renderContent: () => (_jsxs("div", { ref: optionPanelRef, className: cx(classes.optionPanel, 'mfui-MultipleSelectBox__optionPanel', optionPanelProps?.className), tabIndex: -1, onKeyDown: handleKeyDownMenu, children: [enableSearchOptions || enableAllOptionsControls ? (_jsxs(VStack, { className: cx(classes.menuHeader, 'mfui-MultipleSelectBox__menuHeader'), gap: "8px", children: [enableSearchOptions ? (_jsx(SearchBox, { ref: searchInputRef, enableClearButton: true, value: searchText, textBoxSize: "small", autoComplete: "off", onChange: onSearchTextChange, ...searchBoxProps })) : null, 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] })) : null, _jsx("div", { ref: scrollWrapperRef, className: cx(classes.scrollWrapper, 'mfui-MultipleSelectBox__scrollWrapper'), onScroll: enableInfiniteScroll ? handleInfiniteScroll : undefined, children: _jsx("ul", { ref: listBoxRef, role: "listbox", id: listBoxId, className: cx(classes.listBox, 'mfui-MultipleSelectBox__listBox'), tabIndex: -1, style: isVirtualized && totalSize > 0
306
+ ? {
307
+ height: `${String(totalSize)}px`,
308
+ position: 'relative',
309
+ }
310
+ : undefined, children: loading
311
+ ? Array.from({ length: SKELETON_ITEM_COUNT }).map((_, index) => (_jsx("li", { className: cx(classes.skeletonItem, 'mfui-MultipleSelectBox__skeletonItem'), children: _jsx(Skeleton, {}) }, index)))
312
+ : isVirtualized && virtualItems.length > 0
313
+ ? // Virtualized rendering with group support
314
+ [...renderVirtualizedItems(), renderInfiniteScrollLoading(), renderInfiniteScrollError()].filter(Boolean)
315
+ : filteredOptions.length > 0
316
+ ? [
317
+ ...renderNonVirtualizedItems(),
318
+ renderInfiniteScrollLoading(),
319
+ renderInfiniteScrollError(),
320
+ ].filter(Boolean)
321
+ : [
322
+ _jsx("li", { className: cx(classes.emptyMessage, 'mfui-MultipleSelectBox__emptyMessage'), children: _jsx(Typography, { variant: "body", children: enableSearchOptions && searchText && notFoundMessage
323
+ ? notFoundMessage
324
+ : options.length > 0
325
+ ? notFoundMessage
326
+ : emptyMessage }) }, "empty"),
327
+ renderInfiniteScrollError(),
328
+ ].filter(Boolean) }) }), enableApplyControls ? (_jsxs(HStack, { className: cx(classes.menuFooter, 'mfui-MultipleSelectBox__menuFooter'), justifyContent: "space-between", alignItems: "center", children: [_jsx(Typography, { variant: "body", color: "neutral.600", children: selectedCountProps?.render?.(temporaryValues.size) ?? `${String(temporaryValues.size)}件選択中` }), _jsxs(HStack, { gap: "horizontal.0-1of2", children: [_jsx(Button, { ref: cancelButtonRef, size: "small", priority: "secondary", onClick: handleCancelButtonClick, children: cancelButtonProps?.label ?? 'キャンセル' }), _jsx(Button, { ref: applyButtonRef, size: "small", priority: "primary", onClick: handleApplyButtonClick, children: applyButtonProps?.label ?? '適用' })] })] })) : null] })), open: isOptionPanelOpen, targetDOMNode: targetDOMNode, enableAutoUnmount: enableAutoUnmount, onOpenStateChanged: toggleOptionPanel, onBlur: onBlur }));
279
329
  }
@@ -3,8 +3,61 @@ import { type VirtualizerOptions } from '@tanstack/react-virtual';
3
3
  import { type SearchBoxProps } from '../SearchBox';
4
4
  import { type MultipleSelectBoxTriggerProps, type PassedProps } from './MultipleSelectBoxTrigger/MultipleSelectBoxTrigger.types';
5
5
  import { type PopoverProps } from '../Popover';
6
+ import { type InfiniteScrollDirection } from '../utilities/dom/useInfiniteScroll';
6
7
  export type AllowedValueTypes = string | number | undefined;
7
8
  export type VirtualOptionTypes = Pick<VirtualizerOptions<HTMLElement, Element>, 'estimateSize' | 'overscan'>;
9
+ export type InfiniteScrollConfig = {
10
+ /**
11
+ * Enable infinite scroll functionality.
12
+ * When enabled, additional options can be loaded dynamically when scrolling.
13
+ *
14
+ * @default false
15
+ */
16
+ enabled?: boolean;
17
+ /**
18
+ * Callback executed when more options need to be loaded.
19
+ * Called when user scrolls near the top or bottom of the options list.
20
+ *
21
+ * @param direction - The direction of loading ('forward' for bottom, 'backward' for top)
22
+ */
23
+ onLoadMore?: (direction: InfiniteScrollDirection) => Promise<void>;
24
+ /**
25
+ * Whether there are more options available to load in the forward direction (bottom).
26
+ * Used to determine if infinite scroll should trigger when scrolling down.
27
+ *
28
+ * @default true
29
+ */
30
+ hasNextPage?: boolean;
31
+ /**
32
+ * Whether there are more options available to load in the backward direction (top).
33
+ * Used to determine if infinite scroll should trigger when scrolling up.
34
+ *
35
+ * @default true
36
+ */
37
+ hasPreviousPage?: boolean;
38
+ /**
39
+ * The scroll threshold in pixels for triggering infinite scroll.
40
+ * When the scroll position is within this distance from the top or bottom,
41
+ * the onLoadMore callback will be triggered.
42
+ *
43
+ * @default 100
44
+ */
45
+ threshold?: number;
46
+ /**
47
+ * The error message to display when loading fails.
48
+ * This message supports internationalization.
49
+ *
50
+ * @default "読み込みに失敗しました"
51
+ */
52
+ errorMessage?: string;
53
+ /**
54
+ * The text for the retry button when loading fails.
55
+ * This message supports internationalization.
56
+ *
57
+ * @default "再読み込み"
58
+ */
59
+ retryButtonText?: string;
60
+ };
8
61
  export type MultipleSelectBoxOption<T extends AllowedValueTypes = string, AdditionalProps extends Record<string, unknown> = Record<string, never>> = ({
9
62
  /**
10
63
  * The label to be displayed in the list.
@@ -253,6 +306,14 @@ export type MultipleSelectBoxProps<T extends AllowedValueTypes = string, Additio
253
306
  minWidth?: PopoverProps['minWidth'];
254
307
  allowedPlacements?: PopoverProps['allowedPlacements'];
255
308
  };
309
+ /**
310
+ * The properties for the popover wrapper element.
311
+ *
312
+ * @property maxHeight - The maximum height of the popover wrapper element.
313
+ */
314
+ popoverWrapperProps?: {
315
+ maxHeight?: PopoverProps['maxHeight'];
316
+ };
256
317
  /**
257
318
  * Flag to indicate loading state of the options.
258
319
  * When true, the MultipleSelectBox will display skeleton placeholders
@@ -334,4 +395,29 @@ export type MultipleSelectBoxProps<T extends AllowedValueTypes = string, Additio
334
395
  * ```
335
396
  */
336
397
  virtualizationOptions?: VirtualOptionTypes;
398
+ /**
399
+ * Configuration for infinite scroll behavior.
400
+ * Allows loading additional options dynamically when scrolling.
401
+ *
402
+ * @example
403
+ * ```tsx
404
+ * <MultipleSelectBox
405
+ * infiniteScroll={{
406
+ * enabled: true,
407
+ * onLoadMore: async (direction) => {
408
+ * // Load more options from API
409
+ * const newOptions = await fetchOptions(direction);
410
+ * setOptions(prev => direction === 'forward' ?
411
+ * [...prev, ...newOptions] :
412
+ * [...newOptions, ...prev]
413
+ * );
414
+ * },
415
+ * hasNextPage: true,
416
+ * threshold: 100
417
+ * }}
418
+ * />
419
+ * ```
420
+ */
421
+ infiniteScroll?: InfiniteScrollConfig;
337
422
  } & Pick<MultipleSelectBoxTriggerProps<T, AdditionalProps>, keyof PassedProps<T, AdditionalProps>>;
423
+ export { type InfiniteScrollDirection } from '../utilities/dom/useInfiniteScroll';