@moneyforward/mfui-components 4.3.1 → 4.6.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 (108) hide show
  1. package/dist/src/Accordion/Accordion.d.ts +21 -1
  2. package/dist/src/Accordion/Accordion.js +28 -4
  3. package/dist/src/Accordion/Accordion.types.d.ts +21 -0
  4. package/dist/src/Badge/Badge.d.ts +1 -1
  5. package/dist/src/Badge/Badge.js +4 -4
  6. package/dist/src/Badge/Badge.types.d.ts +3 -1
  7. package/dist/src/Breadcrumbs/Breadcrumbs.js +1 -1
  8. package/dist/src/Checkbox/Checkbox.js +7 -3
  9. package/dist/src/CheckboxCard/CheckboxCard.js +3 -1
  10. package/dist/src/DataTable/DataTable.js +9 -2
  11. package/dist/src/DataTable/DataTable.types.d.ts +17 -2
  12. package/dist/src/DataTable/DataTableCell/DataTableCell.js +21 -4
  13. package/dist/src/DataTable/DataTableCell/DataTableCell.types.d.ts +1 -1
  14. package/dist/src/DataTable/DataTableHeaderRow/DataTableHeaderRow.js +9 -5
  15. package/dist/src/DataTable/DataTableHeaderRow/DataTableHeaderRow.types.d.ts +19 -1
  16. package/dist/src/DataTable/DataTableRow/DataTableRow.js +106 -31
  17. package/dist/src/DataTable/DataTableRow/DataTableRow.types.d.ts +63 -12
  18. package/dist/src/DataTable/DataTableRowActionContext.d.ts +34 -0
  19. package/dist/src/DataTable/DataTableRowActionContext.js +8 -0
  20. package/dist/src/DateTimeSelection/DatePicker/DatePickerCalendar/DatePickerCalendar.js +7 -3
  21. package/dist/src/DateTimeSelection/DatePicker/DatePickerDateCell/index.d.ts +1 -1
  22. package/dist/src/DateTimeSelection/DatePicker/DatePickerDateCell/index.js +0 -1
  23. package/dist/src/DateTimeSelection/DateRangePicker/DateRangePickerPopover/DateRangePickerCalendar/DateRangePickerCalendar.js +1 -1
  24. package/dist/src/DateTimeSelection/DateRangePicker/DateRangePickerPopover/utilities/createDateRangePickerPopoverTestUtility.js +2 -2
  25. package/dist/src/DateTimeSelection/DateRangePicker/DateRangePickerProvider/DateRangePickerProvider.js +13 -7
  26. package/dist/src/DateTimeSelection/DateRangePicker/DateRangePickerTrigger/DateRangePickerTrigger.js +1 -1
  27. package/dist/src/DateTimeSelection/FilterDatePicker/FilterDatePicker.js +1 -1
  28. package/dist/src/DateTimeSelection/FilterMonthPicker/FilterMonthPicker.js +1 -1
  29. package/dist/src/DateTimeSelection/FilterMonthRangePicker/FilterMonthRangePicker.js +1 -1
  30. package/dist/src/DateTimeSelection/MonthPicker/MonthPickerPanel/MonthPickerPanel.js +1 -1
  31. package/dist/src/DateTimeSelection/MonthPicker/constants.d.ts +2 -2
  32. package/dist/src/DateTimeSelection/MonthPicker/constants.js +6 -6
  33. package/dist/src/DateTimeSelection/MonthRangePicker/MonthRangePickerMonthCell/MonthRangePickerMonthCell.js +1 -1
  34. package/dist/src/DateTimeSelection/MonthRangePicker/MonthRangePickerNavigation/MonthRangePickerNavigation.js +2 -2
  35. package/dist/src/DateTimeSelection/TimePicker/TimePicker.js +1 -1
  36. package/dist/src/DateTimeSelection/shared/BasePicker/constants.d.ts +2 -2
  37. package/dist/src/DateTimeSelection/shared/BasePicker/constants.js +10 -10
  38. package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerPopover/BaseRangePickerCalendar/BaseRangePickerCalendar.js +1 -1
  39. package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerPopover/constants.js +4 -4
  40. package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerProvider/BaseRangePickerProvider.js +13 -7
  41. package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerTrigger/BaseRangePickerTrigger.js +2 -2
  42. package/dist/src/DateTimeSelection/shared/CalendarGrid/CalendarGrid.js +2 -4
  43. package/dist/src/DateTimeSelection/shared/utilities/japaneseCalendar.js +2 -2
  44. package/dist/src/Dialog/Dialog.d.ts +1 -1
  45. package/dist/src/Dialog/Dialog.types.d.ts +1 -1
  46. package/dist/src/FileDropZone/FileDropZone.js +1 -1
  47. package/dist/src/FileDropZone/utils/fileHelpers.js +1 -1
  48. package/dist/src/MainNavigation/BaseMainNavigation.d.ts +1 -1
  49. package/dist/src/MainNavigation/BaseMainNavigation.js +3 -3
  50. package/dist/src/MainNavigation/MainNavigation.js +2 -1
  51. package/dist/src/MainNavigation/MainNavigation.types.d.ts +9 -0
  52. package/dist/src/MainNavigation/NarrowViewportMainNavigation.js +3 -3
  53. package/dist/src/MultipleSelectBox/MultipleSelectBox.js +27 -18
  54. package/dist/src/MultipleSelectBox/MultipleSelectBoxTrigger/MultipleSelectBoxTrigger.js +1 -1
  55. package/dist/src/MultipleSelectBox/hooks/useCreateOption.js +8 -4
  56. package/dist/src/MultipleSelectBox/hooks/useInitialFocusOnOptionPanelOpen.js +1 -1
  57. package/dist/src/MultipleSelectBox/hooks/useOptionKeyboardNavigation.js +9 -4
  58. package/dist/src/MultipleSelectBox/hooks/useSelectedValues.js +16 -50
  59. package/dist/src/NavigationList/NavigationListItem/NavigationListItem.d.ts +3 -0
  60. package/dist/src/NavigationList/NavigationListItem/NavigationListItem.js +6 -2
  61. package/dist/src/NavigationList/NavigationListItem/NavigationListItem.types.d.ts +2 -1
  62. package/dist/src/NumberBox/NumberBox.d.ts +5 -0
  63. package/dist/src/NumberBox/NumberBox.js +61 -6
  64. package/dist/src/NumberBox/NumberBox.types.d.ts +35 -0
  65. package/dist/src/PageHeader/PageHeader.types.d.ts +2 -0
  66. package/dist/src/Pagination/Pagination.js +4 -1
  67. package/dist/src/Popover/Popover.js +12 -4
  68. package/dist/src/RadioButtonCard/RadioButtonCard.js +3 -1
  69. package/dist/src/SelectBox/SelectBox.js +29 -21
  70. package/dist/src/SelectBox/hooks/useKeyboardNavigation.js +8 -2
  71. package/dist/src/SelectBox/utils/getFlatOptions.js +1 -1
  72. package/dist/src/SidePane/SidePane.js +87 -7
  73. package/dist/src/SidePane/SidePane.types.d.ts +39 -0
  74. package/dist/src/SplitView/hooks/useSplitViewPanelVisibility.js +5 -2
  75. package/dist/src/SubNavigation/SubNavigation.d.ts +10 -1
  76. package/dist/src/SubNavigation/SubNavigation.js +93 -21
  77. package/dist/src/SubNavigation/SubNavigation.types.d.ts +45 -7
  78. package/dist/src/Tabs/Tabs.js +8 -4
  79. package/dist/src/Tabs/hooks/useCalculatedSelectedTabId.js +7 -5
  80. package/dist/src/Tabs/hooks/useTabRegistry.js +5 -5
  81. package/dist/src/Toast/ToastContainer/ToastContainer.js +11 -2
  82. package/dist/src/ToggleSwitch/index.d.ts +1 -1
  83. package/dist/src/ToggleSwitch/index.js +0 -1
  84. package/dist/src/Tooltip/Tooltip.js +8 -3
  85. package/dist/src/Tooltip/hooks/useTooltipDisplayController.js +77 -88
  86. package/dist/src/utilities/dom/containsAcrossShadowRoots.d.ts +21 -0
  87. package/dist/src/utilities/dom/containsAcrossShadowRoots.js +33 -0
  88. package/dist/src/utilities/dom/getFocusableNodes.js +1 -3
  89. package/dist/src/utilities/dom/useFixedColumns.js +6 -8
  90. package/dist/src/utilities/dom/useFocusTrap.js +1 -1
  91. package/dist/src/utilities/dom/useTableEdgeCellMarkers.d.ts +56 -0
  92. package/dist/src/utilities/dom/useTableEdgeCellMarkers.js +223 -0
  93. package/dist/src/utilities/react/flattenChildren.d.ts +15 -0
  94. package/dist/src/utilities/react/flattenChildren.js +28 -0
  95. package/dist/src/utilities/state/useLatest.d.ts +1 -1
  96. package/dist/src/utilities/state/useLatest.js +8 -2
  97. package/dist/styled-system/recipes/badge-slot-recipe.d.ts +1 -1
  98. package/dist/styled-system/recipes/badge-slot-recipe.js +4 -0
  99. package/dist/styled-system/recipes/data-table-cell-slot-recipe.d.ts +1 -1
  100. package/dist/styled-system/recipes/data-table-cell-slot-recipe.js +8 -2
  101. package/dist/styled-system/recipes/data-table-row-slot-recipe.js +13 -6
  102. package/dist/styled-system/recipes/sub-navigation-slot-recipe.d.ts +1 -1
  103. package/dist/styled-system/recipes/sub-navigation-slot-recipe.js +12 -0
  104. package/dist/styles.css +175 -39
  105. package/dist/tsconfig.build.tsbuildinfo +1 -1
  106. package/package.json +12 -12
  107. /package/dist/src/{MultipleSelectBox/utils → utilities/dom}/getDeepActiveElement.d.ts +0 -0
  108. /package/dist/src/{MultipleSelectBox/utils → utilities/dom}/getDeepActiveElement.js +0 -0
