@moneyforward/mfui-components 3.0.0 → 3.2.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 (42) hide show
  1. package/dist/src/DateTimeSelection/shared/BasePicker/BasePicker.d.ts +1 -1
  2. package/dist/src/DateTimeSelection/shared/BasePicker/BasePicker.js +4 -4
  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/FileBox/FileBox.js +2 -3
  8. package/dist/src/FileDropZone/FileDropZone.js +6 -7
  9. package/dist/src/Pagination/ItemsPerPage/ItemsPerPage.js +2 -2
  10. package/dist/src/Pagination/PagePicker/PagePicker.js +2 -2
  11. package/dist/src/Popover/Popover.js +1 -1
  12. package/dist/src/SelectBox/SelectBox.js +46 -8
  13. package/dist/src/SelectBox/SelectBox.types.d.ts +80 -1
  14. package/dist/src/SelectBox/hooks/useInfiniteScroll.d.ts +22 -0
  15. package/dist/src/SelectBox/hooks/useInfiniteScroll.js +65 -0
  16. package/dist/src/TextBox/TextBox.js +2 -2
  17. package/dist/src/ToggleSwitch/ToggleSwitch.d.ts +9 -0
  18. package/dist/src/ToggleSwitch/ToggleSwitch.js +32 -0
  19. package/dist/src/ToggleSwitch/ToggleSwitch.types.d.ts +6 -0
  20. package/dist/src/ToggleSwitch/ToggleSwitch.types.js +1 -0
  21. package/dist/src/ToggleSwitch/index.d.ts +2 -0
  22. package/dist/src/ToggleSwitch/index.js +2 -0
  23. package/dist/src/index.d.ts +1 -0
  24. package/dist/src/index.js +1 -0
  25. package/dist/src/shared/ClearButton/ClearButton.d.ts +4 -3
  26. package/dist/src/shared/ClearButton/ClearButton.js +14 -2
  27. package/dist/src/utilities/dom/getTargetDomNode.js +4 -2
  28. package/dist/src/utilities/dom/useFixedColumns.js +36 -10
  29. package/dist/styled-system/css/conditions.js +1 -1
  30. package/dist/styled-system/jsx/is-valid-prop.js +1 -1
  31. package/dist/styled-system/recipes/clear-button-slot-recipe.d.ts +36 -0
  32. package/dist/styled-system/recipes/clear-button-slot-recipe.js +38 -0
  33. package/dist/styled-system/recipes/index.d.ts +3 -1
  34. package/dist/styled-system/recipes/index.js +2 -0
  35. package/dist/styled-system/recipes/select-box-slot-recipe.d.ts +2 -2
  36. package/dist/styled-system/recipes/select-box-slot-recipe.js +22 -1
  37. package/dist/styled-system/recipes/toggle-switch-slot-recipe.d.ts +33 -0
  38. package/dist/styled-system/recipes/toggle-switch-slot-recipe.js +36 -0
  39. package/dist/styled-system/types/conditions.d.ts +10 -0
  40. package/dist/styles.css +256 -24
  41. package/dist/tsconfig.build.tsbuildinfo +1 -1
  42. package/package.json +1 -1
