@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.
- package/dist/src/Accordion/Accordion.d.ts +21 -1
- package/dist/src/Accordion/Accordion.js +28 -4
- package/dist/src/Accordion/Accordion.types.d.ts +21 -0
- package/dist/src/Badge/Badge.d.ts +1 -1
- package/dist/src/Badge/Badge.js +4 -4
- package/dist/src/Badge/Badge.types.d.ts +3 -1
- package/dist/src/Breadcrumbs/Breadcrumbs.js +1 -1
- package/dist/src/Checkbox/Checkbox.js +7 -3
- package/dist/src/CheckboxCard/CheckboxCard.js +3 -1
- package/dist/src/DataTable/DataTable.js +9 -2
- package/dist/src/DataTable/DataTable.types.d.ts +17 -2
- package/dist/src/DataTable/DataTableCell/DataTableCell.js +21 -4
- package/dist/src/DataTable/DataTableCell/DataTableCell.types.d.ts +1 -1
- package/dist/src/DataTable/DataTableHeaderRow/DataTableHeaderRow.js +9 -5
- package/dist/src/DataTable/DataTableHeaderRow/DataTableHeaderRow.types.d.ts +19 -1
- package/dist/src/DataTable/DataTableRow/DataTableRow.js +106 -31
- package/dist/src/DataTable/DataTableRow/DataTableRow.types.d.ts +63 -12
- package/dist/src/DataTable/DataTableRowActionContext.d.ts +34 -0
- package/dist/src/DataTable/DataTableRowActionContext.js +8 -0
- package/dist/src/DateTimeSelection/DatePicker/DatePickerCalendar/DatePickerCalendar.js +7 -3
- package/dist/src/DateTimeSelection/DatePicker/DatePickerDateCell/index.d.ts +1 -1
- package/dist/src/DateTimeSelection/DatePicker/DatePickerDateCell/index.js +0 -1
- package/dist/src/DateTimeSelection/DateRangePicker/DateRangePickerPopover/DateRangePickerCalendar/DateRangePickerCalendar.js +1 -1
- package/dist/src/DateTimeSelection/DateRangePicker/DateRangePickerPopover/utilities/createDateRangePickerPopoverTestUtility.js +2 -2
- package/dist/src/DateTimeSelection/DateRangePicker/DateRangePickerProvider/DateRangePickerProvider.js +13 -7
- package/dist/src/DateTimeSelection/DateRangePicker/DateRangePickerTrigger/DateRangePickerTrigger.js +1 -1
- package/dist/src/DateTimeSelection/FilterDatePicker/FilterDatePicker.js +1 -1
- package/dist/src/DateTimeSelection/FilterMonthPicker/FilterMonthPicker.js +1 -1
- package/dist/src/DateTimeSelection/FilterMonthRangePicker/FilterMonthRangePicker.js +1 -1
- package/dist/src/DateTimeSelection/MonthPicker/MonthPickerPanel/MonthPickerPanel.js +1 -1
- package/dist/src/DateTimeSelection/MonthPicker/constants.d.ts +2 -2
- package/dist/src/DateTimeSelection/MonthPicker/constants.js +6 -6
- package/dist/src/DateTimeSelection/MonthRangePicker/MonthRangePickerMonthCell/MonthRangePickerMonthCell.js +1 -1
- package/dist/src/DateTimeSelection/MonthRangePicker/MonthRangePickerNavigation/MonthRangePickerNavigation.js +2 -2
- package/dist/src/DateTimeSelection/TimePicker/TimePicker.js +1 -1
- package/dist/src/DateTimeSelection/shared/BasePicker/constants.d.ts +2 -2
- package/dist/src/DateTimeSelection/shared/BasePicker/constants.js +10 -10
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerPopover/BaseRangePickerCalendar/BaseRangePickerCalendar.js +1 -1
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerPopover/constants.js +4 -4
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerProvider/BaseRangePickerProvider.js +13 -7
- package/dist/src/DateTimeSelection/shared/BaseRangePicker/BaseRangePickerTrigger/BaseRangePickerTrigger.js +2 -2
- package/dist/src/DateTimeSelection/shared/CalendarGrid/CalendarGrid.js +2 -4
- package/dist/src/DateTimeSelection/shared/utilities/japaneseCalendar.js +2 -2
- package/dist/src/Dialog/Dialog.d.ts +1 -1
- package/dist/src/Dialog/Dialog.types.d.ts +1 -1
- package/dist/src/FileDropZone/FileDropZone.js +1 -1
- package/dist/src/FileDropZone/utils/fileHelpers.js +1 -1
- package/dist/src/MainNavigation/BaseMainNavigation.d.ts +1 -1
- package/dist/src/MainNavigation/BaseMainNavigation.js +3 -3
- package/dist/src/MainNavigation/MainNavigation.js +2 -1
- package/dist/src/MainNavigation/MainNavigation.types.d.ts +9 -0
- package/dist/src/MainNavigation/NarrowViewportMainNavigation.js +3 -3
- package/dist/src/MultipleSelectBox/MultipleSelectBox.js +27 -18
- package/dist/src/MultipleSelectBox/MultipleSelectBoxTrigger/MultipleSelectBoxTrigger.js +1 -1
- package/dist/src/MultipleSelectBox/hooks/useCreateOption.js +8 -4
- package/dist/src/MultipleSelectBox/hooks/useInitialFocusOnOptionPanelOpen.js +1 -1
- package/dist/src/MultipleSelectBox/hooks/useOptionKeyboardNavigation.js +9 -4
- package/dist/src/MultipleSelectBox/hooks/useSelectedValues.js +16 -50
- package/dist/src/NavigationList/NavigationListItem/NavigationListItem.d.ts +3 -0
- package/dist/src/NavigationList/NavigationListItem/NavigationListItem.js +6 -2
- package/dist/src/NavigationList/NavigationListItem/NavigationListItem.types.d.ts +2 -1
- package/dist/src/NumberBox/NumberBox.d.ts +5 -0
- package/dist/src/NumberBox/NumberBox.js +61 -6
- package/dist/src/NumberBox/NumberBox.types.d.ts +35 -0
- package/dist/src/PageHeader/PageHeader.types.d.ts +2 -0
- package/dist/src/Pagination/Pagination.js +4 -1
- package/dist/src/Popover/Popover.js +12 -4
- package/dist/src/RadioButtonCard/RadioButtonCard.js +3 -1
- package/dist/src/SelectBox/SelectBox.js +29 -21
- package/dist/src/SelectBox/hooks/useKeyboardNavigation.js +8 -2
- package/dist/src/SelectBox/utils/getFlatOptions.js +1 -1
- package/dist/src/SidePane/SidePane.js +87 -7
- package/dist/src/SidePane/SidePane.types.d.ts +39 -0
- package/dist/src/SplitView/hooks/useSplitViewPanelVisibility.js +5 -2
- package/dist/src/SubNavigation/SubNavigation.d.ts +10 -1
- package/dist/src/SubNavigation/SubNavigation.js +93 -21
- package/dist/src/SubNavigation/SubNavigation.types.d.ts +45 -7
- package/dist/src/Tabs/Tabs.js +8 -4
- package/dist/src/Tabs/hooks/useCalculatedSelectedTabId.js +7 -5
- package/dist/src/Tabs/hooks/useTabRegistry.js +5 -5
- package/dist/src/Toast/ToastContainer/ToastContainer.js +11 -2
- package/dist/src/ToggleSwitch/index.d.ts +1 -1
- package/dist/src/ToggleSwitch/index.js +0 -1
- package/dist/src/Tooltip/Tooltip.js +8 -3
- package/dist/src/Tooltip/hooks/useTooltipDisplayController.js +77 -88
- package/dist/src/utilities/dom/containsAcrossShadowRoots.d.ts +21 -0
- package/dist/src/utilities/dom/containsAcrossShadowRoots.js +33 -0
- package/dist/src/utilities/dom/getFocusableNodes.js +1 -3
- package/dist/src/utilities/dom/useFixedColumns.js +6 -8
- package/dist/src/utilities/dom/useFocusTrap.js +1 -1
- package/dist/src/utilities/dom/useTableEdgeCellMarkers.d.ts +56 -0
- package/dist/src/utilities/dom/useTableEdgeCellMarkers.js +223 -0
- package/dist/src/utilities/react/flattenChildren.d.ts +15 -0
- package/dist/src/utilities/react/flattenChildren.js +28 -0
- package/dist/src/utilities/state/useLatest.d.ts +1 -1
- package/dist/src/utilities/state/useLatest.js +8 -2
- package/dist/styled-system/recipes/badge-slot-recipe.d.ts +1 -1
- package/dist/styled-system/recipes/badge-slot-recipe.js +4 -0
- package/dist/styled-system/recipes/data-table-cell-slot-recipe.d.ts +1 -1
- package/dist/styled-system/recipes/data-table-cell-slot-recipe.js +8 -2
- package/dist/styled-system/recipes/data-table-row-slot-recipe.js +13 -6
- package/dist/styled-system/recipes/sub-navigation-slot-recipe.d.ts +1 -1
- package/dist/styled-system/recipes/sub-navigation-slot-recipe.js +12 -0
- package/dist/styles.css +175 -39
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +12 -12
- /package/dist/src/{MultipleSelectBox/utils → utilities/dom}/getDeepActiveElement.d.ts +0 -0
- /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]
|
|
145
|
-
const day = matchedWareki[7] !== undefined ? 1 : Number(matchedWareki[4]
|
|
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>;
|
|
@@ -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]
|
|
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:
|
|
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
|
|
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,
|
|
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
|
-
//
|
|
24
|
-
|
|
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
|
-
}
|
|
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 '
|
|
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
|
|
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
|
-
//
|
|
31
|
-
|
|
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
|
-
}
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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] }),
|
|
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,
|
|
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
|
-
//
|
|
118
|
-
|
|
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
|
-
}
|
|
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';
|