@moneyforward/mfui-components 4.1.0 → 4.1.1
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/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.1",
|
|
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
|
},
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
"@floating-ui/react-dom": "^2.1.2",
|
|
64
64
|
"@tanstack/react-virtual": "^3.13.18",
|
|
65
65
|
"dayjs": "^1.11.13",
|
|
66
|
-
"@moneyforward/mfui-
|
|
67
|
-
"@moneyforward/mfui-
|
|
66
|
+
"@moneyforward/mfui-icons-react": "^4.0.0",
|
|
67
|
+
"@moneyforward/mfui-design-tokens": "^4.0.0"
|
|
68
68
|
},
|
|
69
69
|
"scripts": {
|
|
70
70
|
"prepare:panda": "panda codegen",
|