@@ -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);
@@ -93,7 +93,7 @@ export function BasePicker({ targetDOMNode, enableAutoUnmount = true, open = fal
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.
@@ -1,11 +1,10 @@
1
1
  'use client';
2
2
  import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { forwardRef, useRef, useState } from 'react';
4
- import { Close } from '@moneyforward/mfui-icons-react';
5
4
  import { cx } from '../../styled-system/css';
6
5
  import { fileBoxSlotRecipe } from '../../styled-system/recipes';
7
6
  import { FocusIndicator } from '../FocusIndicator';
8
- import { IconButton } from '../IconButton';
7
+ import { ClearButton } from '../shared/ClearButton';
9
8
  import { useIsTextOverflowed } from '../utilities/dom/useIsTextOverflowed';
10
9
  /**
11
10
  * The component for selecting a single file or multiple files for upload.
@@ -80,6 +79,6 @@ export const FileBox = forwardRef(({ invalid = false, disabled = false, classNam
80
79
  }, id: id, type: "file", accept: accept, multiple: multiple, disabled: disabled,
81
80
  // https://github.com/w3c/html-aria/issues/126
82
81
  // following Chrome's classification
83
- role: "button", className: cx(classes.input, 'mfui-FileBox__input'), onChange: handleChange, onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onDrop: handleDrop }, key), _jsx("div", { "data-mfui-content": "filebox-input-background", className: cx(classes.inputBackground, 'mfui-FileBox__inputBackground') }), _jsxs("div", { "data-mfui-content": "filebox-container", className: cx(classes.container, 'mfui-FileBox__container'), children: [_jsx("div", { "data-mfui-content": "filebox-button", ...props, className: cx(classes.button, 'mfui-FileBox__button', className), children: buttonLabel }), _jsxs("div", { "data-mfui-content": "filebox-file-info", className: cx(classes.fileInfo, 'mfui-FileBox__fileInfo'), children: [renderDisplayValues(), values !== null && values.length > 0 && (_jsx(IconButton, { "aria-label": clearButtonProps?.['aria-label'] ?? 'ファイルをクリアする', className: cx(classes.clearButton, 'mfui-FileBox__clearButton'), onClick: handleClear, children: _jsx(Close, {}) }))] })] })] }) }));
82
+ role: "button", className: cx(classes.input, 'mfui-FileBox__input'), onChange: handleChange, onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onDrop: handleDrop }, key), _jsx("div", { "data-mfui-content": "filebox-input-background", className: cx(classes.inputBackground, 'mfui-FileBox__inputBackground') }), _jsxs("div", { "data-mfui-content": "filebox-container", className: cx(classes.container, 'mfui-FileBox__container'), children: [_jsx("div", { "data-mfui-content": "filebox-button", ...props, className: cx(classes.button, 'mfui-FileBox__button', className), children: buttonLabel }), _jsxs("div", { "data-mfui-content": "filebox-file-info", className: cx(classes.fileInfo, 'mfui-FileBox__fileInfo'), children: [renderDisplayValues(), values !== null && values.length > 0 && (_jsx(ClearButton, { "aria-label": clearButtonProps?.['aria-label'] ?? 'ファイルをクリアする', className: cx(classes.clearButton, 'mfui-FileBox__clearButton'), onClick: handleClear }))] })] })] }) }));
84
83
  });
85
84
  FileBox.displayName = 'FileBox';
@@ -1,14 +1,14 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { forwardRef, useRef, useState, useEffect, useLayoutEffect, useCallback, useMemo } from 'react';
4
- import { Close } from '@moneyforward/mfui-icons-react';
5
4
  import { fileDropZoneSlotRecipe } from '../../styled-system/recipes';
6
5
  import { cx } from '../../styled-system/css';
7
6
  import { useMiddleTruncatedText } from '../utilities/dom/useMiddleTruncatedText';
8
- import { IconButton } from '../IconButton';
7
+ import { ClearButton } from '../shared/ClearButton';
9
8
  import { HelpMessage } from '../HelpMessage';
10
9
  import { Button } from '../Button';
11
10
  import { mergeRefs } from '../utilities/dom/mergeRefs';
11
+ import { useIsNarrowViewport } from '../utilities/responsive/useIsNarrowViewport';
12
12
  /**
13
13
  * The component for selecting a single file or multiple files for upload by dialog or drag and drop.
14
14
  *
@@ -59,8 +59,6 @@ fileSelectButtonLabel = 'ファイルを選択', instructionText, fileCountLabel
59
59
  ? screenReaderTexts.multiple(Number(data))
60
60
  : `選択済みファイル: ${String(data)}個のファイル`;
61
61
  };
62
- // Get clear button aria-label with priority: clearButtonProps['aria-label'] > default
63
- const getClearButtonAriaLabel = () => clearButtonProps?.['aria-label'] || 'ファイルをクリアする';
64
62
  const isControlled = value !== undefined;
65
63
  const [internalValues, setInternalValues] = useState(defaultValue);
66
64
  const [isDragging, setIsDragging] = useState(false);
@@ -72,8 +70,9 @@ fileSelectButtonLabel = 'ファイルを選択', instructionText, fileCountLabel
72
70
  const previousValueRef = useRef(value);
73
71
  const isUserInitiatedClearRef = useRef(false);
74
72
  const displayValues = isControlled ? value : internalValues;
75
- const CLEAR_BUTTON_WIDTH = 24; // Width of clear button to subtract from available space
76
- const truncatedSingleName = useMiddleTruncatedText(displayValues?.[0] ?? '', fileNameRef, CLEAR_BUTTON_WIDTH);
73
+ const isNarrowViewport = useIsNarrowViewport();
74
+ const clearButtonWidth = isNarrowViewport ? 32 : 24;
75
+ const truncatedSingleName = useMiddleTruncatedText(displayValues?.[0] ?? '', fileNameRef, clearButtonWidth);
77
76
  const classes = fileDropZoneSlotRecipe({ isDragging, invalid });
78
77
  const gap = 7; // From spacing.mfui.size.spacing.inline.horizontal.comfort token value
79
78
  const fileInfoWidth = useMemo(() => {
@@ -209,7 +208,7 @@ fileSelectButtonLabel = 'ファイルを選択', instructionText, fileCountLabel
209
208
  }, [displayValues, truncatedSingleName, classes.span, getFileCountText]);
210
209
  return (_jsxs("div", { className: cx(classes.root, 'mfui-FileDropZone__root'), children: [_jsx("input", { ref: mergeRefs(ref, inputRef), id: id, type: "file", disabled: disabled, multiple: multiple, accept: accept, className: cx(classes.input, 'mfui-FileDropZone__input'), tabIndex: -1, "aria-hidden": "true", onChange: handleChange, onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onDrop: handleDrop, ...props }, key), _jsxs("div", { "data-mfui-content": "file-drop-zone-container", className: cx(classes.container, 'mfui-FileDropZone__container', className), style: style, onClick: handleContainerClick, onDragEnter: handleDragEnter, onDragLeave: handleDragLeave, onDragOver: handleDragOver, onDrop: handleDrop, children: [illustrationUrl ? (_jsx("div", { "data-mfui-content": "file-drop-zone-illustration", className: cx(classes.illustration, 'mfui-FileDropZone__illustration'), children: _jsx("img", { src: illustrationUrl, alt: "illustration" }) })) : null, _jsxs("div", { "data-mfui-content": "file-drop-zone-main-container", className: cx(classes.mainContainer, 'mfui-FileDropZone__mainContainer'), style: { maxWidth: fileBoxWidth }, children: [_jsx("div", { "data-mfui-content": "file-drop-zone-instruction", className: cx(classes.instruction, 'mfui-FileDropZone__instruction'), children: _jsx("p", { className: cx(classes.phrase, 'mfui-FileDropZone__phrase'), children: normalizedInstructionText }) }), _jsxs("div", { "data-mfui-content": "file-drop-zone-button-container", className: cx(classes.buttonContainer, 'mfui-FileDropZone__buttonContainer'), children: [_jsx(Button, { ref: buttonRef, className: cx(classes.button, 'mfui-FileDropZone__button'), disabled: disabled, "aria-describedby": errorMessage || helperMessage || (displayValues && displayValues.length > 0)
211
210
  ? `${id ? `${id}-` : ''}file-drop-zone-messages`
212
- : undefined, onClick: handleFileInputTrigger, children: fileSelectButtonLabel }), displayValues !== undefined && displayValues.length > 0 && (_jsxs("div", { "data-mfui-content": "file-drop-zone-file-info", className: cx(classes.fileInfo, 'mfui-FileDropZone__fileInfo'), style: { maxWidth: `${(fileInfoWidth || 147).toString()}px` }, children: [renderDisplayValues, _jsx(IconButton, { ...clearButtonProps, "aria-label": getClearButtonAriaLabel(), className: cx(classes.clearButton, 'mfui-FileDropZone__clearButton'), disabled: disabled, onClick: handleClear, children: _jsx(Close, {}) })] }))] }), _jsxs("div", { id: id ? `${id}-file-drop-zone-messages` : 'file-drop-zone-messages', "data-mfui-content": "file-drop-zone-help-message", className: cx(classes.helpMessage, 'mfui-FileDropZone__helpMessage'), children: [errorMessage ? (_jsx(HelpMessage, { messageType: "error", messageAlign: "center", children: errorMessage })) : null, helperMessage ? _jsx(HelpMessage, { messageAlign: "center", children: helperMessage }) : null, displayValues && displayValues.length > 0 ? (_jsx("div", { className: "sr-only", children: displayValues.length === 1
211
+ : undefined, onClick: handleFileInputTrigger, children: fileSelectButtonLabel }), displayValues !== undefined && displayValues.length > 0 && (_jsxs("div", { "data-mfui-content": "file-drop-zone-file-info", className: cx(classes.fileInfo, 'mfui-FileDropZone__fileInfo'), style: { maxWidth: `${(fileInfoWidth || 147).toString()}px` }, children: [renderDisplayValues, _jsx(ClearButton, { ...clearButtonProps, "aria-label": clearButtonProps?.['aria-label'] ?? 'ファイルをクリアする', className: cx(classes.clearButton, 'mfui-FileDropZone__clearButton'), disabled: disabled, onClick: handleClear })] }))] }), _jsxs("div", { id: id ? `${id}-file-drop-zone-messages` : 'file-drop-zone-messages', "data-mfui-content": "file-drop-zone-help-message", className: cx(classes.helpMessage, 'mfui-FileDropZone__helpMessage'), children: [errorMessage ? (_jsx(HelpMessage, { messageType: "error", messageAlign: "center", children: errorMessage })) : null, helperMessage ? _jsx(HelpMessage, { messageAlign: "center", children: helperMessage }) : null, displayValues && displayValues.length > 0 ? (_jsx("div", { className: "sr-only", children: displayValues.length === 1
213
212
  ? getScreenReaderText('single', displayValues[0] || '')
214
213
  : getScreenReaderText('multiple', displayValues.length) })) : null] })] }), extraElement ? (_jsx("div", { "data-mfui-content": "file-drop-zone-extra", className: cx(classes.extra, 'mfui-FileDropZone__extra'), children: extraElement })) : null] })] }));
215
214
  });
@@ -13,7 +13,7 @@ export function ItemsPerPage({ itemsPerPage, itemsPerPageLabel, itemsPerPageOpti
13
13
  }, value: { value: itemsPerPage.toString(), label: itemsPerPage.toString() }, options: itemsPerPageOptions.map((value) => ({
14
14
  value: value.toString(),
15
15
  label: value.toString(),
16
- })), popoverContentProps: { className: classes.optionPanel }, onChange: (value) => {
17
- onItemsPerPageChange?.(Number(value));
16
+ })), popoverContentProps: { className: classes.optionPanel }, onChange: (option) => {
17
+ onItemsPerPageChange?.(Number(option?.value));
18
18
  } }) })] }));
19
19
  }
@@ -17,8 +17,8 @@ export function PagePicker({ currentPageNumber, totalPages, prevButtonProps, pag
17
17
  }, value: { value: currentPageNumber.toString(), label: currentPageNumber.toString() }, options: Array.from({ length: totalPages }).map((_, index) => ({
18
18
  value: (index + 1).toString(),
19
19
  label: (index + 1).toString(),
20
- })), popoverContentProps: { className: classes.optionPanel }, onChange: (value) => {
21
- onPageChange?.(Number(value));
20
+ })), popoverContentProps: { className: classes.optionPanel }, onChange: (option) => {
21
+ onPageChange?.(Number(option?.value));
22
22
  } }) }), _jsx(Typography, { className: cx(classes.separator, 'mfui-PagePicker__separator'), children: "/" }), _jsxs(Typography, { className: cx(classes.totalPages, 'mfui-PagePicker__totalPages'), children: [totalPages, pageSuffixLabel] }), _jsx(IconButton, { className: cx(classes.nextButton, 'mfui-PagePicker__nextButton'), "aria-label": nextButtonProps?.['aria-label'] ?? '次のページへ進む', disabled: currentPageNumber === totalPages, onClick: () => {
23
23
  onPageChange?.(currentPageNumber + 1);
24
24
  }, children: _jsx(PickerAfter, {}) })] }));
@@ -93,7 +93,7 @@ export function Popover({ renderTrigger, renderContent, open, onOpenStateChanged
93
93
  const triggerElement = refs.reference.current;
94
94
  const popoverElement = refs.floating.current;
95
95
  // Get the appropriate root node for the trigger element
96
- const rootNode = getRootNode(triggerElement);
96
+ const rootNode = getRootNode(triggerElement, { allowDetached: true });
97
97
  // Helper function to check if an element is within the component
98
98
  const isElementWithinComponent = (element) => {
99
99
  if (!element)
@@ -1,12 +1,14 @@
1
1
  'use client';
2
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import React, { forwardRef, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
4
- import { Dropdown as DropdownIcon } from '@moneyforward/mfui-icons-react';
4
+ import { Dropdown as DropdownIcon, Error } from '@moneyforward/mfui-icons-react';
5
5
  import { ClearButton } from '../shared/ClearButton';
6
6
  import { FocusIndicator } from '../FocusIndicator';
7
7
  import { selectBoxSlotRecipe } from '../../styled-system/recipes';
8
8
  import { Typography } from '../Typography';
9
9
  import { SearchBox } from '../SearchBox';
10
+ import { Button } from '../Button';
11
+ import { ProgressIndicator } from '../ProgressIndicator';
10
12
  import { useSearchBox } from './hooks/useSearchBox';
11
13
  import { Skeleton } from '../Skeleton';
12
14
  import { useFocusTrap } from '../utilities/dom/useFocusTrap';
@@ -22,6 +24,7 @@ import { SelectBoxOptionComponent } from './SelectBoxOption';
22
24
  import { SelectBoxOptionGroup } from './SelectBoxOptionGroup';
23
25
  import { isOptionDisabled } from './utils/isSelectableOption';
24
26
  import { useVirtualizedOptions } from './hooks/useVirtualizedOptions';
27
+ import { useInfiniteScroll } from './hooks/useInfiniteScroll';
25
28
  const DEFAULT_SCROLL_OPTIONS = {
26
29
  behavior: 'auto',
27
30
  block: 'center',
@@ -35,7 +38,7 @@ const SKELETON_ITEM_COUNT = 4;
35
38
  export const SelectBox = forwardRef((props, ref) => {
36
39
  const { id, triggerProps, clearButtonProps, enableClearButton = false, size, options = [], defaultValue,
37
40
  // eslint-disable-next-line @typescript-eslint/no-deprecated
38
- placeholder, emptyMessage, disabled, invalid, 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, } = props;
41
+ placeholder, emptyMessage, disabled, invalid, 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;
39
42
  const classes = selectBoxSlotRecipe({ size, showGroupOptionDivider });
40
43
  const triggerRef = useRef(null);
41
44
  const listBoxId = useId();
@@ -117,6 +120,25 @@ export const SelectBox = forwardRef((props, ref) => {
117
120
  estimateSize: virtualizationOptions?.estimateSize,
118
121
  overscan: virtualizationOptions?.overscan,
119
122
  });
123
+ // Extract infinite scroll configuration with defaults
124
+ const baseEnabledInfiniteScroll = infiniteScroll?.enabled ?? false;
125
+ const onLoadMore = infiniteScroll?.onLoadMore;
126
+ const hasNextPage = infiniteScroll?.hasNextPage ?? true;
127
+ const hasPreviousPage = infiniteScroll?.hasPreviousPage ?? true;
128
+ const infiniteScrollThreshold = infiniteScroll?.threshold ?? 100;
129
+ const infiniteScrollErrorMessage = infiniteScroll?.errorMessage ?? '読み込みに失敗しました';
130
+ const infiniteScrollRetryButtonText = infiniteScroll?.retryButtonText ?? '再読み込み';
131
+ // Initialize infinite scroll
132
+ const { isLoading: isInfiniteScrollLoading, error: infiniteScrollError, handleScroll: handleInfiniteScroll, retryLoad: retryInfiniteScroll, } = useInfiniteScroll({
133
+ onLoadMore,
134
+ hasNextPage,
135
+ hasPreviousPage,
136
+ }, {
137
+ enabled: baseEnabledInfiniteScroll,
138
+ threshold: infiniteScrollThreshold,
139
+ });
140
+ // Disable infinite scroll when there's an error
141
+ const enableInfiniteScroll = baseEnabledInfiniteScroll && !infiniteScrollError;
120
142
  const handleSelectOption = useCallback((option) => {
121
143
  if (isOptionDisabled(option))
122
144
  return;
@@ -266,7 +288,23 @@ export const SelectBox = forwardRef((props, ref) => {
266
288
  : options.length > 0
267
289
  ? notFoundMessage
268
290
  : emptyMessage }) })), [classes.emptyMessage, enableSearchOptions, searchText, notFoundMessage, options.length, emptyMessage]);
269
- const optionsNode = (_jsx("div", { ref: scrollWrapperRef, className: cx(classes.scrollWrapper, 'mfui-SelectBox__scrollWrapper'), children: _jsx("ul", { role: "listbox", id: listBoxId, className: cx(classes.listBox, 'mfui-SelectBox__listBox'), tabIndex: -1, style: isVirtualized
291
+ // Render infinite scroll error message with retry button
292
+ const renderInfiniteScrollError = useCallback(() => infiniteScrollError ? (_jsxs("li", { className: cx(classes.infiniteScrollError, 'mfui-SelectBox__infiniteScrollError'), role: "alert", children: [_jsxs("div", { className: cx(classes.infiniteScrollErrorMessage, 'mfui-SelectBox__infiniteScrollErrorMessage'), "aria-live": "polite", children: [_jsx(Error, { "aria-hidden": true, className: cx(classes.infiniteScrollErrorIcon, 'mfui-SelectBox__infiniteScrollErrorIcon') }), _jsx(Typography, { variant: "body", children: infiniteScrollErrorMessage })] }), _jsx("div", { className: cx(classes.infiniteScrollErrorButton, 'mfui-SelectBox__infiniteScrollErrorButton'), children: _jsx(Button, { size: "small", onClick: (event) => {
293
+ event.stopPropagation();
294
+ retryInfiniteScroll();
295
+ }, children: infiniteScrollRetryButtonText }) })] })) : null, [
296
+ infiniteScrollError,
297
+ classes.infiniteScrollError,
298
+ classes.infiniteScrollErrorIcon,
299
+ classes.infiniteScrollErrorMessage,
300
+ classes.infiniteScrollErrorButton,
301
+ retryInfiniteScroll,
302
+ infiniteScrollErrorMessage,
303
+ infiniteScrollRetryButtonText,
304
+ ]);
305
+ // Render infinite scroll loading indicator
306
+ const renderInfiniteScrollLoading = useCallback(() => isInfiniteScrollLoading && enableInfiniteScroll ? (_jsx("div", { className: cx(classes.infiniteScrollLoading, 'mfui-SelectBox__infiniteScrollLoading'), children: _jsx(ProgressIndicator, {}) })) : null, [isInfiniteScrollLoading, enableInfiniteScroll, classes.infiniteScrollLoading]);
307
+ const optionsNode = (_jsx("div", { ref: scrollWrapperRef, className: cx(classes.scrollWrapper, 'mfui-SelectBox__scrollWrapper'), onScroll: enableInfiniteScroll ? handleInfiniteScroll : undefined, children: _jsx("ul", { role: "listbox", id: listBoxId, className: cx(classes.listBox, 'mfui-SelectBox__listBox'), tabIndex: -1, style: isVirtualized
270
308
  ? {
271
309
  height: `${String(totalSize)}px`,
272
310
  position: 'relative',
@@ -275,10 +313,10 @@ export const SelectBox = forwardRef((props, ref) => {
275
313
  ? Array.from({ length: SKELETON_ITEM_COUNT }).map((_, index) => (_jsx("li", { className: cx(classes.skeletonItem, 'mfui-SelectBox__skeletonItem'), children: _jsx(Skeleton, {}) }, index)))
276
314
  : isVirtualized && virtualItems.length > 0
277
315
  ? // Virtualized rendering with group support
278
- renderVirtualizedItems()
316
+ [...renderVirtualizedItems(), renderInfiniteScrollLoading(), renderInfiniteScrollError()].filter(Boolean)
279
317
  : filteredOptions.length > 0
280
- ? renderNonVirtualizedItems()
281
- : renderEmptyMessage() }) }));
318
+ ? [...renderNonVirtualizedItems(), renderInfiniteScrollLoading(), renderInfiniteScrollError()].filter(Boolean)
319
+ : [renderEmptyMessage(), renderInfiniteScrollError()].filter(Boolean) }) }));
282
320
  const handleClearValue = () => {
283
321
  setLocalSelectedOption(null);
284
322
  onChange?.(null);
@@ -290,7 +328,7 @@ export const SelectBox = forwardRef((props, ref) => {
290
328
  return (_jsx(Popover, { open: isOptionPanelOpen, targetDOMNode: targetDOMNode, minWidth: popoverContentProps?.minWidth, maxHeight: popoverWrapperProps?.maxHeight, allowedPlacements: popoverContentProps?.allowedPlacements, enableConstrainedContentWidth: enableConstrainedPopoverWidth, renderContent: () => (_jsx("div", { ref: optionPanelRef, id: listBoxId, className: cx(classes.optionPanel, 'mfui-SelectBox__optionPanel', popoverContentProps?.className), tabIndex: -1, onKeyDown: handleKeyDownMenu, children: renderPopoverContent ? (renderPopoverContent({ searchNode, optionsNode })) : (_jsxs(_Fragment, { children: [searchNode, optionsNode] })) })), renderTrigger: ({ setTriggerRef, togglePopover, isPopoverOpen, handleTriggerKeyDown, handleTriggerBlur }) => (_jsxs("div", { ref: setTriggerRef, ...triggerWrapperProps, className: cx(classes.triggerWrapper, 'mfui-SelectBox__triggerWrapper', triggerWrapperProps?.className), children: [_jsx(FocusIndicator, { children: _jsxs("button", { ref: triggerRef, id: id, type: "button", role: "combobox", disabled: disabled, "aria-label": triggerProps?.['aria-label'], "aria-controls": listBoxId, "aria-expanded": isPopoverOpen, "aria-haspopup": "listbox", "aria-invalid": invalid, className: cx(classes.trigger, 'mfui-SelectBox__trigger', triggerProps?.className), "data-placeholder": !!placeholder && !localSelectedOption, "data-selected": !!localSelectedOption, "data-mfui-has-clear-button": showClearButton, onClick: togglePopover, onKeyDown: (event) => {
291
329
  handleTypeAhead(event.nativeEvent);
292
330
  handleTriggerKeyDown(event);
293
- }, onBlur: handleTriggerBlur, children: [_jsx("span", { "data-mfui-content": "select-box-trigger-display-value", children: renderTriggerLabel() }), _jsx(DropdownIcon, {}), _jsx("input", { ref: ref, type: "hidden", value: localSelectedOption?.value ?? '', name: name, disabled: disabled })] }) }), showClearButton ? (_jsx("div", { className: cx(classes.clearButtonWrapper, 'mfui-SelectBox__clearButtonWrapper'), children: _jsx(ClearButton, { size: size === 'small' ? 'small' : 'default', "aria-label": clearButtonProps?.['aria-label'] ?? '値をクリアする', "data-mfui-content": "select-box-clear-button", onClick: (event) => {
331
+ }, onBlur: handleTriggerBlur, children: [_jsx("span", { "data-mfui-content": "select-box-trigger-display-value", children: renderTriggerLabel() }), _jsx(DropdownIcon, {}), _jsx("input", { ref: ref, type: "hidden", value: localSelectedOption?.value ?? '', name: name, disabled: disabled })] }) }), showClearButton ? (_jsx("div", { className: cx(classes.clearButtonWrapper, 'mfui-SelectBox__clearButtonWrapper'), children: _jsx(ClearButton, { size: size === 'small' ? 'small' : size === 'large' ? 'large' : 'default', "aria-label": clearButtonProps?.['aria-label'] ?? '値をクリアする', "data-mfui-content": "select-box-clear-button", onClick: (event) => {
294
332
  event.stopPropagation();
295
333
  handleClearValue();
296
334
  } }) })) : null] })), contentProps: { className: classes.popover }, enableAutoUnmount: enableAutoUnmount, onOpenStateChanged: toggleOptionPanel, onBlur: onBlur }));
@@ -3,8 +3,61 @@ import { type VirtualizerOptions } from '@tanstack/react-virtual';
3
3
  import { type SelectBoxSlotRecipeVariant } from '../../styled-system/recipes';
4
4
  import { type SearchBoxProps } from '../SearchBox';
5
5
  import { type PopoverProps } from '../Popover';
6
+ import { type InfiniteScrollDirection } from './hooks/useInfiniteScroll';
6
7
  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 "Failed to load data"
51
+ */
52
+ errorMessage?: string;
53
+ /**
54
+ * The text for the retry button when loading fails.
55
+ * This message supports internationalization.
56
+ *
57
+ * @default "Retry"
58
+ */
59
+ retryButtonText?: string;
60
+ };
8
61
  export type BasedAdditionalProps = Record<string, unknown>;
9
62
  export type SelectBoxOption<T extends AllowedValueTypes = string, AdditionalProps extends BasedAdditionalProps = BasedAdditionalProps> = ({
10
63
  /**
@@ -351,5 +404,31 @@ export type SelectBoxProps<T extends AllowedValueTypes = string, AdditionalProps
351
404
  * ```
352
405
  */
353
406
  virtualizationOptions?: VirtualOptionTypes;
407
+ /**
408
+ * Infinite scroll configuration.
409
+ * When provided, enables infinite scroll functionality for loading additional options dynamically.
410
+ *
411
+ * @example
412
+ * ```tsx
413
+ * <SelectBox
414
+ * infiniteScroll={{
415
+ * enabled: true,
416
+ * onLoadMore: async (direction) => {
417
+ * if (direction === 'forward') {
418
+ * const nextOptions = await loadNextPage();
419
+ * setOptions(prev => [...prev, ...nextOptions]);
420
+ * } else {
421
+ * const prevOptions = await loadPreviousPage();
422
+ * setOptions(prev => [...prevOptions, ...prev]);
423
+ * }
424
+ * },
425
+ * hasNextPage: hasMore,
426
+ * hasPreviousPage: hasPrevious,
427
+ * threshold: 50 // Trigger when within 50px of edges
428
+ * }}
429
+ * />
430
+ * ```
431
+ */
432
+ infiniteScroll?: InfiniteScrollConfig;
354
433
  };
355
- export {};
434
+ export { type InfiniteScrollDirection } from './hooks/useInfiniteScroll';
@@ -0,0 +1,22 @@
1
+ export type InfiniteScrollDirection = 'forward' | 'backward';
2
+ export type InfiniteScrollCallbacks = {
3
+ onLoadMore?: (direction: InfiniteScrollDirection) => Promise<void>;
4
+ hasNextPage?: boolean;
5
+ hasPreviousPage?: boolean;
6
+ };
7
+ export type InfiniteScrollError = {
8
+ direction: InfiniteScrollDirection;
9
+ error: Error;
10
+ };
11
+ export type UseInfiniteScrollOptions = {
12
+ enabled?: boolean;
13
+ threshold?: number;
14
+ };
15
+ export type UseInfiniteScrollReturn = {
16
+ isLoading: boolean;
17
+ error: InfiniteScrollError | null;
18
+ handleScroll: (event: React.UIEvent<HTMLElement>) => void;
19
+ retryLoad: () => void;
20
+ clearError: () => void;
21
+ };
22
+ export declare const useInfiniteScroll: (callbacks: InfiniteScrollCallbacks, options?: UseInfiniteScrollOptions) => UseInfiniteScrollReturn;
@@ -0,0 +1,65 @@
1
+ import { useCallback, useRef, useState } from 'react';
2
+ export const useInfiniteScroll = (callbacks, options = {}) => {
3
+ const { enabled = true, threshold = 100 } = options;
4
+ const { onLoadMore, hasNextPage = true, hasPreviousPage = true } = callbacks;
5
+ const [isLoading, setIsLoading] = useState(false);
6
+ const [error, setError] = useState(null);
7
+ const isLoadingRef = useRef(false);
8
+ const executeLoad = useCallback(async (direction, forceLoad = false) => {
9
+ if ((!enabled && !forceLoad) || !onLoadMore || isLoadingRef.current)
10
+ return;
11
+ if (direction === 'forward' && !hasNextPage)
12
+ return;
13
+ if (direction === 'backward' && !hasPreviousPage)
14
+ return;
15
+ try {
16
+ isLoadingRef.current = true;
17
+ setIsLoading(true);
18
+ setError(null);
19
+ await onLoadMore(direction);
20
+ }
21
+ catch (error_) {
22
+ const errorObject = error_ instanceof Error ? error_ : new Error('Unknown error');
23
+ setError({ direction, error: errorObject });
24
+ }
25
+ finally {
26
+ isLoadingRef.current = false;
27
+ setIsLoading(false);
28
+ }
29
+ }, [enabled, onLoadMore, hasNextPage, hasPreviousPage]);
30
+ const handleScroll = useCallback((event) => {
31
+ if (!enabled || isLoadingRef.current)
32
+ return;
33
+ const target = event.currentTarget;
34
+ const { scrollTop, scrollHeight, clientHeight } = target;
35
+ const nearTop = scrollTop <= threshold;
36
+ const nearBottom = scrollTop + clientHeight >= scrollHeight - threshold;
37
+ if (nearBottom && hasNextPage) {
38
+ executeLoad('forward').catch(() => {
39
+ // Error handling is done within executeLoad
40
+ });
41
+ }
42
+ else if (nearTop && hasPreviousPage) {
43
+ executeLoad('backward').catch(() => {
44
+ // Error handling is done within executeLoad
45
+ });
46
+ }
47
+ }, [enabled, threshold, hasNextPage, hasPreviousPage, executeLoad]);
48
+ const retryLoad = useCallback(() => {
49
+ if (error) {
50
+ executeLoad(error.direction, true).catch(() => {
51
+ // Error handling is done within executeLoad
52
+ });
53
+ }
54
+ }, [error, executeLoad]);
55
+ const clearError = useCallback(() => {
56
+ setError(null);
57
+ }, []);
58
+ return {
59
+ isLoading,
60
+ error,
61
+ handleScroll,
62
+ retryLoad,
63
+ clearError,
64
+ };
65
+ };
@@ -43,9 +43,9 @@ export const TextBox = forwardRef(({ enableClearButton = false, onClear, invalid
43
43
  const displayValue = (inputProps.value ?? inputProps.defaultValue ?? '');
44
44
  return shouldUseTouchDeviceMode ? (
45
45
  // Touch device: wrapper + hidden input + display div
46
- _jsxs("div", { ...wrapperProps, className: cx(classes.root, 'mfui-TextBox__root', wrapperProps.className), children: [_jsx("input", { type: "hidden", value: displayValue, name: inputProps.name }), prefixSlot ? _jsx("div", { className: cx(classes.prefix, 'mfui-TextBox__prefix'), children: prefixSlot }) : null, _jsx("div", { className: cx(classes.input, 'mfui-TextBox__input'), children: displayValue || inputProps.placeholder }), shouldShowClearButton ? (_jsx(ClearButton, { size: textBoxSize === 'small' ? 'small' : 'default', "aria-label": clearButtonProps?.['aria-label'] ?? '値をクリアする', className: cx(classes.clearButton, 'mfui-TextBox__clearButton'), disabled: disabled, onClick: (event) => {
46
+ _jsxs("div", { ...wrapperProps, className: cx(classes.root, 'mfui-TextBox__root', wrapperProps.className), children: [_jsx("input", { type: "hidden", value: displayValue, name: inputProps.name }), prefixSlot ? _jsx("div", { className: cx(classes.prefix, 'mfui-TextBox__prefix'), children: prefixSlot }) : null, _jsx("div", { className: cx(classes.input, 'mfui-TextBox__input'), children: displayValue || inputProps.placeholder }), shouldShowClearButton ? (_jsx(ClearButton, { size: textBoxSize === 'small' ? 'small' : textBoxSize === 'large' ? 'large' : 'default', "aria-label": clearButtonProps?.['aria-label'] ?? '値をクリアする', className: cx(classes.clearButton, 'mfui-TextBox__clearButton'), disabled: disabled, onClick: (event) => {
47
47
  onClear?.(event);
48
48
  } })) : null, suffixSlot ? _jsx("div", { className: cx(classes.suffix, 'mfui-TextBox__suffix'), children: suffixSlot }) : null] })) : (
49
49
  // Desktop: Standard input with FocusIndicator
50
- _jsx(FocusIndicator, { children: _jsxs("div", { ...wrapperProps, className: cx(classes.root, 'mfui-TextBox__root', wrapperProps.className), children: [prefixSlot ? _jsx("div", { className: cx(classes.prefix, 'mfui-TextBox__prefix'), children: prefixSlot }) : null, _jsx("input", { ref: mergedInputRef, disabled: disabled, ...inputProps, className: cx(classes.input, 'mfui-TextBox__input', inputProps.className), onChange: handleChange, onInput: handleInput }), shouldShowClearButton ? (_jsx(ClearButton, { size: textBoxSize === 'small' ? 'small' : 'default', "aria-label": clearButtonProps?.['aria-label'] ?? '値をクリアする', className: cx(classes.clearButton, 'mfui-TextBox__clearButton'), disabled: disabled, onClick: onClickClear })) : null, suffixSlot ? _jsx("div", { className: cx(classes.suffix, 'mfui-TextBox__suffix'), children: suffixSlot }) : null] }) }));
50
+ _jsx(FocusIndicator, { children: _jsxs("div", { ...wrapperProps, className: cx(classes.root, 'mfui-TextBox__root', wrapperProps.className), children: [prefixSlot ? _jsx("div", { className: cx(classes.prefix, 'mfui-TextBox__prefix'), children: prefixSlot }) : null, _jsx("input", { ref: mergedInputRef, disabled: disabled, ...inputProps, className: cx(classes.input, 'mfui-TextBox__input', inputProps.className), onChange: handleChange, onInput: handleInput }), shouldShowClearButton ? (_jsx(ClearButton, { size: textBoxSize === 'small' ? 'small' : textBoxSize === 'large' ? 'large' : 'default', "aria-label": clearButtonProps?.['aria-label'] ?? '値をクリアする', className: cx(classes.clearButton, 'mfui-TextBox__clearButton'), disabled: disabled, onClick: onClickClear })) : null, suffixSlot ? _jsx("div", { className: cx(classes.suffix, 'mfui-TextBox__suffix'), children: suffixSlot }) : null] }) }));
51
51
  });
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The general-purpose ToggleSwitch component.
3
+ * This component provides a switch to toggle between ON and OFF states.
4
+ *
5
+ * This component extends the props of `<input type="checkbox">` element.
6
+ *
7
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox
8
+ */
9
+ export declare const ToggleSwitch: import("react").ForwardRefExoticComponent<Omit<import("react").DetailedHTMLProps<import("react").InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, "ref"> & import("react").RefAttributes<HTMLInputElement>>;
@@ -0,0 +1,32 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { forwardRef, useState } from 'react';
4
+ import { cx } from '../../styled-system/css';
5
+ import { toggleSwitchSlotRecipe } from '../../styled-system/recipes';
6
+ import { FocusIndicator } from '../FocusIndicator/FocusIndicator';
7
+ /**
8
+ * The general-purpose ToggleSwitch component.
9
+ * This component provides a switch to toggle between ON and OFF states.
10
+ *
11
+ * This component extends the props of `<input type="checkbox">` element.
12
+ *
13
+ * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/checkbox
14
+ */
15
+ export const ToggleSwitch = forwardRef(({ className, checked, defaultChecked, disabled, onChange, ...props }, ref) => {
16
+ const [localChecked, setLocalChecked] = useState(!!defaultChecked);
17
+ const isControlled = checked !== undefined;
18
+ const classes = toggleSwitchSlotRecipe();
19
+ const isChecked = isControlled ? checked : localChecked;
20
+ return (_jsx(FocusIndicator, { children: _jsxs("span", { className: cx(classes.root, 'mfui-ToggleSwitch__root', className), children: [_jsx("span", { className: cx(classes.handle, 'mfui-ToggleSwitch__handle'), "data-mfui-content": "toggle-handle" }), _jsx("input", { ref: ref, type: "checkbox", role: "switch", checked: isChecked, disabled: disabled, className: cx(classes.input, 'mfui-ToggleSwitch__input'), "data-mfui-content": "toggle-input", onChange: (event) => {
21
+ onChange?.(event);
22
+ // If the event is defaultPrevented in "onChange" prop via "e.preventDefault()", do not update local
23
+ // states.
24
+ if (event.defaultPrevented) {
25
+ return;
26
+ }
27
+ if (!isControlled) {
28
+ setLocalChecked(event.target.checked);
29
+ }
30
+ }, ...props })] }) }));
31
+ });
32
+ ToggleSwitch.displayName = 'ToggleSwitch';
@@ -0,0 +1,6 @@
1
+ import { type ComponentPropsWithoutRef } from 'react';
2
+ /**
3
+ * ToggleSwitch component props.
4
+ * Extends the standard HTML input checkbox element props.
5
+ */
6
+ export type ToggleSwitchProps = ComponentPropsWithoutRef<'input'>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export * from './ToggleSwitch';
2
+ export * from './ToggleSwitch.types';
@@ -0,0 +1,2 @@
1
+ export * from './ToggleSwitch';
2
+ export * from './ToggleSwitch.types';