@moneyforward/mfui-components 4.5.0 → 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/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/Checkbox/Checkbox.js +7 -3
- package/dist/src/DataTable/DataTable.types.d.ts +2 -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 +7 -5
- package/dist/src/DataTable/DataTableRow/DataTableRow.js +104 -32
- package/dist/src/DataTable/DataTableRow/DataTableRow.types.d.ts +44 -15
- 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/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 +1 -1
- package/dist/src/DateTimeSelection/shared/utilities/japaneseCalendar.js +2 -2
- package/dist/src/FileDropZone/FileDropZone.js +1 -1
- package/dist/src/FileDropZone/utils/fileHelpers.js +1 -1
- package/dist/src/MainNavigation/BaseMainNavigation.js +1 -1
- package/dist/src/MainNavigation/NarrowViewportMainNavigation.js +1 -1
- package/dist/src/MultipleSelectBox/MultipleSelectBoxTrigger/MultipleSelectBoxTrigger.js +1 -1
- package/dist/src/MultipleSelectBox/hooks/useCreateOption.js +8 -4
- package/dist/src/MultipleSelectBox/hooks/useOptionKeyboardNavigation.js +9 -4
- package/dist/src/MultipleSelectBox/hooks/useSelectedValues.js +16 -50
- package/dist/src/NumberBox/NumberBox.js +8 -4
- package/dist/src/PageHeader/PageHeader.types.d.ts +2 -0
- package/dist/src/Popover/Popover.js +11 -2
- package/dist/src/SelectBox/SelectBox.js +9 -10
- package/dist/src/SelectBox/hooks/useKeyboardNavigation.js +8 -2
- package/dist/src/SplitView/hooks/useSplitViewPanelVisibility.js +5 -2
- package/dist/src/SubNavigation/SubNavigation.js +80 -19
- package/dist/src/SubNavigation/SubNavigation.types.d.ts +18 -6
- package/dist/src/Tabs/Tabs.js +8 -4
- package/dist/src/Tabs/hooks/useCalculatedSelectedTabId.js +7 -5
- package/dist/src/Toast/ToastContainer/ToastContainer.js +11 -2
- package/dist/src/Tooltip/Tooltip.js +8 -3
- package/dist/src/Tooltip/hooks/useTooltipDisplayController.js +77 -88
- package/dist/src/utilities/dom/getFocusableNodes.js +1 -3
- package/dist/src/utilities/dom/useFixedColumns.js +6 -8
- 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 +4 -0
- 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 +155 -35
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +12 -12
|
@@ -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';
|
|
@@ -149,13 +149,17 @@ export const NumberBox = forwardRef(({ onChange, value, defaultValue, textAlign
|
|
|
149
149
|
const pendingCursorRef = useRef(null);
|
|
150
150
|
const [cursorRestoreToken, requestCursorRestore] = useReducer((count) => count + 1, 0);
|
|
151
151
|
const [displayValue, setDisplayValue] = useState(() => formatWithCommas(sanitizeInput(String(value ?? defaultValue ?? ''))));
|
|
152
|
-
//
|
|
153
|
-
|
|
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);
|
|
154
158
|
if (value !== undefined) {
|
|
155
159
|
const sanitized = sanitizeInput(String(value));
|
|
156
160
|
setDisplayValue(formatWithCommas(sanitized));
|
|
157
161
|
}
|
|
158
|
-
}
|
|
162
|
+
}
|
|
159
163
|
// Restore cursor position after display value update
|
|
160
164
|
useLayoutEffect(() => {
|
|
161
165
|
if (pendingCursorRef.current !== null && inputRef.current) {
|
|
@@ -44,6 +44,8 @@ export type PageHeaderProps = {
|
|
|
44
44
|
showBackButton?: boolean;
|
|
45
45
|
/**
|
|
46
46
|
* It's for controlling the back button.
|
|
47
|
+
*
|
|
48
|
+
* The default `aria-label` is `戻る`, but set it to `{destination}へ戻る` (e.g. `請求書一覧へ戻る`) so that users know where the button navigates to.
|
|
47
49
|
*/
|
|
48
50
|
backButtonProps?: Omit<IconButtonProps, 'outlined' | 'children'>;
|
|
49
51
|
/**
|
|
@@ -40,6 +40,15 @@ export function Popover({ renderTrigger, renderContent, open, onOpenStateChanged
|
|
|
40
40
|
constrainToTriggerWidth: enableConstrainedContentWidth,
|
|
41
41
|
enableCenteredLayoutOnNarrowViewport,
|
|
42
42
|
});
|
|
43
|
+
// Mirrors `refs.floating.current` into state so `TargetDomNodeProvider` below can be
|
|
44
|
+
// given it directly, instead of reading a ref's `.current` during render (unsafe: a
|
|
45
|
+
// ref update never triggers the re-render needed to pass a fresh value to context
|
|
46
|
+
// consumers). `refs.setFloating` still runs so floating-ui keeps positioning it.
|
|
47
|
+
const [floatingElement, setFloatingElement] = useState(null);
|
|
48
|
+
const setFloatingRef = useCallback((node) => {
|
|
49
|
+
refs.setFloating(node);
|
|
50
|
+
setFloatingElement(node);
|
|
51
|
+
}, [refs]);
|
|
43
52
|
// Fire onOpen / onClose *only* when the open state actually changes.
|
|
44
53
|
// We keep a ref of the previous open state so a new toggleEvent while already
|
|
45
54
|
// open does not retrigger `onOpen`, which previously caused unexpected focus
|
|
@@ -253,8 +262,8 @@ export function Popover({ renderTrigger, renderContent, open, onOpenStateChanged
|
|
|
253
262
|
});
|
|
254
263
|
};
|
|
255
264
|
}, [isPopoverOpen, refs.reference, refs.floating]);
|
|
256
|
-
return (_jsxs(_Fragment, { children: [renderTrigger(options), shouldRenderTheContent ? (_jsx(Portal, { targetDOMNode: targetElement, children: _jsx("div", { ref:
|
|
265
|
+
return (_jsxs(_Fragment, { children: [renderTrigger(options), shouldRenderTheContent ? (_jsx(Portal, { targetDOMNode: targetElement, children: _jsx("div", { ref: setFloatingRef, id: contentProps.id, tabIndex: contentProps.tabIndex ?? 0, "data-mfui-content": "popover-portal-content", "data-mfui-popover-depth": nestingDepth, className: cx(classes.root, 'mfui-Popover__root', contentProps.className), style: {
|
|
257
266
|
...floatingStyles,
|
|
258
267
|
...(!enableAutoUnmount && !isPopoverOpen && { display: 'none' }),
|
|
259
|
-
}, onKeyDown: handleKeyDownPopover, children: _jsx(TargetDomNodeProvider, { targetDomNode:
|
|
268
|
+
}, onKeyDown: handleKeyDownPopover, children: _jsx(TargetDomNodeProvider, { targetDomNode: floatingElement, children: _jsx(PopoverContextProvider, { children: renderContent(options) }) }) }) })) : null] }));
|
|
260
269
|
}
|
|
@@ -47,7 +47,7 @@ export const SelectBox = forwardRef((props, ref) => {
|
|
|
47
47
|
const optionPanelRef = useRef(null);
|
|
48
48
|
const scrollWrapperRef = useRef(null);
|
|
49
49
|
const searchInputRef = useRef(null);
|
|
50
|
-
const [localSelectedOption, setLocalSelectedOption] = useState(() => defaultValue ?? null);
|
|
50
|
+
const [localSelectedOption, setLocalSelectedOption] = useState(() => value ?? defaultValue ?? null);
|
|
51
51
|
const { isOpen: isOptionPanelOpen, toggle: toggleOptionPanel, close: closeOptionPanel, } = useDisclosure({
|
|
52
52
|
value: false,
|
|
53
53
|
onToggle: onOpenStateChanged,
|
|
@@ -63,17 +63,16 @@ export const SelectBox = forwardRef((props, ref) => {
|
|
|
63
63
|
resetSearchText();
|
|
64
64
|
}
|
|
65
65
|
}, [isOptionPanelOpen, resetSearchText]);
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
// Only update when value prop is explicitly provided (controlled mode)
|
|
66
|
+
// Adjusted during render (React's documented pattern for resyncing state to a
|
|
67
|
+
// changed prop) rather than in an effect, so the UI never flashes the stale value
|
|
68
|
+
// for one frame. Only fires when value prop is explicitly provided (controlled mode).
|
|
69
|
+
const [prevValue, setPrevValue] = useState(value);
|
|
70
|
+
if (value !== prevValue) {
|
|
71
|
+
setPrevValue(value);
|
|
73
72
|
if (value !== undefined) {
|
|
74
|
-
setLocalSelectedOption(
|
|
73
|
+
setLocalSelectedOption(value);
|
|
75
74
|
}
|
|
76
|
-
}
|
|
75
|
+
}
|
|
77
76
|
useUpdateEffect(() => {
|
|
78
77
|
if (value === undefined) {
|
|
79
78
|
setLocalSelectedOption(null);
|
|
@@ -30,9 +30,15 @@ export function useKeyboardNavigation({ isOptionPanelOpen, loading, enableSearch
|
|
|
30
30
|
return selectedIndex !== -1 ? selectedIndex : 0;
|
|
31
31
|
}, [flatOptions, localSelectedOption]);
|
|
32
32
|
const [tabbableOptionIndex, setTabbableOptionIndex] = useState(getDefaultTabbableIndex());
|
|
33
|
-
|
|
33
|
+
// Adjusted during render (React's documented pattern for resyncing state to changed
|
|
34
|
+
// props) rather than in an effect, so the UI never flashes the stale index for one frame.
|
|
35
|
+
const [prevFlatOptions, setPrevFlatOptions] = useState(flatOptions);
|
|
36
|
+
const [prevLocalSelectedOption, setPrevLocalSelectedOption] = useState(localSelectedOption);
|
|
37
|
+
if (flatOptions !== prevFlatOptions || localSelectedOption !== prevLocalSelectedOption) {
|
|
38
|
+
setPrevFlatOptions(flatOptions);
|
|
39
|
+
setPrevLocalSelectedOption(localSelectedOption);
|
|
34
40
|
setTabbableOptionIndex(getDefaultTabbableIndex());
|
|
35
|
-
}
|
|
41
|
+
}
|
|
36
42
|
const resetTabbableIndex = useCallback(() => {
|
|
37
43
|
setTabbableOptionIndex(getDefaultTabbableIndex());
|
|
38
44
|
}, [getDefaultTabbableIndex]);
|
|
@@ -19,8 +19,11 @@ export function useSplitViewPanelVisibility(isOpen, controlledPanelSlot) {
|
|
|
19
19
|
if (wasRightPanelVisibleOnMountRef.current === null) {
|
|
20
20
|
wasRightPanelVisibleOnMountRef.current = isRightPanelVisible;
|
|
21
21
|
}
|
|
22
|
-
// Initialize hasRightPanelBeenVisible based on initial render state
|
|
23
|
-
|
|
22
|
+
// Initialize hasRightPanelBeenVisible based on initial render state. Reads
|
|
23
|
+
// `isRightPanelVisible` directly rather than the ref just written above — on first
|
|
24
|
+
// render (the only render this initial value matters for) they're the same value, and
|
|
25
|
+
// this avoids passing a ref's `.current` into a function call during render.
|
|
26
|
+
const [hasRightPanelBeenVisible, setHasRightPanelBeenVisible] = useState(isRightPanelVisible);
|
|
24
27
|
// Track previous visibility state
|
|
25
28
|
// Note: This ref is updated in useSplitViewAnimation hook to ensure correct timing
|
|
26
29
|
const previousIsRightPanelVisibleRef = useRef(isRightPanelVisible);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { DisclosureBasicCollapsed, Lock } from '@moneyforward/mfui-icons-react';
|
|
3
|
-
import { forwardRef } from 'react';
|
|
3
|
+
import { forwardRef, useId } from 'react';
|
|
4
4
|
import { cx } from '../../styled-system/css';
|
|
5
5
|
import { subNavigationSlotRecipe } from '../../styled-system/recipes';
|
|
6
6
|
import { FocusIndicator } from '../FocusIndicator';
|
|
@@ -30,6 +30,7 @@ SubNavigation.displayName = 'SubNavigation';
|
|
|
30
30
|
* - a disclosure button with children
|
|
31
31
|
*
|
|
32
32
|
* This function is splitted from the SubNavigation component to allow for recursive rendering for deep nesting.
|
|
33
|
+
* It is called inside loops, so it must not call hooks. Create them in the rendered components instead.
|
|
33
34
|
*
|
|
34
35
|
* @param navigationItem - The navigation item to render.
|
|
35
36
|
*
|
|
@@ -44,57 +45,117 @@ SubNavigation.displayName = 'SubNavigation';
|
|
|
44
45
|
* @returns The rendered navigation item.
|
|
45
46
|
*/
|
|
46
47
|
const renderNavigationItem = (navigationItem, customLinkComponent, classes, nestLevel, renderNavigationLabel) => {
|
|
47
|
-
|
|
48
|
-
if (children === undefined) {
|
|
48
|
+
if (navigationItem.children === undefined) {
|
|
49
49
|
return (_jsx(NavigationLink, { navigationItem: navigationItem, customLinkComponent: customLinkComponent, classes: classes, renderNavigationLabel: renderNavigationLabel }));
|
|
50
50
|
}
|
|
51
|
-
|
|
51
|
+
const nestedNavigationList = (_jsx(NestedNavigationList, { parentNavigationItem: navigationItem, customLinkComponent: customLinkComponent, classes: classes, nestLevel: nestLevel + 1, renderNavigationLabel: renderNavigationLabel }));
|
|
52
|
+
if (navigationItem.href) {
|
|
52
53
|
// The the parent item will be rendered as a link with separated disclosure icon button.
|
|
53
|
-
return (_jsxs(_Fragment, { children: [_jsx(NavigationLink, { navigationItem: navigationItem, customLinkComponent: customLinkComponent, classes: classes, renderNavigationLabel: renderNavigationLabel }),
|
|
54
|
+
return (_jsxs(_Fragment, { children: [_jsx(NavigationLink, { navigationItem: navigationItem, customLinkComponent: customLinkComponent, classes: classes, renderNavigationLabel: renderNavigationLabel }), nestedNavigationList] }));
|
|
54
55
|
}
|
|
55
56
|
// The the parent item will be rendered as a disclosure button with an icon.
|
|
56
|
-
return
|
|
57
|
+
return nestedNavigationList;
|
|
57
58
|
};
|
|
58
59
|
/**
|
|
59
|
-
*
|
|
60
|
+
* Create the IDs only when the item has a description.
|
|
61
|
+
* Without a description, the item keeps its accessible name from its content.
|
|
62
|
+
*
|
|
63
|
+
* @param hasDescription - Whether the item has a description.
|
|
64
|
+
*
|
|
65
|
+
* @returns The IDs, or `undefined` when the item has no description.
|
|
66
|
+
*/
|
|
67
|
+
function useLabelIds(hasDescription) {
|
|
68
|
+
const id = useId();
|
|
69
|
+
if (!hasDescription)
|
|
70
|
+
return undefined;
|
|
71
|
+
return { label: `${id}-label`, status: `${id}-status`, description: `${id}-description` };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Get the ARIA attributes for the element that contains the NavigationLabelGroup.
|
|
75
|
+
*
|
|
76
|
+
* @param labelIds - The IDs from `useLabelIds`.
|
|
77
|
+
*
|
|
78
|
+
* @param hasStatus - Whether the status is displayed.
|
|
79
|
+
*
|
|
80
|
+
* @returns The ARIA attributes. Empty when the item has no description.
|
|
81
|
+
*/
|
|
82
|
+
function getLabelAriaProps(labelIds, hasStatus) {
|
|
83
|
+
if (!labelIds)
|
|
84
|
+
return {};
|
|
85
|
+
return {
|
|
86
|
+
'aria-labelledby': hasStatus ? `${labelIds.label} ${labelIds.status}` : labelIds.label,
|
|
87
|
+
'aria-describedby': labelIds.description,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* This is a group of a text label, an optional description, an optional icon, and a status.
|
|
60
92
|
* This is used for all navigation links and parent items for disclosure buttons in SubNavigation.
|
|
61
93
|
*/
|
|
62
|
-
function NavigationLabelGroup({ label, labelIcon, locked, lockIconProps, statusSlot, classes, renderNavigationLabel, }) {
|
|
94
|
+
function NavigationLabelGroup({ label, description, labelIcon, locked, lockIconProps, statusSlot, labelIds, classes, renderNavigationLabel, }) {
|
|
63
95
|
// If locked=true, display <Lock /> icon in preference to any other status of the statusSlot option.
|
|
64
96
|
const renderStatus = () => {
|
|
65
97
|
if (locked) {
|
|
66
|
-
return (_jsx("
|
|
98
|
+
return (_jsx("span", { id: labelIds?.status, className: cx(classes.statusSlot, 'mfui-SubNavigation__statusSlot'), children: _jsx(Lock, { role: "img", "aria-label": lockIconProps?.['aria-label'] ?? 'ロックされています' }) }));
|
|
67
99
|
}
|
|
68
100
|
if (statusSlot) {
|
|
69
|
-
return _jsx("
|
|
101
|
+
return (_jsx("span", { id: labelIds?.status, className: cx(classes.statusSlot, 'mfui-SubNavigation__statusSlot'), children: statusSlot }));
|
|
70
102
|
}
|
|
71
103
|
return null;
|
|
72
104
|
};
|
|
73
|
-
|
|
105
|
+
const labelText = renderNavigationLabel ? (renderNavigationLabel(label)) : (_jsx(Typography, { variant: "strongControlLabel", className: cx(classes.label, 'mfui-SubNavigation__label'), children: label }));
|
|
106
|
+
return (_jsxs(_Fragment, { children: [_jsxs("span", { className: cx(classes.labelContainer, 'mfui-SubNavigation__labelContainer'), children: [labelIcon ? (_jsx("span", { "aria-hidden": "true", className: cx(classes.labelIcon, 'mfui-SubNavigation__labelIcon'), children: labelIcon })) : null, _jsxs("span", { className: cx(classes.labelContent, 'mfui-SubNavigation__labelContent'), children: [labelIds ? _jsx("span", { id: labelIds.label, children: labelText }) : labelText, description ? (_jsx(Typography, { variant: "helpMessage", id: labelIds?.description, "data-mfui-content": "sub-navigation-description", className: cx(classes.description, 'mfui-SubNavigation__description'), children: description })) : null] })] }), renderStatus()] }));
|
|
74
107
|
}
|
|
75
108
|
/**
|
|
76
109
|
* This is a link that has NavigationLabelGroup.
|
|
77
110
|
* This is used for all navigation links in SubNavigation.
|
|
78
111
|
*/
|
|
79
112
|
function NavigationLink({ navigationItem, customLinkComponent, classes, renderNavigationLabel, }) {
|
|
80
|
-
const { label, href, isCurrent, isExternal, labelIcon, statusSlot, locked, lockIconProps } = navigationItem;
|
|
113
|
+
const { label, description, href, isCurrent, isExternal, labelIcon, statusSlot, locked, lockIconProps } = navigationItem;
|
|
114
|
+
const labelIds = useLabelIds(!!description);
|
|
115
|
+
const labelAriaProps = getLabelAriaProps(labelIds, !!locked || !!statusSlot);
|
|
116
|
+
const labelGroup = (_jsx(NavigationLabelGroup, { label,
|
|
117
|
+
description,
|
|
118
|
+
labelIcon,
|
|
119
|
+
locked,
|
|
120
|
+
lockIconProps,
|
|
121
|
+
statusSlot,
|
|
122
|
+
labelIds,
|
|
123
|
+
classes,
|
|
124
|
+
renderNavigationLabel }));
|
|
81
125
|
if (navigationItem.children === undefined && navigationItem.onClick) {
|
|
82
126
|
const { onClick } = navigationItem;
|
|
83
|
-
return (_jsx(FocusIndicator, { children: _jsx("button", { type: "button", className: cx(classes.link, 'mfui-SubNavigation__link'), "aria-current": isCurrent ? 'page' : undefined, onClick: () => {
|
|
127
|
+
return (_jsx(FocusIndicator, { children: _jsx("button", { type: "button", className: cx(classes.link, 'mfui-SubNavigation__link'), "aria-current": isCurrent ? 'page' : undefined, ...labelAriaProps, onClick: () => {
|
|
84
128
|
onClick(navigationItem.href);
|
|
85
|
-
}, children:
|
|
129
|
+
}, children: labelGroup }) }));
|
|
86
130
|
}
|
|
87
131
|
if (customLinkComponent && !isExternal) {
|
|
88
132
|
const Tag = customLinkComponent;
|
|
89
|
-
return (_jsx(FocusIndicator, { children: _jsx(Tag, { href: href, className: cx(classes.link, 'mfui-SubNavigation__link'), "aria-current": isCurrent ? 'page' : undefined, children:
|
|
133
|
+
return (_jsx(FocusIndicator, { children: _jsx(Tag, { href: href, className: cx(classes.link, 'mfui-SubNavigation__link'), "aria-current": isCurrent ? 'page' : undefined, ...labelAriaProps, children: labelGroup }) }));
|
|
90
134
|
}
|
|
91
|
-
return (_jsx(FocusIndicator, { children: _jsx("a", { href: href, target: isExternal ? '_blank' : undefined, className: cx(classes.link, 'mfui-SubNavigation__link'), "aria-current": isCurrent ? 'page' : undefined, rel: "noreferrer", children:
|
|
135
|
+
return (_jsx(FocusIndicator, { children: _jsx("a", { href: href, target: isExternal ? '_blank' : undefined, className: cx(classes.link, 'mfui-SubNavigation__link'), "aria-current": isCurrent ? 'page' : undefined, rel: "noreferrer", ...labelAriaProps, children: labelGroup }) }));
|
|
92
136
|
}
|
|
93
137
|
/**
|
|
94
138
|
* This is a details element that contains a list of nested navigation links.
|
|
139
|
+
*
|
|
140
|
+
* - When the parent item has `href`, the summary is only the disclosure icon button next to the parent link.
|
|
141
|
+
* - Otherwise, the summary is the disclosure button with the label of the parent item.
|
|
95
142
|
*/
|
|
96
|
-
function NestedNavigationList({
|
|
143
|
+
function NestedNavigationList({ parentNavigationItem, customLinkComponent, classes, nestLevel, renderNavigationLabel, }) {
|
|
144
|
+
const { label, description, href, labelIcon, statusSlot, locked, lockIconProps, children } = parentNavigationItem;
|
|
145
|
+
// With href, the description belongs to the parent link, not to the disclosure icon button.
|
|
146
|
+
const labelIds = useLabelIds(!href && !!description);
|
|
147
|
+
const labelAriaProps = getLabelAriaProps(labelIds, !!locked || !!statusSlot);
|
|
97
148
|
// Check whether all descendants have isCurrent=true by calling itself recursively.
|
|
98
|
-
const isCurrentPathInChildren = (children) => children?.some(({ isCurrent, children }) => isCurrent || (children && isCurrentPathInChildren(children)))
|
|
99
|
-
|
|
149
|
+
const isCurrentPathInChildren = (children) => children?.some(({ isCurrent, children }) => !!isCurrent || !!(children && isCurrentPathInChildren(children))) ??
|
|
150
|
+
false;
|
|
151
|
+
const summarySlot = href ? (_jsx("span", { "data-mfui-content": "sub-navigation-disclosure-icon", className: cx(classes.disclosureIcon, 'mfui-SubNavigation__disclosureIcon'), children: _jsx(DisclosureBasicCollapsed, { role: "img", "aria-label": label }) })) : (_jsxs(_Fragment, { children: [_jsx(NavigationLabelGroup, { label,
|
|
152
|
+
description,
|
|
153
|
+
labelIcon,
|
|
154
|
+
locked,
|
|
155
|
+
lockIconProps,
|
|
156
|
+
statusSlot,
|
|
157
|
+
labelIds,
|
|
158
|
+
classes,
|
|
159
|
+
renderNavigationLabel }), _jsx("span", { "data-mfui-content": "sub-navigation-disclosure-icon", className: cx(classes.disclosureIcon, 'mfui-SubNavigation__disclosureIcon'), children: _jsx(DisclosureBasicCollapsed, { "aria-hidden": "true" }) })] }));
|
|
160
|
+
return (_jsxs("details", { open: isCurrentPathInChildren(children), className: cx(classes.details, 'mfui-SubNavigation__details'), children: [_jsx(FocusIndicator, { children: _jsx("summary", { className: cx(classes.summary, 'mfui-SubNavigation__summary'), ...labelAriaProps, children: summarySlot }) }), _jsx("ul", { className: cx(classes.list, 'mfui-SubNavigation__list'), children: children.map((nestedNavigationItem, index) => (_jsx("li", { className: cx(classes.nestedListItem, 'mfui-SubNavigation__nestedListItem'), "data-mfui-nest-level": nestLevel, children: nestedNavigationItem.children === undefined ? (_jsx(NavigationLink, { navigationItem: nestedNavigationItem, customLinkComponent: customLinkComponent, classes: classes, renderNavigationLabel: renderNavigationLabel })) : (renderNavigationItem(nestedNavigationItem, customLinkComponent, classes, nestLevel, renderNavigationLabel)) }, index))) })] }));
|
|
100
161
|
}
|
|
@@ -76,12 +76,12 @@ type BaseSubNavigationProps = {
|
|
|
76
76
|
customLinkComponent?: ElementType;
|
|
77
77
|
/**
|
|
78
78
|
* Custom render function for each navigation item's label.
|
|
79
|
-
* Receives the item's raw `label` string and returns a ReactNode
|
|
80
|
-
* the default `<Typography variant="strongControlLabel">`
|
|
81
|
-
* When omitted, the label string is rendered
|
|
79
|
+
* Receives the item's raw `label` string and returns a ReactNode that replaces
|
|
80
|
+
* the default `<Typography variant="strongControlLabel">` label.
|
|
81
|
+
* When omitted, the label string is rendered with the default label style.
|
|
82
82
|
*
|
|
83
|
-
* Use this to truncate long labels or otherwise customize how labels are displayed
|
|
84
|
-
*
|
|
83
|
+
* Use this to truncate long labels or otherwise customize how labels are displayed.
|
|
84
|
+
* It only affects the label, not the item's `description`.
|
|
85
85
|
*
|
|
86
86
|
* To show a tooltip on both hover and focus, wrap the entire link with a `Tooltip`
|
|
87
87
|
* via `customLinkComponent` instead — placing a `Tooltip` inside `renderNavigationLabel`
|
|
@@ -90,7 +90,7 @@ type BaseSubNavigationProps = {
|
|
|
90
90
|
*
|
|
91
91
|
* @param label - The label string of the navigation item.
|
|
92
92
|
*
|
|
93
|
-
* @returns ReactNode - Custom content rendered
|
|
93
|
+
* @returns ReactNode - Custom content rendered in place of the default label.
|
|
94
94
|
*
|
|
95
95
|
* @example
|
|
96
96
|
* ```tsx
|
|
@@ -184,6 +184,18 @@ type BaseNavigationItem = {
|
|
|
184
184
|
* The text label briefly describing the linked page.
|
|
185
185
|
*/
|
|
186
186
|
label: string;
|
|
187
|
+
/**
|
|
188
|
+
* A short supplementary text displayed below the text label.
|
|
189
|
+
*
|
|
190
|
+
* Screen readers announce it as the item's description, after the label and the status.
|
|
191
|
+
* Only plain text is accepted because the item itself is a link or a button.
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```tsx
|
|
195
|
+
* <SubNavigation navigationItems={[{ label: '請求書', description: '作成・送付', href: '/invoices' }]} />
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
198
|
+
description?: string;
|
|
187
199
|
/**
|
|
188
200
|
* Whether the navigation item is the current page.
|
|
189
201
|
*
|
package/dist/src/Tabs/Tabs.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { useMemo,
|
|
3
|
+
import { useMemo, useState } from 'react';
|
|
4
4
|
import { tabsSlotRecipe } from '../../styled-system/recipes';
|
|
5
5
|
import { useCalculatedSelectedTabId } from './hooks/useCalculatedSelectedTabId';
|
|
6
6
|
import { useTabRegistry } from './hooks/useTabRegistry';
|
|
@@ -13,7 +13,10 @@ import { cx } from '../../styled-system/css';
|
|
|
13
13
|
function Base({ selectedTabId, onSelectedTabChange, targetDOMNode, enableAutoUnmount = true, children, className, style, }) {
|
|
14
14
|
const classNames = tabsSlotRecipe();
|
|
15
15
|
const [shouldShowMenu, setShouldShowMenu] = useState(false);
|
|
16
|
-
|
|
16
|
+
// State (not a ref) so `getTargetDomNode` below can be given the wrapper element
|
|
17
|
+
// without reading a ref's `.current` during render (unsafe: a ref update never
|
|
18
|
+
// triggers the re-render needed to pass a fresh value into context).
|
|
19
|
+
const [wrapperElement, setWrapperElement] = useState(null);
|
|
17
20
|
const { tabs, registerTab, updateTab, unregisterTab } = useTabRegistry();
|
|
18
21
|
const { calculatedSelectedTabId, setCalculatedSelectedTab } = useCalculatedSelectedTabId({
|
|
19
22
|
selectedTabId,
|
|
@@ -26,7 +29,7 @@ function Base({ selectedTabId, onSelectedTabChange, targetDOMNode, enableAutoUnm
|
|
|
26
29
|
tabs,
|
|
27
30
|
shouldShowMenu,
|
|
28
31
|
setShouldShowMenu,
|
|
29
|
-
targetDOMNode: getTargetDomNode(targetDOMNode,
|
|
32
|
+
targetDOMNode: getTargetDomNode(targetDOMNode, wrapperElement),
|
|
30
33
|
registerTab,
|
|
31
34
|
updateTab,
|
|
32
35
|
unregisterTab,
|
|
@@ -38,12 +41,13 @@ function Base({ selectedTabId, onSelectedTabChange, targetDOMNode, enableAutoUnm
|
|
|
38
41
|
shouldShowMenu,
|
|
39
42
|
setShouldShowMenu,
|
|
40
43
|
targetDOMNode,
|
|
44
|
+
wrapperElement,
|
|
41
45
|
registerTab,
|
|
42
46
|
updateTab,
|
|
43
47
|
unregisterTab,
|
|
44
48
|
enableAutoUnmount,
|
|
45
49
|
]);
|
|
46
|
-
return (_jsx(TabContext.Provider, { value: contextValue, children: _jsx("div", { ref:
|
|
50
|
+
return (_jsx(TabContext.Provider, { value: contextValue, children: _jsx("div", { ref: setWrapperElement, className: cx(classNames.root, 'mfui-Tabs__root', className), style: style, children: children }) }));
|
|
47
51
|
}
|
|
48
52
|
/**
|
|
49
53
|
* The Tabs component manages the tabs, tab lists, and tab panels within a tab section.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useState } from 'react';
|
|
2
2
|
/**
|
|
3
3
|
* Custom hook to manage the selected tab ID based on provided options.
|
|
4
4
|
* Automatically updates the selected tab ID when the `selectedTabId` prop changes.
|
|
@@ -12,11 +12,13 @@ import { useEffect, useState } from 'react';
|
|
|
12
12
|
export function useCalculatedSelectedTabId({ selectedTabId, onSelectedTabChange, tabs, }) {
|
|
13
13
|
// implement
|
|
14
14
|
const [calculatedSelectedTabId, setCalculatedSelectedTabId] = useState(selectedTabId);
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
15
|
+
// Adjusted during render (React's documented pattern for resyncing state to a changed
|
|
16
|
+
// prop) rather than in an effect, so the UI never flashes the stale tab for one frame.
|
|
17
|
+
const [prevSelectedTabId, setPrevSelectedTabId] = useState(selectedTabId);
|
|
18
|
+
if (selectedTabId !== prevSelectedTabId) {
|
|
19
|
+
setPrevSelectedTabId(selectedTabId);
|
|
18
20
|
setCalculatedSelectedTabId(selectedTabId);
|
|
19
|
-
}
|
|
21
|
+
}
|
|
20
22
|
return {
|
|
21
23
|
// If still nothing, we pick the first tabId from the tab list
|
|
22
24
|
calculatedSelectedTabId: calculatedSelectedTabId ?? tabs[0]?.id,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
3
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
4
4
|
import { Toast } from '../Toast';
|
|
5
5
|
import { toastSlotRecipe } from '../../../styled-system/recipes';
|
|
6
6
|
import { cx } from '../../../styled-system/css';
|
|
@@ -34,6 +34,15 @@ export function ToastContainer({ items, onItemClose }) {
|
|
|
34
34
|
const [containerHeight, setContainerHeight] = useState(0);
|
|
35
35
|
const [activeElement, setActiveElement] = useState(null);
|
|
36
36
|
const [isMobile, setIsMobile] = useState(false);
|
|
37
|
+
// Mirrors `firstItemRef.current` into state so `TargetDomNodeProvider` below can be
|
|
38
|
+
// given it directly, instead of reading a ref's `.current` during render (unsafe: a
|
|
39
|
+
// ref update never triggers the re-render needed to pass a fresh value to context
|
|
40
|
+
// consumers).
|
|
41
|
+
const [firstItemElement, setFirstItemElement] = useState(null);
|
|
42
|
+
const setFirstItemRef = useCallback((node) => {
|
|
43
|
+
firstItemRef.current = node;
|
|
44
|
+
setFirstItemElement(node);
|
|
45
|
+
}, []);
|
|
37
46
|
// Detect viewport width for responsive CONTAINER_GAP
|
|
38
47
|
useLayoutEffect(() => {
|
|
39
48
|
const checkViewportWidth = () => {
|
|
@@ -114,7 +123,7 @@ export function ToastContainer({ items, onItemClose }) {
|
|
|
114
123
|
};
|
|
115
124
|
}, [items, activeElement, onItemClose]);
|
|
116
125
|
const containerGap = isMobile ? CONTAINER_GAP_MOBILE : CONTAINER_GAP_DESKTOP;
|
|
117
|
-
return (_jsx(TargetDomNodeProvider, { targetDomNode:
|
|
126
|
+
return (_jsx(TargetDomNodeProvider, { targetDomNode: firstItemElement, children: _jsx("div", { style: { height: containerHeight === 0 ? 0 : `calc(${containerHeight.toString()}px + ${containerGap})` }, className: cx(classes.container, 'mfui-ToastContainer__container'), children: items.map(({ key, onClose, duration, ...props }, index) => (_jsx(Toast, { ref: [setFirstItemRef, secondItemRef][index] ?? null, ...props, style: {
|
|
118
127
|
zIndex: 1000 - index,
|
|
119
128
|
}, activeElement: activeElement,
|
|
120
129
|
// If index is 0, set the duration to the first item. Otherwise, the duration will be `undefined`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { forwardRef, useId, useRef, isValidElement, cloneElement, useEffect, useMemo } from 'react';
|
|
3
|
+
import { forwardRef, useId, useRef, isValidElement, cloneElement, useEffect, useMemo, useState } from 'react';
|
|
4
4
|
import { getOverflowAncestors } from '@floating-ui/react-dom';
|
|
5
5
|
import { cx } from '../../styled-system/css';
|
|
6
6
|
import { tooltipSlotRecipe } from '../../styled-system/recipes';
|
|
@@ -52,9 +52,14 @@ export const Tooltip = forwardRef(({ children, open, onOpenStateChanged, content
|
|
|
52
52
|
// Allow consumers to control the content id so an external element (e.g. a form field)
|
|
53
53
|
// can reference the single accessible copy of the content via its own aria-describedby.
|
|
54
54
|
const tooltipId = contentProps?.id ?? generatedId;
|
|
55
|
+
// Mirrors `rootRef.current` into state so it can be read below without reading a
|
|
56
|
+
// ref's `.current` during render (unsafe: a ref update never triggers the re-render
|
|
57
|
+
// needed to pass a fresh value onward). `rootRef` itself is kept for the effects and
|
|
58
|
+
// `useOnClickOutside` call further down, which read it safely outside of render.
|
|
59
|
+
const [rootElement, setRootElement] = useState(null);
|
|
55
60
|
// Use automatic target resolution matching Portal.tsx pattern
|
|
56
61
|
// Resolution order: explicit prop → parent context → document.body fallback
|
|
57
|
-
const targetElement = useAutomaticTargetDomNode(targetDOMNode,
|
|
62
|
+
const targetElement = useAutomaticTargetDomNode(targetDOMNode, rootElement);
|
|
58
63
|
const { open: calculatedOpen, rootAttributes, tooltipAttributes, closeImmediate, closeWithManager, } = useTooltipDisplayController({
|
|
59
64
|
trigger,
|
|
60
65
|
wrapperProps,
|
|
@@ -89,7 +94,7 @@ export const Tooltip = forwardRef(({ children, open, onOpenStateChanged, content
|
|
|
89
94
|
ignoreRefs: [tooltipRef],
|
|
90
95
|
disabled: !calculatedOpen || !hasClickTrigger || open !== undefined,
|
|
91
96
|
});
|
|
92
|
-
const triggerRef = mergeRefs(setTriggerReference, rootRef, ref);
|
|
97
|
+
const triggerRef = mergeRefs(setTriggerReference, rootRef, setRootElement, ref);
|
|
93
98
|
const classes = tooltipSlotRecipe();
|
|
94
99
|
const ariaDescribedBy = !hideFromScreenReader && content != null ? tooltipId : undefined;
|
|
95
100
|
return (_jsxs(_Fragment, { children: [_jsx("div", { ref: triggerRef, ...wrapperProps, className: cx(classes.root, 'mfui-Tooltip__root', className), ...rootAttributes, children: isValidElement(children) ? (cloneElement(children, ariaDescribedBy ? { 'aria-describedby': ariaDescribedBy } : undefined)) : (_jsx("span", { ...(ariaDescribedBy ? { 'aria-describedby': ariaDescribedBy } : undefined), children: children })) }), !calculatedOpen && !hideFromScreenReader && content != null && (_jsx(Portal, { targetDOMNode: targetElement, children: _jsx("div", { "data-mfui-tooltip-hidden": true, id: tooltipId, style: {
|