@moneyforward/mfui-components 4.1.0 → 4.1.2
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/GlobalHeader/GlobalHeader.d.ts +3 -0
- package/dist/src/GlobalHeader/GlobalHeader.js +3 -0
- package/dist/src/MainNavigation/MainNavigation.d.ts +3 -0
- package/dist/src/MainNavigation/MainNavigation.js +3 -0
- package/dist/src/SidePane/hooks/useSidePaneStateController.js +5 -1
- package/dist/src/Sidebar/Sidebar.d.ts +3 -0
- package/dist/src/Sidebar/Sidebar.js +3 -0
- package/dist/src/Toast/Toast.js +41 -28
- package/dist/src/Toast/ToastContainer/ToastContainer.js +16 -6
- package/package.json +2 -2
|
@@ -2,5 +2,8 @@ import { type GlobalHeaderProps } from './GlobalHeader.types';
|
|
|
2
2
|
/**
|
|
3
3
|
* The global header component.
|
|
4
4
|
* The global header component is a top-level navigation component that is used to display the application's logo and navigation elements.
|
|
5
|
+
*
|
|
6
|
+
* `GlobalHeader` should generally be paired with `MainNavigation` for the primary navigation area.
|
|
7
|
+
* If you need a self-contained layout that already includes both header and navigation features, use `Sidebar` instead.
|
|
5
8
|
*/
|
|
6
9
|
export declare function GlobalHeader({ className, mainNavigationButtonProps, applicationsMenuSlot, experimentalApplicationsMenuButtonProps, logoSlot, rightPaneSlot, disableResponsive, }: GlobalHeaderProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -8,6 +8,9 @@ import { ApplicationsExperimentalIcon } from './ApplicationsExperimentalIcon';
|
|
|
8
8
|
/**
|
|
9
9
|
* The global header component.
|
|
10
10
|
* The global header component is a top-level navigation component that is used to display the application's logo and navigation elements.
|
|
11
|
+
*
|
|
12
|
+
* `GlobalHeader` should generally be paired with `MainNavigation` for the primary navigation area.
|
|
13
|
+
* If you need a self-contained layout that already includes both header and navigation features, use `Sidebar` instead.
|
|
11
14
|
*/
|
|
12
15
|
export function GlobalHeader({ className, mainNavigationButtonProps, applicationsMenuSlot, experimentalApplicationsMenuButtonProps, logoSlot, rightPaneSlot, disableResponsive = false, }) {
|
|
13
16
|
const responsive = !disableResponsive;
|
|
@@ -5,5 +5,8 @@ import { type MainNavigationProps, type MainNavigationRef } from './MainNavigati
|
|
|
5
5
|
*
|
|
6
6
|
* When the viewport is narrow, the navigation will be displayed as a drawer.
|
|
7
7
|
* When the viewport is not narrow, the navigation will be displayed as a sidebar.
|
|
8
|
+
*
|
|
9
|
+
* `MainNavigation` is intended to be paired with `GlobalHeader`. If you need a self-contained
|
|
10
|
+
* layout that already includes both header and navigation features, use `Sidebar` instead.
|
|
8
11
|
*/
|
|
9
12
|
export declare const MainNavigation: import("react").ForwardRefExoticComponent<MainNavigationProps & import("react").RefAttributes<MainNavigationRef>>;
|
|
@@ -8,6 +8,9 @@ import { NarrowViewportMainNavigation } from './NarrowViewportMainNavigation';
|
|
|
8
8
|
*
|
|
9
9
|
* When the viewport is narrow, the navigation will be displayed as a drawer.
|
|
10
10
|
* When the viewport is not narrow, the navigation will be displayed as a sidebar.
|
|
11
|
+
*
|
|
12
|
+
* `MainNavigation` is intended to be paired with `GlobalHeader`. If you need a self-contained
|
|
13
|
+
* layout that already includes both header and navigation features, use `Sidebar` instead.
|
|
11
14
|
*/
|
|
12
15
|
export const MainNavigation = forwardRef(({ className, featureShortcut, navigationItems, disableResponsive = false, width = 'normal', customLinkComponent, enableCollapsible, nestedNavigationDisplay, isCollapsed, onCollapsedChange, collapseButtonProps, expandButtonProps, }, ref) => {
|
|
13
16
|
const commonProps = {
|
|
@@ -56,10 +56,14 @@ export function useSidePaneStateController({ open, onClose, ref, insideElement }
|
|
|
56
56
|
// Autofocus to the pane window (`role="dialog"` element) when it opens.
|
|
57
57
|
// Screen readers honor `aria-modal` only when focus is on the `role="dialog"`
|
|
58
58
|
// element or its descendants, so the root wrapper must not be the focus target.
|
|
59
|
+
// `preventScroll` is required because the pane window starts outside its container
|
|
60
|
+
// (the slide-in animation begins at `right: -100%`). Without it the browser scrolls
|
|
61
|
+
// the container to reveal the pane, which shifts the surrounding content and hides
|
|
62
|
+
// the animation — `overflow: hidden` containers are still programmatically scrollable.
|
|
59
63
|
useEffect(() => {
|
|
60
64
|
if (!localOpen)
|
|
61
65
|
return;
|
|
62
|
-
(insideElement ?? sidePaneElement)?.focus();
|
|
66
|
+
(insideElement ?? sidePaneElement)?.focus({ preventScroll: true });
|
|
63
67
|
}, [insideElement, sidePaneElement, localOpen]);
|
|
64
68
|
return {
|
|
65
69
|
localOpen,
|
|
@@ -10,6 +10,9 @@ declare function SidebarBase({ className, logoSlot, mainNavigationItems, extraNa
|
|
|
10
10
|
* A navigation layout component that combines a logo header, a navigation area,
|
|
11
11
|
* and a footer with icon menu and user information.
|
|
12
12
|
*
|
|
13
|
+
* `Sidebar` is a self-contained alternative to `GlobalHeader` and `MainNavigation`, since it
|
|
14
|
+
* already includes both header and navigation features.
|
|
15
|
+
*
|
|
13
16
|
* The width is resizable via the drag handle on the right edge. See the `WidthPersistence` story for
|
|
14
17
|
* an example of persisting the width across reloads via `onWidthChange` and `defaultWidth`.
|
|
15
18
|
*
|
|
@@ -51,6 +51,9 @@ function SidebarToggleButton({ isCollapsed, ariaLabel, navId, onClick, }) {
|
|
|
51
51
|
* A navigation layout component that combines a logo header, a navigation area,
|
|
52
52
|
* and a footer with icon menu and user information.
|
|
53
53
|
*
|
|
54
|
+
* `Sidebar` is a self-contained alternative to `GlobalHeader` and `MainNavigation`, since it
|
|
55
|
+
* already includes both header and navigation features.
|
|
56
|
+
*
|
|
54
57
|
* The width is resizable via the drag handle on the right edge. See the `WidthPersistence` story for
|
|
55
58
|
* an example of persisting the width across reloads via `onWidthChange` and `defaultWidth`.
|
|
56
59
|
*
|
package/dist/src/Toast/Toast.js
CHANGED
|
@@ -8,6 +8,8 @@ import { IconButton } from '../IconButton';
|
|
|
8
8
|
import { Typography } from '../Typography';
|
|
9
9
|
import { mergeRefs } from '../utilities/dom/mergeRefs';
|
|
10
10
|
import { ProgressIndicator } from '../ProgressIndicator';
|
|
11
|
+
import { useLatest } from '../utilities/state/useLatest';
|
|
12
|
+
const MIN_AUTO_DISMISS_DURATION_MS = 5000;
|
|
11
13
|
/**
|
|
12
14
|
* The general-purpose Toast component.
|
|
13
15
|
* This component switches the variants of looks depends on the props: type and actionText.
|
|
@@ -17,7 +19,10 @@ export const Toast = forwardRef(({ className, type = 'success', actionSlot, mess
|
|
|
17
19
|
const toastRef = useRef(null);
|
|
18
20
|
const timerRef = useRef(null);
|
|
19
21
|
const startTimeRef = useRef(null);
|
|
20
|
-
const
|
|
22
|
+
const remainingDurationRef = useRef(0);
|
|
23
|
+
// Prevent duplicate callbacks when a consumer keeps an auto-dismissed Toast mounted.
|
|
24
|
+
const hasAutoDismissedRef = useRef(false);
|
|
25
|
+
const latestOnClose = useLatest(onClose);
|
|
21
26
|
// The status is conveyed by the message content itself, so the status icon is decorative
|
|
22
27
|
// and hidden from assistive technology unless a label is explicitly provided.
|
|
23
28
|
const statusIconLabel = statusIconProps?.['aria-label'];
|
|
@@ -37,46 +42,54 @@ export const Toast = forwardRef(({ className, type = 'success', actionSlot, mess
|
|
|
37
42
|
}
|
|
38
43
|
return _jsx(Pending, { ...statusIconA11yProps });
|
|
39
44
|
})();
|
|
40
|
-
//
|
|
45
|
+
// Keep cleanup tied to the duration lifecycle. Cleaning up in the focus-driven effect below
|
|
46
|
+
// would discard the remaining duration whenever focus enters or leaves the Toast.
|
|
41
47
|
useEffect(() => {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
localDurationRef.current = 0;
|
|
48
|
+
remainingDurationRef.current = duration;
|
|
49
|
+
hasAutoDismissedRef.current = false;
|
|
50
|
+
return () => {
|
|
46
51
|
if (timerRef.current) {
|
|
47
52
|
clearTimeout(timerRef.current);
|
|
48
53
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
timerRef.current = null;
|
|
55
|
+
startTimeRef.current = null;
|
|
56
|
+
};
|
|
52
57
|
}, [duration]);
|
|
53
58
|
useEffect(() => {
|
|
54
|
-
|
|
55
|
-
if (!localDurationRef.current || localDurationRef.current <= 0) {
|
|
59
|
+
if (duration < MIN_AUTO_DISMISS_DURATION_MS || hasAutoDismissedRef.current) {
|
|
56
60
|
return;
|
|
57
61
|
}
|
|
58
|
-
const
|
|
62
|
+
const isFocusWithinToast = !!toastRef.current &&
|
|
59
63
|
!!activeElement &&
|
|
60
64
|
(toastRef.current === activeElement ||
|
|
61
65
|
activeElement.closest('[data-mfui-content="toast-item"]') === toastRef.current);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
timerRef.current = setTimeout(() => {
|
|
66
|
-
onClose?.();
|
|
67
|
-
}, localDurationRef.current);
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
// If the toast is focused, stop the timer and calculate the left time.
|
|
71
|
-
if (isFocusVisible) {
|
|
72
|
-
const passedTime = Date.now() - (startTimeRef.current ?? 0);
|
|
73
|
-
const leftTime = localDurationRef.current - passedTime;
|
|
74
|
-
if (timerRef.current) {
|
|
75
|
-
clearTimeout(timerRef.current);
|
|
66
|
+
if (isFocusWithinToast) {
|
|
67
|
+
if (!timerRef.current || startTimeRef.current === null) {
|
|
68
|
+
return;
|
|
76
69
|
}
|
|
70
|
+
const passedTime = Date.now() - startTimeRef.current;
|
|
71
|
+
clearTimeout(timerRef.current);
|
|
77
72
|
timerRef.current = null;
|
|
78
|
-
|
|
73
|
+
startTimeRef.current = null;
|
|
74
|
+
remainingDurationRef.current = Math.max(remainingDurationRef.current - passedTime, 0);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (timerRef.current) {
|
|
78
|
+
return;
|
|
79
79
|
}
|
|
80
|
-
|
|
80
|
+
if (remainingDurationRef.current <= 0) {
|
|
81
|
+
hasAutoDismissedRef.current = true;
|
|
82
|
+
latestOnClose.current?.();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
startTimeRef.current = Date.now();
|
|
86
|
+
timerRef.current = setTimeout(() => {
|
|
87
|
+
timerRef.current = null;
|
|
88
|
+
startTimeRef.current = null;
|
|
89
|
+
remainingDurationRef.current = 0;
|
|
90
|
+
hasAutoDismissedRef.current = true;
|
|
91
|
+
latestOnClose.current?.();
|
|
92
|
+
}, remainingDurationRef.current);
|
|
93
|
+
}, [activeElement, duration, latestOnClose]);
|
|
81
94
|
return (_jsx(FocusIndicator, { children: _jsxs("div", { ref: mergeRefs(ref, toastRef), className: cx(classes.item, 'mfui-Toast__item', className), role: type === 'failure' ? 'alert' : 'status', "aria-live": type === 'failure' ? undefined : 'polite', "data-mfui-content": "toast-item", ...props, children: [_jsxs("div", { className: cx(classes.body, 'mfui-Toast__body'), children: [_jsxs("div", { className: cx(classes.message, 'mfui-Toast__message'), children: [_jsx("div", { className: cx(classes.leftIcon, 'mfui-Toast__leftIcon'), children: customIcon ?? icon }), _jsx(Typography, { children: message })] }), _jsx(IconButton, { "aria-label": closeButtonProps?.['aria-label'] ?? '閉じる', disabled: !showCloseButton, onClick: onClose, children: _jsx(Close, {}) })] }), actionSlot ? _jsx("div", { className: cx(classes.footer, 'mfui-Toast__footer'), children: actionSlot }) : null] }) }));
|
|
82
95
|
});
|
|
@@ -74,14 +74,24 @@ export function ToastContainer({ items, onItemClose }) {
|
|
|
74
74
|
}, [items, isMobile]);
|
|
75
75
|
// Detect focus on the toast or its content
|
|
76
76
|
useLayoutEffect(() => {
|
|
77
|
-
const
|
|
78
|
-
|
|
77
|
+
const handleFocus = (event) => {
|
|
78
|
+
if (!(event.target instanceof Element)) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
setActiveElement(event.target);
|
|
82
|
+
};
|
|
83
|
+
const handleBlur = (event) => {
|
|
84
|
+
// Losing window focus also has no related target, but DOM focus remains on the same element.
|
|
85
|
+
if (event.relatedTarget === null && !document.hasFocus()) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
setActiveElement(event.relatedTarget instanceof Element ? event.relatedTarget : null);
|
|
79
89
|
};
|
|
80
|
-
window.addEventListener('focus',
|
|
81
|
-
window.addEventListener('blur',
|
|
90
|
+
window.addEventListener('focus', handleFocus, true);
|
|
91
|
+
window.addEventListener('blur', handleBlur, true);
|
|
82
92
|
return () => {
|
|
83
|
-
window.removeEventListener('focus',
|
|
84
|
-
window.removeEventListener('blur',
|
|
93
|
+
window.removeEventListener('focus', handleFocus, true);
|
|
94
|
+
window.removeEventListener('blur', handleBlur, true);
|
|
85
95
|
};
|
|
86
96
|
}, []);
|
|
87
97
|
// Close the first toast when the "Esc" key is pressed and the toast or its content has a focus
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moneyforward/mfui-components",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.2",
|
|
4
4
|
"description": "React UI Component Library for all Money Forward products",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"react-dom": "19.2.3",
|
|
52
52
|
"storybook": "10.5.10",
|
|
53
53
|
"ts-node": "10.9.2",
|
|
54
|
-
"tsx": "4.23.
|
|
54
|
+
"tsx": "4.23.13",
|
|
55
55
|
"vitest": "4.1.11",
|
|
56
56
|
"oxlint-config-mfui": "^1.0.0"
|
|
57
57
|
},
|