@@ -141,8 +141,8 @@ export function warekiToDate(wareki) {
141
141
  // Traditional format: groups 3,4 (month, day)
142
142
  // Western format: groups 5,6 (month, day)
143
143
  // Month-only format: group 7 (month), day defaults to 1
144
- const month = Number(matchedWareki[3] || matchedWareki[5] || matchedWareki[7]);
145
- const day = matchedWareki[7] !== undefined ? 1 : Number(matchedWareki[4] || matchedWareki[6]);
144
+ const month = Number(matchedWareki[3] ?? matchedWareki[5] ?? matchedWareki[7]);
145
+ const day = matchedWareki[7] !== undefined ? 1 : Number(matchedWareki[4] ?? matchedWareki[6]);
146
146
  // Validation: ensure era year is reasonable (1-200 years per era)
147
147
  if (eraYear < 1 || eraYear > 200) {
148
148
  return {
@@ -19,6 +19,6 @@ export declare const Dialog: import("react").ForwardRefExoticComponent<{
19
19
  enableAutoUnmount?: boolean;
20
20
  disableBackdropClose?: boolean;
21
21
  customFocusElement?: import("react").RefObject<HTMLElement | null> | HTMLElement | null;
22
- } & Omit<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref">, "onClose" | "open"> & {
22
+ } & Omit<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref">, "onClose" | "open" | "title"> & {
23
23
  ref?: (import("react").RefCallback<HTMLDivElement> | import("react").RefObject<HTMLDivElement | null> | null) | undefined;
24
24
  } & import("react").Attributes>;
@@ -97,4 +97,4 @@ export type DialogProps = {
97
97
  * ```
98
98
  */
99
99
  customFocusElement?: RefObject<HTMLElement | null> | HTMLElement | null;
100
- } & Omit<ComponentPropsWithoutRef<'div'>, 'open' | 'onClose'>;
100
+ } & Omit<ComponentPropsWithoutRef<'div'>, 'open' | 'onClose' | 'title'>;
@@ -231,7 +231,7 @@ fileSelectButtonLabel = 'ファイルを選択', instructionText, fileCountLabel
231
231
  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'), children: [_jsx("div", { "data-mfui-content": "file-drop-zone-instruction", className: cx(classes.instruction, 'mfui-FileDropZone__instruction'), style: fileBoxMaxWidthStyle, 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'), style: fileBoxMaxWidthStyle, children: [_jsx(Button, { ref: buttonRef, className: cx(classes.button, 'mfui-FileDropZone__button'), disabled: disabled, "aria-describedby": errorMessages.length > 0 || helperMessage || (displayValues && displayValues.length > 0)
232
232
  ? `${id ? `${id}-` : ''}file-drop-zone-messages`
233
233
  : 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: [errorMessages.length > 0 ? (_jsx("div", { role: "alert", "data-mfui-content": "file-drop-zone-error-messages", className: cx(classes.errorMessages, 'mfui-FileDropZone__errorMessages'), children: errorMessages.map((message, index) => (_jsx(HelpMessage, { role: undefined, messageType: "error", messageAlign: "center", children: message }, index))) })) : null, helperMessage ? _jsx(HelpMessage, { messageAlign: "center", children: helperMessage }) : null, displayValues && displayValues.length > 0 ? (_jsx("div", { className: "sr-only", children: displayValues.length === 1
234
- ? getScreenReaderText('single', displayValues[0] || '')
234
+ ? getScreenReaderText('single', displayValues[0] ?? '')
235
235
  : 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] })] }));
236
236
  });
237
237
  FileDropZone.displayName = 'FileDropZone';
@@ -23,7 +23,7 @@ export function formatFileSize(bytes) {
23
23
  const i = Math.floor(Math.log(bytes) / Math.log(k));
24
24
  const unitIndex = Math.min(i, units.length - 1);
25
25
  const size = bytes / k ** unitIndex;
26
- return `${size.toFixed(1)} ${units[unitIndex] || 'B'}`;
26
+ return `${size.toFixed(1)} ${units[unitIndex] ?? 'B'}`;
27
27
  }
28
28
  /**
29
29
  * Get file extension from filename
@@ -3,4 +3,4 @@ import { type MainNavigationProps } from './MainNavigation.types';
3
3
  * The component for the main navigation on the normal viewport.
4
4
  * This component must only be used in MainNavigation.tsx, do not export it.
5
5
  */
6
- export declare function BaseMainNavigation({ className, featureShortcut, navigationItems, enableCollapsible, nestedNavigationDisplay, width, customLinkComponent, disableResponsive, isCollapsed: controlledIsCollapsed, onCollapsedChange, collapseButtonProps, expandButtonProps, }: MainNavigationProps): import("react/jsx-runtime").JSX.Element;
6
+ export declare function BaseMainNavigation({ className, featureShortcut, navigationItems, navLandmarkLabel, enableCollapsible, nestedNavigationDisplay, width, customLinkComponent, disableResponsive, isCollapsed: controlledIsCollapsed, onCollapsedChange, collapseButtonProps, expandButtonProps, }: MainNavigationProps): import("react/jsx-runtime").JSX.Element;
@@ -13,7 +13,7 @@ import { Popover } from '../Popover';
13
13
  * The component for the main navigation on the normal viewport.
14
14
  * This component must only be used in MainNavigation.tsx, do not export it.
15
15
  */
16
- export function BaseMainNavigation({ className, featureShortcut, navigationItems, enableCollapsible = false, nestedNavigationDisplay = 'accordion', width = 'normal', customLinkComponent, disableResponsive, isCollapsed: controlledIsCollapsed, onCollapsedChange, collapseButtonProps, expandButtonProps, }) {
16
+ export function BaseMainNavigation({ className, featureShortcut, navigationItems, navLandmarkLabel, enableCollapsible = false, nestedNavigationDisplay = 'accordion', width = 'normal', customLinkComponent, disableResponsive, isCollapsed: controlledIsCollapsed, onCollapsedChange, collapseButtonProps, expandButtonProps, }) {
17
17
  const [localIsCollapsed, setLocalIsCollapsed] = useState(controlledIsCollapsed ?? false);
18
18
  const [openPopoverIndex, setOpenPopoverIndex] = useState(null);
19
19
  const isControlled = controlledIsCollapsed !== undefined;
@@ -31,7 +31,7 @@ export function BaseMainNavigation({ className, featureShortcut, navigationItems
31
31
  const toggleButtonLabel = isCollapsed
32
32
  ? (expandButtonProps?.label ?? 'ナビゲーションを拡げる')
33
33
  : (collapseButtonProps?.label ?? 'ナビゲーションを折りたたむ');
34
- return (_jsxs("div", { "data-mfui-collapsed": isCollapsed, className: cx(classes.root, 'mfui-BaseMainNavigation__root', className), children: [featureShortcut ? (_jsx("div", { className: cx(classes.featureShortcut, 'mfui-BaseMainNavigation__featureShortcut'), children: _jsx(FocusIndicator, { children: _jsx(NavigationLink, { tag: InternalLinkTag, isExternal: featureShortcut.isExternal, href: featureShortcut.href, className: cx(classes.featureShortcutAnchor, 'mfui-BaseMainNavigation__featureShortcutAnchor'), children: _jsx(Typography, { variant: "controlLabel", children: featureShortcut.label }) }) }) })) : null, _jsxs("nav", { id: navId, className: cx(classes.nav, 'mfui-BaseMainNavigation__nav'), children: [_jsx("ul", { className: cx(classes.list, 'mfui-BaseMainNavigation__list'), children: navigationItems.map((navigation, index) => {
34
+ return (_jsxs("div", { "data-mfui-collapsed": isCollapsed, className: cx(classes.root, 'mfui-BaseMainNavigation__root', className), children: [featureShortcut ? (_jsx("div", { className: cx(classes.featureShortcut, 'mfui-BaseMainNavigation__featureShortcut'), children: _jsx(FocusIndicator, { children: _jsx(NavigationLink, { tag: InternalLinkTag, isExternal: featureShortcut.isExternal, href: featureShortcut.href, className: cx(classes.featureShortcutAnchor, 'mfui-BaseMainNavigation__featureShortcutAnchor'), children: _jsx(Typography, { variant: "controlLabel", children: featureShortcut.label }) }) }) })) : null, _jsxs("nav", { id: navId, "aria-label": navLandmarkLabel, className: cx(classes.nav, 'mfui-BaseMainNavigation__nav'), children: [_jsx("ul", { className: cx(classes.list, 'mfui-BaseMainNavigation__list'), children: navigationItems.map((navigation, index) => {
35
35
  const isGroupLabel = navigation.children && 'isGroupLabel' in navigation && navigation.isGroupLabel;
36
36
  const isGroupMenu = 'isGroupMenu' in navigation && navigation.isGroupMenu;
37
37
  if (isGroupLabel || isGroupMenu) {
@@ -70,7 +70,7 @@ function NestedNavigationItemRenderer({ navigation, originalIndex, navId, classe
70
70
  const hasCurrentChildren = navigation.children.some((child) => child.isCurrent);
71
71
  return (_jsx("li", { className: cx(classes.listItem, 'mfui-BaseMainNavigation__listItem'), "data-mfui-popover-portal-target-id": popoverPortalTargetId, children: isCollapsed || nestedNavigationDisplay === 'popover' ? (_jsx(PopoverNestedNavigationItem, { navigation: navigation, classes: classes, InternalLinkTag: InternalLinkTag, isCollapsed: isCollapsed, hasCurrentChildren: hasCurrentChildren, popoverPortalTargetId: popoverPortalTargetId, isOpen: openPopoverIndex === originalIndex, onOpenChange: (isOpen) => {
72
72
  setOpenPopoverIndex(isOpen ? originalIndex : null);
73
- } })) : (_jsxs("details", { open: ('isOpenByDefault' in navigation && navigation.isOpenByDefault) || hasCurrentChildren, className: cx(classes.parentDetails, 'mfui-BaseMainNavigation__parentDetails'), children: [_jsx(FocusIndicator, { position: "inside", children: _jsxs("summary", { className: cx(classes.parentSummary, 'mfui-BaseMainNavigation__parentSummary'), children: [_jsx("div", { "aria-hidden": "true", className: cx(classes.parentSummaryIcon, 'mfui-BaseMainNavigation__parentSummaryIcon'), children: navigation.icon }), _jsx(Typography, { variant: "controlLabel", children: navigation.label }), _jsx(DisclosureBasicCollapsed, { className: "mfui-BaseMainNavigation__icon_collapsed", "aria-hidden": "true", "data-mfui-content": "main-navigation-icon-collapsed" }), _jsx(DisclosureBasicExpanded, { className: "mfui-BaseMainNavigation__icon_expanded", "aria-hidden": "true", "data-mfui-content": "main-navigation-icon-expanded" })] }) }), _jsx("ul", { className: cx(classes.childrenList, 'mfui-BaseMainNavigation__childrenList'), children: navigation.children.map((child, childIndex) => (_jsx("li", { className: cx(classes.childrenListItem, 'mfui-BaseMainNavigation__childrenListItem'), children: _jsx(FocusIndicator, { position: "inside", children: _jsx(NavigationLink, { tag: InternalLinkTag, isExternal: child.isExternal, href: child.href, className: cx(classes.childrenListItemAnchor, 'mfui-BaseMainNavigation__childrenListItemAnchor'), "aria-current": child.isCurrent ? 'page' : undefined, children: _jsx(Typography, { variant: "controlLabel", children: child.label }) }) }) }, childIndex))) })] })) }));
73
+ } })) : (_jsxs("details", { open: !!('isOpenByDefault' in navigation && navigation.isOpenByDefault) || hasCurrentChildren, className: cx(classes.parentDetails, 'mfui-BaseMainNavigation__parentDetails'), children: [_jsx(FocusIndicator, { position: "inside", children: _jsxs("summary", { className: cx(classes.parentSummary, 'mfui-BaseMainNavigation__parentSummary'), children: [_jsx("div", { "aria-hidden": "true", className: cx(classes.parentSummaryIcon, 'mfui-BaseMainNavigation__parentSummaryIcon'), children: navigation.icon }), _jsx(Typography, { variant: "controlLabel", children: navigation.label }), _jsx(DisclosureBasicCollapsed, { className: "mfui-BaseMainNavigation__icon_collapsed", "aria-hidden": "true", "data-mfui-content": "main-navigation-icon-collapsed" }), _jsx(DisclosureBasicExpanded, { className: "mfui-BaseMainNavigation__icon_expanded", "aria-hidden": "true", "data-mfui-content": "main-navigation-icon-expanded" })] }) }), _jsx("ul", { className: cx(classes.childrenList, 'mfui-BaseMainNavigation__childrenList'), children: navigation.children.map((child, childIndex) => (_jsx("li", { className: cx(classes.childrenListItem, 'mfui-BaseMainNavigation__childrenListItem'), children: _jsx(FocusIndicator, { position: "inside", children: _jsx(NavigationLink, { tag: InternalLinkTag, isExternal: child.isExternal, href: child.href, className: cx(classes.childrenListItemAnchor, 'mfui-BaseMainNavigation__childrenListItemAnchor'), "aria-current": child.isCurrent ? 'page' : undefined, children: _jsx(Typography, { variant: "controlLabel", children: child.label }) }) }) }, childIndex))) })] })) }));
74
74
  }
75
75
  /**
76
76
  * The component for displaying nested navigation with a popover.
@@ -12,11 +12,12 @@ import { NarrowViewportMainNavigation } from './NarrowViewportMainNavigation';
12
12
  * `MainNavigation` is intended to be paired with `GlobalHeader`. If you need a self-contained
13
13
  * layout that already includes both header and navigation features, use `Sidebar` instead.
14
14
  */
15
- export const MainNavigation = forwardRef(({ className, featureShortcut, navigationItems, disableResponsive = false, width = 'normal', customLinkComponent, enableCollapsible, nestedNavigationDisplay, isCollapsed, onCollapsedChange, collapseButtonProps, expandButtonProps, }, ref) => {
15
+ export const MainNavigation = forwardRef(({ className, featureShortcut, navigationItems, navLandmarkLabel = 'メイン', disableResponsive = false, width = 'normal', customLinkComponent, enableCollapsible, nestedNavigationDisplay, isCollapsed, onCollapsedChange, collapseButtonProps, expandButtonProps, }, ref) => {
16
16
  const commonProps = {
17
17
  className,
18
18
  featureShortcut,
19
19
  navigationItems,
20
+ navLandmarkLabel,
20
21
  width,
21
22
  customLinkComponent,
22
23
  };
@@ -186,6 +186,15 @@ export type MainNavigationProps = {
186
186
  * The navigation items.
187
187
  */
188
188
  navigationItems: NavigationItem[];
189
+ /**
190
+ * The accessible name for the navigation landmark, applied as `aria-label` to both the sidebar `<nav>`
191
+ * and the narrow viewport drawer `<nav>`.
192
+ *
193
+ * Avoid including the role name (e.g. 'ナビゲーション'); assistive technology already announces it.
194
+ *
195
+ * @default 'メイン'
196
+ */
197
+ navLandmarkLabel?: string;
189
198
  /**
190
199
  * Whether the navigation is collapsible.
191
200
  *
@@ -13,7 +13,7 @@ import { Portal } from '../Portal';
13
13
  * The component for the main navigation on the narrow viewport.
14
14
  * This component must only be used in MainNavigation.tsx, do not export it.
15
15
  */
16
- export const NarrowViewportMainNavigation = forwardRef(({ className, featureShortcut, navigationItems, width = 'normal', customLinkComponent }, ref) => {
16
+ export const NarrowViewportMainNavigation = forwardRef(({ className, featureShortcut, navigationItems, navLandmarkLabel, width = 'normal', customLinkComponent }, ref) => {
17
17
  const classes = narrowViewportMainNavigationSlotRecipe({ width });
18
18
  const InternalLinkTag = customLinkComponent ?? 'a';
19
19
  const navId = useId();
@@ -48,12 +48,12 @@ export const NarrowViewportMainNavigation = forwardRef(({ className, featureShor
48
48
  }
49
49
  return (_jsx(Portal, { children: _jsxs("div", { ref: dialogRef, role: "dialog", className: cx(classes.root, 'mfui-NarrowViewportMainNavigation__root', className), tabIndex: -1, onKeyDown: handleOnKeyDown, children: [_jsx("div", { "data-mfui-content": "backdrop", className: cx(classes.backdrop, 'mfui-NarrowViewportMainNavigation__backdrop'), onClick: handleCloseMainNavigation }), _jsxs("div", { "data-mfui-content": "inside", className: cx(classes.container, 'mfui-NarrowViewportMainNavigation__container'), onClick: (event) => {
50
50
  event.stopPropagation();
51
- }, children: [featureShortcut ? (_jsx("div", { className: cx(classes.featureShortcut, 'mfui-NarrowViewportMainNavigation__featureShortcut'), children: _jsx(FocusIndicator, { children: _jsx(NavigationLink, { tag: InternalLinkTag, isExternal: featureShortcut.isExternal, href: featureShortcut.href, className: cx(classes.featureShortcutAnchor, 'mfui-NarrowViewportMainNavigation__featureShortcutAnchor'), onClick: handleCloseMainNavigation, children: _jsx(Typography, { variant: "controlLabel", children: featureShortcut.label }) }) }) })) : null, _jsxs("nav", { className: cx(classes.nav, 'mfui-NarrowViewportMainNavigation__nav'), children: [_jsx("ul", { className: cx(classes.list, 'mfui-NarrowViewportMainNavigation__list'), children: navigationItems.map((navigation, index) => {
51
+ }, children: [featureShortcut ? (_jsx("div", { className: cx(classes.featureShortcut, 'mfui-NarrowViewportMainNavigation__featureShortcut'), children: _jsx(FocusIndicator, { children: _jsx(NavigationLink, { tag: InternalLinkTag, isExternal: featureShortcut.isExternal, href: featureShortcut.href, className: cx(classes.featureShortcutAnchor, 'mfui-NarrowViewportMainNavigation__featureShortcutAnchor'), onClick: handleCloseMainNavigation, children: _jsx(Typography, { variant: "controlLabel", children: featureShortcut.label }) }) }) })) : null, _jsxs("nav", { "aria-label": navLandmarkLabel, className: cx(classes.nav, 'mfui-NarrowViewportMainNavigation__nav'), children: [_jsx("ul", { className: cx(classes.list, 'mfui-NarrowViewportMainNavigation__list'), children: navigationItems.map((navigation, index) => {
52
52
  const isGroupLabel = navigation.children && 'isGroupLabel' in navigation && navigation.isGroupLabel;
53
53
  if (isGroupLabel) {
54
54
  return null;
55
55
  }
56
- return (_jsx("li", { className: cx(classes.listItem, 'mfui-NarrowViewportMainNavigation__listItem'), children: navigation.children ? (_jsxs("details", { open: ('isOpenByDefault' in navigation && navigation.isOpenByDefault) ||
56
+ return (_jsx("li", { className: cx(classes.listItem, 'mfui-NarrowViewportMainNavigation__listItem'), children: navigation.children ? (_jsxs("details", { open: !!('isOpenByDefault' in navigation && navigation.isOpenByDefault) ||
57
57
  hasCurrentNavigationInChildren(navigation.children), className: cx(classes.parentDetails, 'mfui-NarrowViewportMainNavigation__parentDetails'), children: [_jsx(FocusIndicator, { position: "inside", children: _jsxs("summary", { className: cx(classes.parentSummary, 'mfui-NarrowViewportMainNavigation__parentSummary'), children: [_jsx("div", { "aria-hidden": "true", className: cx(classes.parentSummaryIcon, 'mfui-NarrowViewportMainNavigation__parentSummaryIcon'), children: navigation.icon }), _jsx(Typography, { variant: "controlLabel", children: navigation.label }), _jsx(DisclosureBasicCollapsed, { className: "mfui-NarrowViewportMainNavigation__icon_collapsed", "aria-hidden": "true", "data-mfui-content": "main-navigation-icon-collapsed" }), _jsx(DisclosureBasicExpanded, { className: "mfui-NarrowViewportMainNavigation__icon_expanded", "aria-hidden": "true", "data-mfui-content": "main-navigation-icon-expanded" })] }) }), _jsx("ul", { className: cx(classes.childrenList, 'mfui-NarrowViewportMainNavigation__childrenList'), children: navigation.children.map((child, childIndex) => (_jsx("li", { className: cx(classes.childrenListItem, 'mfui-NarrowViewportMainNavigation__childrenListItem'), children: _jsx(FocusIndicator, { position: "inside", children: _jsx(NavigationLink, { tag: InternalLinkTag, isExternal: child.isExternal, href: child.href, className: cx(classes.childrenListItemAnchor, 'mfui-NarrowViewportMainNavigation__childrenListItemAnchor'), "aria-current": child.isCurrent ? 'page' : undefined, onClick: handleCloseMainNavigation, children: _jsx(Typography, { variant: "controlLabel", children: child.label }) }) }) }, childIndex))) })] })) : (_jsx(FocusIndicator, { position: "inside", children: _jsx(NavigationLink, { tag: InternalLinkTag, isExternal: navigation.isExternal, href: navigation.href, className: cx(classes.listItemAnchor, 'mfui-NarrowViewportMainNavigation__listItemAnchor'), "aria-current": navigation.isCurrent ? 'page' : undefined, onClick: handleCloseMainNavigation, children: _jsxs("div", { className: cx(classes.labelGroup, 'mfui-NarrowViewportMainNavigation__labelGroup'), children: [_jsxs("div", { className: cx(classes.iconAndLabel, 'mfui-NarrowViewportMainNavigation__iconAndLabel'), children: [_jsx("div", { "aria-hidden": "true", className: cx(classes.listItemAnchorIcon, 'mfui-NarrowViewportMainNavigation__listItemAnchorIcon'), children: navigation.icon }), _jsx(Typography, { variant: "controlLabel", children: navigation.label })] }), navigation.locked ? (_jsx(Lock, { className: cx(classes.lockIcon, 'mfui-NarrowViewportMainNavigation__lockIcon'), role: "img", "aria-label": navigation.lockIconProps?.['aria-label'] ?? 'ロックされています' })) : null] }) }) })) }, index));
58
58
  }) }), navigationItems.map((navigation, index) => {
59
59
  const isGroupLabel = navigation.children && 'isGroupLabel' in navigation && navigation.isGroupLabel;
@@ -339,29 +339,38 @@ placeholder, emptyMessage, disabled, invalid, required, caution, targetDOMNode,
339
339
  `${String(localSelectedValuesSet.size)}件選択中`) })) : null;
340
340
  const applyControlsNode = enableApplyControls ? (_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;
341
341
  const resolvedNotFoundMessage = typeof notFoundMessage === 'function' ? notFoundMessage(searchText.trim()) : notFoundMessage;
342
+ // Early returns rather than a four-level ternary: the branches are mutually exclusive
343
+ // and each returns a different shape (skeletons, virtualized rows, options, or the
344
+ // empty message).
345
+ const renderListContent = () => {
346
+ if (loading) {
347
+ return Array.from({ length: SKELETON_ITEM_COUNT }).map((_, index) => (_jsx("li", { className: cx(classes.skeletonItem, 'mfui-MultipleSelectBox__skeletonItem'), children: _jsx(Skeleton, {}) }, index)));
348
+ }
349
+ if (isVirtualized && virtualItems.length > 0) {
350
+ return renderVirtualizedItems();
351
+ }
352
+ if (filteredOptions.length > 0) {
353
+ return [
354
+ ...renderNonVirtualizedItems(),
355
+ !isVirtualized && renderInfiniteScrollLoading(),
356
+ !isVirtualized && renderInfiniteScrollError(),
357
+ ].filter(Boolean);
358
+ }
359
+ return [
360
+ _jsx("li", { className: cx(classes.emptyMessage, 'mfui-MultipleSelectBox__emptyMessage'), children: _jsx(Typography, { variant: "body", children: enableSearchOptions && searchText && resolvedNotFoundMessage
361
+ ? resolvedNotFoundMessage
362
+ : options.length > 0
363
+ ? resolvedNotFoundMessage
364
+ : emptyMessage }) }, "empty"),
365
+ !isVirtualized && renderInfiniteScrollError(),
366
+ ].filter(Boolean);
367
+ };
342
368
  const optionsNode = (_jsxs("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
343
369
  ? {
344
370
  height: `${String(totalSize)}px`,
345
371
  position: 'relative',
346
372
  }
347
- : undefined, children: loading
348
- ? Array.from({ length: SKELETON_ITEM_COUNT }).map((_, index) => (_jsx("li", { className: cx(classes.skeletonItem, 'mfui-MultipleSelectBox__skeletonItem'), children: _jsx(Skeleton, {}) }, index)))
349
- : isVirtualized && virtualItems.length > 0
350
- ? renderVirtualizedItems()
351
- : filteredOptions.length > 0
352
- ? [
353
- ...renderNonVirtualizedItems(),
354
- !isVirtualized && renderInfiniteScrollLoading(),
355
- !isVirtualized && renderInfiniteScrollError(),
356
- ].filter(Boolean)
357
- : [
358
- _jsx("li", { className: cx(classes.emptyMessage, 'mfui-MultipleSelectBox__emptyMessage'), children: _jsx(Typography, { variant: "body", children: enableSearchOptions && searchText && resolvedNotFoundMessage
359
- ? resolvedNotFoundMessage
360
- : options.length > 0
361
- ? resolvedNotFoundMessage
362
- : emptyMessage }) }, "empty"),
363
- !isVirtualized && renderInfiniteScrollError(),
364
- ].filter(Boolean) }), isVirtualized ? renderInfiniteScrollLoading() : null, isVirtualized ? renderInfiniteScrollError('div') : null, _jsx(OptionGroupLabelDescriptions, { groups: rendersHiddenGroupLabels ? optionGroupLabels : [] })] }));
373
+ : undefined, children: renderListContent() }), isVirtualized ? renderInfiniteScrollLoading() : null, isVirtualized ? renderInfiniteScrollError('div') : null, _jsx(OptionGroupLabelDescriptions, { groups: rendersHiddenGroupLabels ? optionGroupLabels : [] })] }));
365
374
  const createOptionAreaNode = isCreateOptionVisible ? (_jsxs("div", { className: cx(classes.createOptionArea, 'mfui-MultipleSelectBox__createOptionArea'), children: [_jsxs("button", { "data-mfui-content": "create-new-option", type: "button", className: cx(classes.createOptionButton, 'mfui-MultipleSelectBox__createOptionButton'), disabled: isCreating, onClick: handleCreateOption, children: [_jsx(Add, { "aria-hidden": true }), createOptionProps?.renderLabel?.(searchText.trim()) ?? (_jsxs(_Fragment, { children: [_jsx(Typography, { variant: "strongBody", children: searchText.trim() }), _jsx(Typography, { variant: "body", children: "\u3092\u8FFD\u52A0" })] }))] }), createError ? (_jsx("div", { className: cx(classes.createOptionErrorMessage, 'mfui-MultipleSelectBox__createOptionErrorMessage'), children: _jsx(HelpMessage, { messageType: "error", children: createOptionProps?.renderErrorMessage?.(searchText.trim()) ??
366
375
  `${searchText.trim()}を追加できませんでした。` }) })) : null] })) : null;
367
376
  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,
@@ -48,6 +48,6 @@ placeholder, renderDisplayValue, updateSelectedValues, clearButtonProps, disable
48
48
  .map((option) => option.value)
49
49
  .filter((v) => v != null)
50
50
  .map(String);
51
- return (_jsx(FocusIndicator, { children: _jsxs("div", { ref: wrapperRef, className: cx(classes.wrapper, 'mfui-MultipleSelectBoxTrigger__wrapper', triggerWrapperProps?.className), children: [_jsx("button", { ref: mergeRefs(ref, triggerRef), type: "button", role: "combobox", "aria-controls": isOptionPanelOpen ? listBoxId : undefined, "aria-expanded": isOptionPanelOpen, "aria-haspopup": "listbox", "aria-invalid": invalid, "aria-required": required || undefined, "data-mfui-caution": caution && !invalid ? true : undefined, value: selectedValues, disabled: disabled, onKeyDown: onKeyDown, onBlur: onBlur, ...rest, ...triggerProps, className: cx(classes.trigger, 'mfui-MultipleSelectBoxTrigger__trigger', triggerProps?.className) }), _jsx("input", { type: "hidden", name: name, value: selectedValues }), _jsx("div", { className: cx(classes.displayValues, 'mfui-MultipleSelectBoxTrigger__displayValues'), "data-mfui-content": "multiple-select-box-trigger-display-values", children: renderer() }), selectedOptions.length > 0 && !disabled && !disableClearButton ? (_jsx("div", { className: cx(classes.clearButtonContainer, 'mfui-MultipleSelectBoxTrigger__clearButtonContainer'), children: _jsx(ClearButton, { size: size === 'small' ? 'small' : 'default', className: cx(classes.clearButton, 'mfui-MultipleSelectBoxTrigger__clearButton'), "aria-label": clearButtonProps?.['aria-label'] ?? '値をクリアする', "data-mfui-content": "multiple-select-box-clear-button", onClick: handleClearValue }) })) : null, _jsx("div", { className: cx(classes.dropdownIconContainer, 'mfui-MultipleSelectBoxTrigger__dropdownIconContainer'), children: _jsx(DropdownIcon, { "aria-hidden": "true", className: cx(classes.dropdownIcon, 'mfui-MultipleSelectBoxTrigger__dropdownIcon') }) })] }) }));
51
+ return (_jsx(FocusIndicator, { children: _jsxs("div", { ref: wrapperRef, className: cx(classes.wrapper, 'mfui-MultipleSelectBoxTrigger__wrapper', triggerWrapperProps?.className), children: [_jsx("button", { ref: mergeRefs(ref, triggerRef), type: "button", role: "combobox", "aria-controls": isOptionPanelOpen ? listBoxId : undefined, "aria-expanded": isOptionPanelOpen, "aria-haspopup": "listbox", "aria-invalid": invalid, "aria-required": required ? true : undefined, "data-mfui-caution": caution && !invalid ? true : undefined, value: selectedValues, disabled: disabled, onKeyDown: onKeyDown, onBlur: onBlur, ...rest, ...triggerProps, className: cx(classes.trigger, 'mfui-MultipleSelectBoxTrigger__trigger', triggerProps?.className) }), _jsx("input", { type: "hidden", name: name, value: selectedValues }), _jsx("div", { className: cx(classes.displayValues, 'mfui-MultipleSelectBoxTrigger__displayValues'), "data-mfui-content": "multiple-select-box-trigger-display-values", children: renderer() }), selectedOptions.length > 0 && !disabled && !disableClearButton ? (_jsx("div", { className: cx(classes.clearButtonContainer, 'mfui-MultipleSelectBoxTrigger__clearButtonContainer'), children: _jsx(ClearButton, { size: size === 'small' ? 'small' : 'default', className: cx(classes.clearButton, 'mfui-MultipleSelectBoxTrigger__clearButton'), "aria-label": clearButtonProps?.['aria-label'] ?? '値をクリアする', "data-mfui-content": "multiple-select-box-clear-button", onClick: handleClearValue }) })) : null, _jsx("div", { className: cx(classes.dropdownIconContainer, 'mfui-MultipleSelectBoxTrigger__dropdownIconContainer'), children: _jsx(DropdownIcon, { "aria-hidden": "true", className: cx(classes.dropdownIcon, 'mfui-MultipleSelectBoxTrigger__dropdownIcon') }) })] }) }));
52
52
  }
53
53
  export const MultipleSelectBoxTrigger = forwardRef(MultipleSelectBoxTriggerInner);
@@ -1,5 +1,5 @@
1
1
  'use client';
2
- import { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import { useCallback, useMemo, useState } from 'react';
3
3
  import { isSelectableOption } from '../utils/isSelectableOption';
4
4
  /**
5
5
  * Custom hook to manage the "Create New Option" feature for MultipleSelectBox.
@@ -20,10 +20,14 @@ export const useCreateOption = ({ enableCreateOption, enableSearchOptions, searc
20
20
  const exactMatchExists = filteredOptions.some((option) => isSelectableOption(option) && option.label === trimmedSearchText);
21
21
  return !exactMatchExists;
22
22
  }, [enableCreateOption, enableSearchOptions, trimmedSearchText, filteredOptions]);
23
- // Clear error state when search text changes
24
- useEffect(() => {
23
+ // Adjusted during render (React's documented pattern for resyncing state to a changed
24
+ // prop) rather than in an effect. Clears the error banner as soon as the search text
25
+ // changes, matching the comment above the effect this replaces.
26
+ const [prevSearchText, setPrevSearchText] = useState(searchText);
27
+ if (searchText !== prevSearchText) {
28
+ setPrevSearchText(searchText);
25
29
  setCreateError(false);
26
- }, [searchText]);
30
+ }
27
31
  const doCreate = useCallback(async () => {
28
32
  try {
29
33
  await onCreateOption?.(trimmedSearchText);
@@ -1,5 +1,5 @@
1
1
  import { useEffect } from 'react';
2
- import { getDeepActiveElement } from '../utils/getDeepActiveElement';
2
+ import { getDeepActiveElement } from '../../utilities/dom/getDeepActiveElement';
3
3
  import { isSelectableOption } from '../utils/isSelectableOption';
4
4
  /**
5
5
  * Control the focus when the option panel is opened.
@@ -1,4 +1,4 @@
1
- import { useState, useCallback, useEffect } from 'react';
1
+ import { useState, useCallback } from 'react';
2
2
  import { findSiblingElements } from '../../utilities/dom/findSiblingElements';
3
3
  import { isSelectableOption, isOptionDisabled } from '../utils/isSelectableOption';
4
4
  /**
@@ -27,10 +27,15 @@ export function useOptionKeyboardNavigation({ filteredOptions, selectedValues, o
27
27
  }, [filteredOptions, selectedValues]);
28
28
  // State to track which option should be tabbable
29
29
  const [tabbableOptionIndex, setTabbableOptionIndex] = useState(getDefaultTabbableIndex());
30
- // Update tabbable index when filtered options or selected values change
31
- useEffect(() => {
30
+ // Adjusted during render (React's documented pattern for resyncing state to changed
31
+ // props) rather than in an effect, so the UI never flashes the stale index for one frame.
32
+ const [prevFilteredOptions, setPrevFilteredOptions] = useState(filteredOptions);
33
+ const [prevSelectedValues, setPrevSelectedValues] = useState(selectedValues);
34
+ if (filteredOptions !== prevFilteredOptions || selectedValues !== prevSelectedValues) {
35
+ setPrevFilteredOptions(filteredOptions);
36
+ setPrevSelectedValues(selectedValues);
32
37
  setTabbableOptionIndex(getDefaultTabbableIndex());
33
- }, [getDefaultTabbableIndex]);
38
+ }
34
39
  // Update tabbable index when selected values change
35
40
  const resetTabbableIndex = useCallback(() => {
36
41
  setTabbableOptionIndex(getDefaultTabbableIndex());
@@ -1,4 +1,4 @@
1
- import { useState, useEffect, useMemo } from 'react';
1
+ import { useState, useMemo } from 'react';
2
2
  /**
3
3
  * Helper function to convert defaultValue/value to options array and values set
4
4
  *
@@ -34,66 +34,32 @@ export function useSelectedValues({ options, defaultValue, value, onChange }) {
34
34
  // Store selected options with their full data (including labels)
35
35
  const [selectedOptionsMap, setSelectedOptionsMap] = useState(() => {
36
36
  // For initialization, use value if provided, otherwise use defaultValue
37
- const initialValue = value !== undefined ? value : defaultValue;
37
+ const initialValue = value ?? defaultValue;
38
38
  const { optionsMap } = initializeSelectedOptions(initialValue);
39
39
  return optionsMap;
40
40
  });
41
41
  // Store selected values as a Set for fast lookup
42
42
  const [localSelectedValuesSet, setLocalSelectedValuesSet] = useState(() => {
43
43
  // For initialization, use value if provided, otherwise use defaultValue
44
- const initialValue = value !== undefined ? value : defaultValue;
44
+ const initialValue = value ?? defaultValue;
45
45
  const { valuesSet } = initializeSelectedOptions(initialValue);
46
46
  return valuesSet;
47
47
  });
48
48
  // Derive local selected options array from the map
49
49
  const localSelectedOptions = useMemo(() => [...selectedOptionsMap.values()], [selectedOptionsMap]);
50
- // Update local state when controlled value changes
51
- useEffect(() => {
52
- if (!isControlled) {
53
- return;
54
- }
55
- const { optionsMap, valuesSet } = initializeSelectedOptions(value);
56
- // Preserve existing options from the map if they're not in the new options array
57
- setSelectedOptionsMap((previousMap) => {
58
- const newOptionsMap = new Map(optionsMap);
59
- for (const selectedValue of valuesSet) {
60
- if (!newOptionsMap.has(selectedValue) && previousMap.has(selectedValue)) {
61
- // Preserve the option from the existing map
62
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
63
- newOptionsMap.set(selectedValue, previousMap.get(selectedValue));
64
- }
65
- }
66
- return newOptionsMap;
67
- });
68
- setLocalSelectedValuesSet(valuesSet);
69
- }, [value, options, isControlled]);
70
- // Sync state for display-only mode (value provided but no onChange)
71
- // This handles scenarios like disabled/display-only components where
72
- // the parent provides values but doesn't expect interaction
73
- useEffect(() => {
74
- // Skip if this is controlled (already handled above) or uncontrolled (no value)
75
- if (isControlled || value === undefined) {
76
- return;
50
+ // Adjusted during render (React's documented pattern for resyncing state to a changed
51
+ // prop) rather than in an effect, so the UI never flashes the stale value for one
52
+ // frame. Covers both controlled (value + onChange) and display-only (value, no
53
+ // onChange) modes; uncontrolled mode (value === undefined) is left untouched.
54
+ const [prevValue, setPrevValue] = useState(value);
55
+ if (value !== prevValue) {
56
+ setPrevValue(value);
57
+ if (value !== undefined) {
58
+ const { optionsMap, valuesSet } = initializeSelectedOptions(value);
59
+ setSelectedOptionsMap(optionsMap);
60
+ setLocalSelectedValuesSet(valuesSet);
77
61
  }
78
- // This is display-only mode: value is provided but no onChange
79
- // Treat it like controlled for state sync purposes
80
- const { optionsMap, valuesSet } = initializeSelectedOptions(value);
81
- // Preserve existing options from the map if they're not in the new options array
82
- setSelectedOptionsMap((previousMap) => {
83
- const newOptionsMap = new Map(optionsMap);
84
- for (const selectedValue of valuesSet) {
85
- if (!newOptionsMap.has(selectedValue) && previousMap.has(selectedValue)) {
86
- // Preserve the option from the existing map
87
- const existingOption = previousMap.get(selectedValue);
88
- if (existingOption) {
89
- newOptionsMap.set(selectedValue, existingOption);
90
- }
91
- }
92
- }
93
- return newOptionsMap;
94
- });
95
- setLocalSelectedValuesSet(valuesSet);
96
- }, [value, options, isControlled]);
62
+ }
97
63
  // Helper function to update the selected values
98
64
  const updateSelectedValues = (newValues) => {
99
65
  const newValuesSet = new Set(newValues);
@@ -115,7 +81,7 @@ export function useSelectedValues({ options, defaultValue, value, onChange }) {
115
81
  }
116
82
  // Controlled component: Don't update local state directly
117
83
  // Let the parent component control the state via value prop
118
- // The useEffect will sync local state when value prop changes
84
+ // The render-time adjustment above will sync local state when value prop changes
119
85
  if (isControlled) {
120
86
  const selectedOptions = [...newOptionsMap.values()];
121
87
  onChange(selectedOptions);
@@ -6,5 +6,8 @@ import { type NavigationListItemProps } from './NavigationListItem.types';
6
6
  * If the href prop is provided, the item will be rendered as an anchor element.
7
7
  * If the onClick prop is provided (without href), it will be rendered as a button element.
8
8
  * Otherwise, it will be rendered as a div element and not interactive.
9
+ *
10
+ * The trailing navigation indicator is displayed whenever the item can be activated,
11
+ * that is when either the href or the onClick prop is provided.
9
12
  */
10
13
  export declare const NavigationListItem: import("react").ForwardRefExoticComponent<NavigationListItemProps & import("react").RefAttributes<HTMLLIElement>>;
@@ -16,6 +16,9 @@ const ARIA_DISABLED_PROPS = { 'aria-disabled': true, 'data-mfui-disabled': '' };
16
16
  * If the href prop is provided, the item will be rendered as an anchor element.
17
17
  * If the onClick prop is provided (without href), it will be rendered as a button element.
18
18
  * Otherwise, it will be rendered as a div element and not interactive.
19
+ *
20
+ * The trailing navigation indicator is displayed whenever the item can be activated,
21
+ * that is when either the href or the onClick prop is provided.
19
22
  */
20
23
  export const NavigationListItem = forwardRef(({ title, icon, description, leadingContent, href, target, customLinkComponent, onClick, disabled, className, ...rest }, ref) => {
21
24
  const classes = navigationListItemSlotRecipe();
@@ -26,7 +29,8 @@ export const NavigationListItem = forwardRef(({ title, icon, description, leadin
26
29
  return 'button';
27
30
  return 'div';
28
31
  }, [href, customLinkComponent, onClick]);
29
- const isInteractive = (href !== undefined || onClick !== undefined) && !disabled;
32
+ const isActivatable = href !== undefined || onClick !== undefined;
33
+ const isInteractive = isActivatable && !disabled;
30
34
  const resolvedHref = disabled ? undefined : href;
31
35
  const disabledProps = useMemo(() => {
32
36
  if (!disabled)
@@ -35,6 +39,6 @@ export const NavigationListItem = forwardRef(({ title, icon, description, leadin
35
39
  return BUTTON_DISABLED_PROPS;
36
40
  return ARIA_DISABLED_PROPS;
37
41
  }, [disabled, onClick, href]);
38
- const innerElement = (_jsx(FocusIndicator, { hideWhiteBorder: true, position: "inside", children: _jsxs(Tag, { ...rest, href: resolvedHref, target: target, type: href === undefined && onClick !== undefined ? 'button' : undefined, "data-mfui-focusable": isInteractive, className: cx(classes.root, 'mfui-NavigationListItem__root', className), onClick: disabled ? undefined : onClick, ...disabledProps, children: [leadingContent != null ? (_jsx("span", { className: cx(classes.leading, 'mfui-NavigationListItem__leading'), children: leadingContent })) : null, _jsxs("span", { className: cx(classes.content, 'mfui-NavigationListItem__content'), children: [_jsxs("span", { className: cx(classes.title, 'mfui-NavigationListItem__title'), children: [icon != null ? (_jsx("span", { className: cx(classes.titleIcon, 'mfui-NavigationListItem__titleIcon'), children: icon })) : null, _jsx(Typography, { variant: "contentHeading", children: title })] }), description != null ? _jsx(Typography, { variant: "body", children: description }) : null] }), href !== undefined ? (_jsx("span", { className: cx(classes.trailing, 'mfui-NavigationListItem__trailing'), children: _jsx(NavigationToDetail, { "aria-hidden": true }) })) : null] }) }));
42
+ const innerElement = (_jsx(FocusIndicator, { hideWhiteBorder: true, position: "inside", children: _jsxs(Tag, { ...rest, href: resolvedHref, target: target, type: href === undefined && onClick !== undefined ? 'button' : undefined, "data-mfui-focusable": isInteractive, className: cx(classes.root, 'mfui-NavigationListItem__root', className), onClick: disabled ? undefined : onClick, ...disabledProps, children: [leadingContent != null ? (_jsx("span", { className: cx(classes.leading, 'mfui-NavigationListItem__leading'), children: leadingContent })) : null, _jsxs("span", { className: cx(classes.content, 'mfui-NavigationListItem__content'), children: [_jsxs("span", { className: cx(classes.title, 'mfui-NavigationListItem__title'), children: [icon != null ? (_jsx("span", { className: cx(classes.titleIcon, 'mfui-NavigationListItem__titleIcon'), children: icon })) : null, _jsx(Typography, { variant: "contentHeading", children: title })] }), description != null ? _jsx(Typography, { variant: "body", children: description }) : null] }), isActivatable ? (_jsx("span", { className: cx(classes.trailing, 'mfui-NavigationListItem__trailing'), children: _jsx(NavigationToDetail, { "aria-hidden": true }) })) : null] }) }));
39
43
  return _jsx("li", { ref: ref, children: innerElement });
40
44
  });
@@ -25,7 +25,8 @@ type ButtonProps = {
25
25
  customLinkComponent?: undefined;
26
26
  /**
27
27
  * The click handler for the navigation item.
28
- * When provided without href, the item is rendered as a button element.
28
+ * When provided without href, the item is rendered as a button element
29
+ * and displays the same trailing navigation indicator as an anchor item.
29
30
  */
30
31
  onClick: ComponentPropsWithoutRef<'button'>['onClick'];
31
32
  /**
@@ -5,8 +5,13 @@ import React from 'react';
5
5
  * and ignores invalid characters.
6
6
  *
7
7
  * The `onChange` callback receives the raw numeric string without commas.
8
+ *
9
+ * `maxIntegerDigits` and `decimalPlaces` cap how much the user can enter. Edits that would
10
+ * exceed either limit are silently rejected, so state the limit in an adjacent `HelpMessage`.
8
11
  */
9
12
  export declare const NumberBox: React.ForwardRefExoticComponent<Omit<import("..").TextBoxProps, "onChange" | "prefixSlot" | "suffixSlot"> & {
10
13
  onChange?: (value: string) => void;
11
14
  textAlign?: 'left' | 'right';
15
+ maxIntegerDigits?: number;
16
+ decimalPlaces?: number;
12
17
  } & React.RefAttributes<HTMLInputElement>>;
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { forwardRef, useCallback, useEffect, useLayoutEffect, useReducer, useRef, useState } from 'react';
3
+ import { forwardRef, useCallback, useLayoutEffect, useReducer, useRef, useState } from 'react';
4
4
  import { cx } from '../../styled-system/css';
5
5
  import { mergeRefs } from '../utilities/dom/mergeRefs';
6
6
  import { TextBox } from '../TextBox';
@@ -102,25 +102,64 @@ const mapRawCursorToDisplay = (formatted, rawCursor) => {
102
102
  }
103
103
  return formatted.length;
104
104
  };
105
+ /**
106
+ * Count the digit characters in a raw numeric string.
107
+ * The minus sign and the decimal point are not digits, so they are not counted.
108
+ *
109
+ * @param raw - The raw numeric string.
110
+ */
111
+ const countDigits = (raw) => (raw.match(/\d/g) ?? []).length;
112
+ /**
113
+ * Count the digit characters in the integer part of a raw numeric string, i.e. before the
114
+ * decimal point. `maxIntegerDigits` caps this count independently of `decimalPlaces`, so the
115
+ * two limits stack: `maxIntegerDigits={5}` with `decimalPlaces={2}` allows up to 7 digits total.
116
+ *
117
+ * @param raw - The raw numeric string.
118
+ */
119
+ const countIntegerDigits = (raw) => {
120
+ const dotIndex = raw.indexOf('.');
121
+ return countDigits(dotIndex === -1 ? raw : raw.slice(0, dotIndex));
122
+ };
123
+ /**
124
+ * Count the decimal places a raw numeric string occupies.
125
+ * A trailing dot counts as one place because it commits the value to a decimal part:
126
+ * that is what lets `decimalPlaces={0}` reject the dot itself while `decimalPlaces={2}`
127
+ * still allows the in-progress `123.`.
128
+ *
129
+ * @param raw - The raw numeric string.
130
+ */
131
+ const countDecimalPlaces = (raw) => {
132
+ const dotIndex = raw.indexOf('.');
133
+ if (dotIndex === -1)
134
+ return 0;
135
+ return Math.max(raw.length - dotIndex - 1, 1);
136
+ };
105
137
  /**
106
138
  * NumberBox is a numeric input component that wraps TextBox.
107
139
  * It provides real-time comma formatting, full-width digit conversion,
108
140
  * and ignores invalid characters.
109
141
  *
110
142
  * The `onChange` callback receives the raw numeric string without commas.
143
+ *
144
+ * `maxIntegerDigits` and `decimalPlaces` cap how much the user can enter. Edits that would
145
+ * exceed either limit are silently rejected, so state the limit in an adjacent `HelpMessage`.
111
146
  */
112
- export const NumberBox = forwardRef(({ onChange, value, defaultValue, textAlign = 'right', style, enableClearButton, ...rest }, ref) => {
147
+ export const NumberBox = forwardRef(({ onChange, value, defaultValue, textAlign = 'right', style, enableClearButton, maxIntegerDigits, decimalPlaces, ...rest }, ref) => {
113
148
  const inputRef = useRef(null);
114
149
  const pendingCursorRef = useRef(null);
115
150
  const [cursorRestoreToken, requestCursorRestore] = useReducer((count) => count + 1, 0);
116
151
  const [displayValue, setDisplayValue] = useState(() => formatWithCommas(sanitizeInput(String(value ?? defaultValue ?? ''))));
117
- // Sync with controlled value
118
- useEffect(() => {
152
+ // Adjusted during render (React's documented pattern for resyncing state to a
153
+ // changed prop) rather than in an effect, so the UI never flashes the stale value
154
+ // for one frame. Only fires when value prop is explicitly provided (controlled mode).
155
+ const [prevValue, setPrevValue] = useState(value);
156
+ if (value !== prevValue) {
157
+ setPrevValue(value);
119
158
  if (value !== undefined) {
120
159
  const sanitized = sanitizeInput(String(value));
121
160
  setDisplayValue(formatWithCommas(sanitized));
122
161
  }
123
- }, [value]);
162
+ }
124
163
  // Restore cursor position after display value update
125
164
  useLayoutEffect(() => {
126
165
  if (pendingCursorRef.current !== null && inputRef.current) {
@@ -165,6 +204,22 @@ export const NumberBox = forwardRef(({ onChange, value, defaultValue, textAlign
165
204
  rawCursor = rawPos - 1;
166
205
  }
167
206
  }
207
+ // Reject an edit that breaks a limit, but only when it makes that measure worse.
208
+ // A value that arrives over the limit from the outside must stay editable back into range.
209
+ const exceedsDigitLimit = maxIntegerDigits !== undefined &&
210
+ countIntegerDigits(sanitized) > maxIntegerDigits &&
211
+ countIntegerDigits(sanitized) > countIntegerDigits(prevSanitized);
212
+ const exceedsDecimalLimit = decimalPlaces !== undefined &&
213
+ countDecimalPlaces(sanitized) > decimalPlaces &&
214
+ countDecimalPlaces(sanitized) > countDecimalPlaces(prevSanitized);
215
+ if (exceedsDigitLimit || exceedsDecimalLimit) {
216
+ // The display value does not change, so the cursor belongs before the rejected input.
217
+ const rawGained = Math.max(sanitized.length - prevSanitized.length, 0);
218
+ pendingCursorRef.current = mapRawCursorToDisplay(displayValue, Math.max(rawCursor - rawGained, 0));
219
+ // State is unchanged, so bump the token to make the layout effect restore the input.
220
+ requestCursorRestore();
221
+ return;
222
+ }
168
223
  const formatted = formatWithCommas(sanitized);
169
224
  let newCursor = mapRawCursorToDisplay(formatted, rawCursor);
170
225
  // Rejected characters keep the cursor before the ignored input.
@@ -179,7 +234,7 @@ export const NumberBox = forwardRef(({ onChange, value, defaultValue, textAlign
179
234
  requestCursorRestore();
180
235
  }
181
236
  onChange?.(sanitized);
182
- }, [displayValue, onChange]);
237
+ }, [displayValue, onChange, maxIntegerDigits, decimalPlaces]);
183
238
  return (_jsx(TextBox, { ref: mergeRefs(ref, inputRef), ...rest, className: cx('mfui-NumberBox__root', rest.className), enableClearButton: Boolean(enableClearButton) && displayValue !== '', style: { textAlign, ...style }, value: displayValue, onChange: handleChange }));
184
239
  });
185
240
  NumberBox.displayName = 'NumberBox';