@mezzanine-ui/react 1.0.0-canary.2 → 1.0.0-canary.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/AlertBanner/AlertBanner.d.ts +93 -0
  2. package/AlertBanner/AlertBanner.js +135 -0
  3. package/AlertBanner/index.d.ts +2 -0
  4. package/AlertBanner/index.js +1 -0
  5. package/{Overlay/Overlay.d.ts → Backdrop/Backdrop.d.ts} +12 -16
  6. package/Backdrop/Backdrop.js +33 -0
  7. package/Backdrop/index.d.ts +2 -0
  8. package/Backdrop/index.js +1 -0
  9. package/Badge/Badge.d.ts +20 -14
  10. package/Badge/Badge.js +14 -16
  11. package/Badge/BadgeContainer.d.ts +1 -1
  12. package/Badge/BadgeContainer.js +1 -1
  13. package/Badge/index.d.ts +1 -2
  14. package/Badge/typings.d.ts +42 -0
  15. package/Button/Button.js +2 -2
  16. package/Calendar/CalendarContext.js +1 -1
  17. package/DismissButton/DismissButton.d.ts +34 -0
  18. package/DismissButton/DismissButton.js +19 -0
  19. package/DismissButton/index.d.ts +3 -0
  20. package/DismissButton/index.js +5 -0
  21. package/Drawer/Drawer.js +2 -2
  22. package/Dropdown/Dropdown.js +12 -22
  23. package/InlineMessage/InlineMessage.d.ts +33 -0
  24. package/InlineMessage/InlineMessage.js +37 -0
  25. package/InlineMessage/InlineMessageGroup.d.ts +24 -0
  26. package/InlineMessage/InlineMessageGroup.js +21 -0
  27. package/InlineMessage/index.d.ts +4 -0
  28. package/InlineMessage/index.js +2 -0
  29. package/Message/Message.d.ts +4 -2
  30. package/Message/Message.js +85 -8
  31. package/Message/MessageTimerController.d.ts +14 -0
  32. package/Message/MessageTimerController.js +27 -0
  33. package/Modal/Modal.js +2 -2
  34. package/Modal/useModalContainer.d.ts +1 -1
  35. package/Modal/useModalContainer.js +2 -4
  36. package/Navigation/NavigationSubMenu.js +15 -26
  37. package/Notifier/NotifierManager.d.ts +15 -2
  38. package/Notifier/NotifierManager.js +60 -18
  39. package/Notifier/createNotifier.d.ts +9 -0
  40. package/Notifier/createNotifier.js +3 -2
  41. package/Notifier/typings.d.ts +3 -1
  42. package/PageFooter/PageFooter.d.ts +58 -8
  43. package/PageFooter/PageFooter.js +34 -3
  44. package/Popover/Popover.js +4 -8
  45. package/Popper/Popper.d.ts +15 -10
  46. package/Popper/Popper.js +88 -17
  47. package/Popper/index.d.ts +1 -1
  48. package/Portal/Portal.d.ts +13 -5
  49. package/Portal/Portal.js +25 -10
  50. package/Portal/index.d.ts +2 -0
  51. package/Portal/index.js +1 -0
  52. package/Portal/portalRegistry.d.ts +17 -0
  53. package/Portal/portalRegistry.js +99 -0
  54. package/Provider/ConfigProvider.d.ts +1 -0
  55. package/Provider/ConfigProvider.js +1 -0
  56. package/ResultState/ResultState.d.ts +36 -0
  57. package/ResultState/ResultState.js +24 -0
  58. package/ResultState/index.d.ts +2 -0
  59. package/ResultState/index.js +1 -0
  60. package/Skeleton/Skeleton.d.ts +10 -3
  61. package/Skeleton/Skeleton.js +12 -4
  62. package/Spin/Spin.d.ts +39 -0
  63. package/{Loading/Loading.js → Spin/Spin.js} +8 -8
  64. package/Spin/index.d.ts +2 -0
  65. package/Spin/index.js +1 -0
  66. package/Stepper/Step.d.ts +1 -35
  67. package/Stepper/Step.js +43 -10
  68. package/Stepper/Stepper.d.ts +1 -14
  69. package/Stepper/Stepper.js +49 -15
  70. package/Stepper/index.d.ts +3 -2
  71. package/Stepper/index.js +1 -0
  72. package/Stepper/typings.d.ts +77 -0
  73. package/Stepper/useStepDistance.d.ts +10 -0
  74. package/Stepper/useStepDistance.js +81 -0
  75. package/Stepper/useStepper.d.ts +21 -0
  76. package/Stepper/useStepper.js +26 -0
  77. package/Table/Table.js +2 -2
  78. package/Table/TableBody.js +2 -2
  79. package/Tag/Tag.d.ts +2 -25
  80. package/Tag/Tag.js +14 -13
  81. package/Tag/index.d.ts +1 -1
  82. package/Tag/typings.d.ts +79 -0
  83. package/Toggle/Toggle.d.ts +46 -0
  84. package/Toggle/Toggle.js +29 -0
  85. package/Toggle/index.d.ts +3 -0
  86. package/Toggle/index.js +1 -0
  87. package/Tooltip/Tooltip.d.ts +13 -2
  88. package/Tooltip/Tooltip.js +48 -10
  89. package/Tooltip/useDelayMouseEnterLeave.d.ts +1 -2
  90. package/Tooltip/useDelayMouseEnterLeave.js +0 -4
  91. package/Tree/TreeNode.js +5 -5
  92. package/_internal/InputTriggerPopper/InputTriggerPopper.js +14 -24
  93. package/_internal/SlideFadeOverlay/SlideFadeOverlay.d.ts +2 -2
  94. package/_internal/SlideFadeOverlay/SlideFadeOverlay.js +4 -27
  95. package/hooks/useScrollLock.d.ts +28 -0
  96. package/hooks/useScrollLock.js +134 -0
  97. package/index.d.ts +87 -80
  98. package/index.js +74 -69
  99. package/package.json +7 -7
  100. package/utils/get-css-variable-value.d.ts +1 -0
  101. package/utils/get-css-variable-value.js +9 -0
  102. package/Loading/Loading.d.ts +0 -33
  103. package/Loading/index.d.ts +0 -2
  104. package/Loading/index.js +0 -1
  105. package/Overlay/Overlay.js +0 -31
  106. package/Overlay/index.d.ts +0 -2
  107. package/Overlay/index.js +0 -1
  108. package/Switch/Switch.d.ts +0 -43
  109. package/Switch/Switch.js +0 -29
  110. package/Switch/index.d.ts +0 -3
  111. package/Switch/index.js +0 -1
  112. package/utils/scroll-lock.d.ts +0 -2
  113. package/utils/scroll-lock.js +0 -24
@@ -0,0 +1,33 @@
1
+ import { InlineMessageSeverity } from '@mezzanine-ui/core/inline-message';
2
+ import { IconDefinition } from '@mezzanine-ui/icons';
3
+ import { NativeElementPropsWithoutKeyAndRef } from '../utils/jsx-types';
4
+ export interface InlineMessageProps extends NativeElementPropsWithoutKeyAndRef<'div'> {
5
+ /**
6
+ * The content of the inline message value(only string is supported).
7
+ */
8
+ content: string;
9
+ /**
10
+ * The class name of the inline message element.
11
+ */
12
+ className?: string;
13
+ /**
14
+ * The icon of the inline message.
15
+ */
16
+ icon?: IconDefinition;
17
+ /**
18
+ * Called when user clicks the close button.
19
+ */
20
+ onClose?: () => void;
21
+ /**
22
+ * The inline message severity (`'info' | 'warning' | 'error'`).
23
+ */
24
+ severity: InlineMessageSeverity;
25
+ }
26
+ /**
27
+ * The react component for `mezzanine` inline message.
28
+ *
29
+ * Use `InlineMessage` directly in your layout to display contextual feedback,
30
+ * and provide `onClose` callback when you need to react to dismiss events.
31
+ */
32
+ declare const InlineMessage: import("react").ForwardRefExoticComponent<InlineMessageProps & import("react").RefAttributes<HTMLDivElement>>;
33
+ export default InlineMessage;
@@ -0,0 +1,37 @@
1
+ 'use client';
2
+ import { jsx, jsxs } from 'react/jsx-runtime';
3
+ import { forwardRef, useState, useCallback, useMemo } from 'react';
4
+ import { inlineMessageClasses, inlineMessageIcons } from '@mezzanine-ui/core/inline-message';
5
+ import DismissButton from '../DismissButton/DismissButton.js';
6
+ import Icon from '../Icon/Icon.js';
7
+ import cx from 'clsx';
8
+
9
+ /**
10
+ * The react component for `mezzanine` inline message.
11
+ *
12
+ * Use `InlineMessage` directly in your layout to display contextual feedback,
13
+ * and provide `onClose` callback when you need to react to dismiss events.
14
+ */
15
+ const InlineMessage = forwardRef(function InlineMessage(props, ref) {
16
+ const { content, className, icon, onClose, severity } = props;
17
+ const [visible, setVisible] = useState(true);
18
+ const handleClose = useCallback(() => {
19
+ setVisible(false);
20
+ if (onClose) {
21
+ onClose();
22
+ }
23
+ }, [onClose]);
24
+ const iconNode = useMemo(() => {
25
+ if (icon) {
26
+ return jsx(Icon, { className: inlineMessageClasses.icon, icon: icon });
27
+ }
28
+ const severityIcon = inlineMessageIcons[severity];
29
+ return jsx(Icon, { className: inlineMessageClasses.icon, icon: severityIcon });
30
+ }, [icon, severity]);
31
+ if (!visible) {
32
+ return null;
33
+ }
34
+ return (jsxs("div", { ref: ref, "aria-live": "polite", className: cx(inlineMessageClasses.host, inlineMessageClasses.severity(severity), className), role: "status", children: [jsxs("div", { className: inlineMessageClasses.contentContainer, children: [iconNode, jsx("span", { className: inlineMessageClasses.content, children: content })] }), severity === 'info' ? (jsx(DismissButton, { onClick: handleClose, type: "standard", variant: "base" })) : null] }));
35
+ });
36
+
37
+ export { InlineMessage as default };
@@ -0,0 +1,24 @@
1
+ import { Key, ReactNode } from 'react';
2
+ import { NativeElementPropsWithoutKeyAndRef } from '../utils/jsx-types';
3
+ import { InlineMessageProps } from './InlineMessage';
4
+ export interface InlineMessageGroupItem extends Omit<InlineMessageProps, 'content'> {
5
+ key: Key;
6
+ content: string;
7
+ }
8
+ export interface InlineMessageGroupProps extends Omit<NativeElementPropsWithoutKeyAndRef<'div'>, 'children'> {
9
+ /**
10
+ * The inline messages within the group.
11
+ * Ignored when `children` provided.
12
+ */
13
+ items?: InlineMessageGroupItem[];
14
+ /**
15
+ * Custom inline message nodes.
16
+ */
17
+ children?: ReactNode;
18
+ /**
19
+ * Callback invoked when any item triggers `onClose`.
20
+ */
21
+ onItemClose?: (key: Key) => void;
22
+ }
23
+ declare const InlineMessageGroup: import("react").ForwardRefExoticComponent<InlineMessageGroupProps & import("react").RefAttributes<HTMLDivElement>>;
24
+ export default InlineMessageGroup;
@@ -0,0 +1,21 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { inlineMessageGroupClasses } from '@mezzanine-ui/core/inline-message';
3
+ import { forwardRef, useMemo } from 'react';
4
+ import InlineMessage from './InlineMessage.js';
5
+ import cx from 'clsx';
6
+
7
+ const mapItemsToMessages = (items, onItemClose) => items === null || items === void 0 ? void 0 : items.map(({ key, content, onClose, ...restItemProps }) => (jsx(InlineMessage, { ...restItemProps, content: content, onClose: () => {
8
+ if (onClose) {
9
+ onClose();
10
+ }
11
+ if (onItemClose) {
12
+ onItemClose(key);
13
+ }
14
+ } }, key)));
15
+ const InlineMessageGroup = forwardRef(function InlineMessageGroup(props, ref) {
16
+ const { children: childrenProp, className, items, onItemClose, ...rest } = props;
17
+ const children = useMemo(() => childrenProp !== null && childrenProp !== void 0 ? childrenProp : mapItemsToMessages(items, onItemClose), [childrenProp, items, onItemClose]);
18
+ return (jsx("div", { ...rest, ref: ref, className: cx(inlineMessageGroupClasses.host, className), "aria-live": "polite", role: "region", children: children }));
19
+ });
20
+
21
+ export { InlineMessageGroup as default };
@@ -0,0 +1,4 @@
1
+ export { default } from './InlineMessage';
2
+ export type { InlineMessageProps } from './InlineMessage';
3
+ export { default as InlineMessageGroup } from './InlineMessageGroup';
4
+ export type { InlineMessageGroupItem, InlineMessageGroupProps, } from './InlineMessageGroup';
@@ -0,0 +1,2 @@
1
+ export { default } from './InlineMessage.js';
2
+ export { default as InlineMessageGroup } from './InlineMessageGroup.js';
@@ -1,6 +1,6 @@
1
1
  import { MessageSeverity } from '@mezzanine-ui/core/message';
2
2
  import { IconDefinition } from '@mezzanine-ui/icons';
3
- import { FC, Key } from 'react';
3
+ import type { FC, Key } from 'react';
4
4
  import { Notifier, NotifierData, NotifierConfig } from '../Notifier';
5
5
  import { SlideFadeProps } from '../Transition';
6
6
  export interface MessageConfigProps extends Pick<NotifierConfig, 'duration'>, Pick<SlideFadeProps, 'onEnter' | 'onEntering' | 'onEntered' | 'onExit' | 'onExiting' | 'onExited' | 'easing' | 'direction'> {
@@ -25,7 +25,9 @@ export interface MessageData extends Omit<NotifierData, 'onClose'>, MessageConfi
25
25
  */
26
26
  severity?: MessageSeverity;
27
27
  }
28
- export type MessageType = FC<MessageData> & Notifier<MessageData, MessageConfigProps> & Record<string, (message: MessageData['children'], props?: Omit<MessageData, 'children' | 'severity' | 'icon'>) => Key>;
28
+ export type MessageType = FC<MessageData> & Notifier<MessageData, MessageConfigProps> & Record<string, (message: MessageData['children'], props?: Omit<MessageData, 'children' | 'severity' | 'icon'> & {
29
+ key?: Key;
30
+ }) => Key>;
29
31
  /**
30
32
  * The react component for `mezzanine` message.
31
33
  *
@@ -1,6 +1,8 @@
1
- import { useState, useEffect, createElement } from 'react';
1
+ 'use client';
2
+ import { useState, useRef, useCallback, useEffect, createElement } from 'react';
2
3
  import { jsx, jsxs } from 'react/jsx-runtime';
3
4
  import { messageClasses, messageIcons } from '@mezzanine-ui/core/message';
5
+ import { messageTimerController } from './MessageTimerController.js';
4
6
  import SlideFade from '../Transition/SlideFade.js';
5
7
  import Icon from '../Icon/Icon.js';
6
8
  import { createNotifier } from '../Notifier/createNotifier.js';
@@ -15,16 +17,72 @@ import cx from 'clsx';
15
17
  const Message = ((props) => {
16
18
  const { children, duration, icon, reference, severity, onExited: onExitedProp, ...restTransitionProps } = props;
17
19
  const [open, setOpen] = useState(true);
20
+ const timerRef = useRef(null);
21
+ const remainingTimeRef = useRef(duration || 0);
22
+ const startTimeRef = useRef(Date.now());
23
+ // 清理計時器
24
+ const clearTimer = useCallback(() => {
25
+ if (timerRef.current) {
26
+ window.clearTimeout(timerRef.current);
27
+ timerRef.current = null;
28
+ }
29
+ }, []);
30
+ // 開始計時器
31
+ const startTimer = useCallback((time) => {
32
+ clearTimer();
33
+ if (time > 0) {
34
+ startTimeRef.current = Date.now();
35
+ remainingTimeRef.current = time;
36
+ timerRef.current = window.setTimeout(() => {
37
+ setOpen(false);
38
+ }, time);
39
+ }
40
+ }, [clearTimer]);
41
+ // 暫停計時器
42
+ const pauseTimer = useCallback(() => {
43
+ if (timerRef.current) {
44
+ clearTimer();
45
+ const elapsed = Date.now() - startTimeRef.current;
46
+ remainingTimeRef.current = Math.max(0, remainingTimeRef.current - elapsed);
47
+ }
48
+ }, [clearTimer]);
49
+ // 恢復計時器
50
+ const resumeTimer = useCallback(() => {
51
+ if (remainingTimeRef.current > 0) {
52
+ startTimer(remainingTimeRef.current);
53
+ }
54
+ }, [startTimer]);
55
+ // 初始設定計時器
18
56
  useEffect(() => {
19
57
  if (open && duration) {
20
- const timer = window.setTimeout(() => {
21
- setOpen(false);
22
- }, duration);
58
+ startTimer(duration);
59
+ }
60
+ else if (open && duration === false) {
61
+ // duration 為 false 時,清除計時器(不自動關閉)
62
+ clearTimer();
63
+ }
64
+ return () => {
65
+ clearTimer();
66
+ };
67
+ }, [open, duration, clearTimer, startTimer]);
68
+ // 註冊到全域控制器
69
+ useEffect(() => {
70
+ if (reference && duration) {
71
+ messageTimerController.register(reference, {
72
+ pause: pauseTimer,
73
+ resume: resumeTimer,
74
+ });
23
75
  return () => {
24
- window.clearTimeout(timer);
76
+ messageTimerController.unregister(reference);
25
77
  };
26
78
  }
27
- }, [open, duration]);
79
+ }, [reference, duration, pauseTimer, resumeTimer]);
80
+ // 清理計時器(組件卸載時)
81
+ useEffect(() => {
82
+ return () => {
83
+ clearTimer();
84
+ };
85
+ }, [clearTimer]);
28
86
  const onExited = (node) => {
29
87
  if (onExitedProp) {
30
88
  onExitedProp(node);
@@ -32,11 +90,24 @@ const Message = ((props) => {
32
90
  if (reference)
33
91
  Message.remove(reference);
34
92
  };
35
- return (jsx(SlideFade, { in: open, appear: true, onExited: onExited, ...restTransitionProps, children: jsxs("div", { className: cx(messageClasses.host, severity ? messageClasses.severity(severity) : ''), children: [icon ? jsx(Icon, { className: messageClasses.icon, icon: icon }) : null, jsx("span", { className: messageClasses.content, children: children })] }) }));
93
+ const handleMouseEnter = () => {
94
+ messageTimerController.pause();
95
+ };
96
+ const handleMouseLeave = () => {
97
+ messageTimerController.resume();
98
+ };
99
+ const handleFocus = () => {
100
+ messageTimerController.pause();
101
+ };
102
+ const handleBlur = () => {
103
+ messageTimerController.resume();
104
+ };
105
+ return (jsx(SlideFade, { in: open, appear: true, onExited: onExited, direction: "up", ...restTransitionProps, children: jsxs("div", { className: cx(messageClasses.host, severity ? messageClasses.severity(severity) : ''), onBlur: handleBlur, onFocus: handleFocus, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, role: "status", children: [icon ? (jsx(Icon, { className: messageClasses.icon, icon: icon, spin: severity === 'loading' })) : null, jsx("span", { className: messageClasses.content, children: children })] }) }));
36
106
  });
37
107
  const { add, config, destroy, remove } = createNotifier({
38
108
  duration: 3000,
39
- render: (message) => createElement(Message, { ...message, key: undefined }),
109
+ maxCount: 4,
110
+ render: (message) => (createElement(Message, { ...message, reference: message.key, key: undefined })),
40
111
  setRoot: (root) => {
41
112
  root === null || root === void 0 ? void 0 : root.setAttribute('class', messageClasses.root);
42
113
  },
@@ -62,12 +133,17 @@ const severities = [
62
133
  key: 'info',
63
134
  icon: messageIcons.info,
64
135
  },
136
+ {
137
+ key: 'loading',
138
+ icon: messageIcons.loading,
139
+ },
65
140
  ];
66
141
  const validSeverities = [
67
142
  'success',
68
143
  'warning',
69
144
  'error',
70
145
  'info',
146
+ 'loading',
71
147
  ];
72
148
  severities.forEach((severity) => {
73
149
  Message[severity.key] = (message, props) => Message.add({
@@ -77,6 +153,7 @@ severities.forEach((severity) => {
77
153
  ? severity.key
78
154
  : undefined,
79
155
  icon: severity.icon,
156
+ duration: severity.key === 'loading' ? false : props === null || props === void 0 ? void 0 : props.duration,
80
157
  });
81
158
  });
82
159
 
@@ -0,0 +1,14 @@
1
+ import type { Key } from 'react';
2
+ declare class MessageTimerController {
3
+ private isPaused;
4
+ private timers;
5
+ pause(): void;
6
+ register(key: Key, callbacks: {
7
+ pause: () => void;
8
+ resume: () => void;
9
+ }): void;
10
+ resume(): void;
11
+ unregister(key: Key): void;
12
+ }
13
+ export declare const messageTimerController: MessageTimerController;
14
+ export {};
@@ -0,0 +1,27 @@
1
+ class MessageTimerController {
2
+ constructor() {
3
+ this.isPaused = false;
4
+ this.timers = new Map();
5
+ }
6
+ pause() {
7
+ if (this.isPaused)
8
+ return;
9
+ this.isPaused = true;
10
+ this.timers.forEach((timer) => timer.pause());
11
+ }
12
+ register(key, callbacks) {
13
+ this.timers.set(key, callbacks);
14
+ }
15
+ resume() {
16
+ if (!this.isPaused)
17
+ return;
18
+ this.isPaused = false;
19
+ this.timers.forEach((timer) => timer.resume());
20
+ }
21
+ unregister(key) {
22
+ this.timers.delete(key);
23
+ }
24
+ }
25
+ const messageTimerController = new MessageTimerController();
26
+
27
+ export { messageTimerController };
package/Modal/Modal.js CHANGED
@@ -11,13 +11,13 @@ import cx from 'clsx';
11
11
  * The react component for `mezzanine` modal.
12
12
  */
13
13
  const Modal = forwardRef(function Modal(props, ref) {
14
- const { children, className, container, direction = 'down', disableCloseOnBackdropClick = false, disableCloseOnEscapeKeyDown = false, disablePortal = false, fullScreen = false, hideBackdrop = false, hideCloseIcon = false, invisibleBackdrop = false, loading = false, onBackdropClick, onClose, open, severity = 'info', size = 'medium', ...rest } = props;
14
+ const { children, className, container, direction = 'down', disableCloseOnBackdropClick = false, disableCloseOnEscapeKeyDown = false, disablePortal = false, fullScreen = false, hideCloseIcon = false, loading = false, onBackdropClick, onClose, open, severity = 'info', size = 'medium', ...rest } = props;
15
15
  const modalControl = useMemo(() => ({
16
16
  loading,
17
17
  severity,
18
18
  }), [loading, severity]);
19
19
  const { Container: ModalContainer } = useModalContainer();
20
- return (jsx(ModalControlContext.Provider, { value: modalControl, children: jsx(ModalContainer, { className: modalClasses.overlay, container: container, direction: direction, disableCloseOnBackdropClick: disableCloseOnBackdropClick, disableCloseOnEscapeKeyDown: disableCloseOnEscapeKeyDown, disablePortal: disablePortal, hideBackdrop: hideBackdrop, invisibleBackdrop: invisibleBackdrop, onBackdropClick: onBackdropClick, onClose: onClose, open: open, ref: ref, children: jsxs("div", { ...rest, className: cx(modalClasses.host, modalClasses.severity(severity), modalClasses.size(size), {
20
+ return (jsx(ModalControlContext.Provider, { value: modalControl, children: jsx(ModalContainer, { className: modalClasses.overlay, container: container, direction: direction, disableCloseOnBackdropClick: disableCloseOnBackdropClick, disableCloseOnEscapeKeyDown: disableCloseOnEscapeKeyDown, disablePortal: disablePortal, onBackdropClick: onBackdropClick, onClose: onClose, open: open, ref: ref, children: jsxs("div", { ...rest, className: cx(modalClasses.host, modalClasses.severity(severity), modalClasses.size(size), {
21
21
  [modalClasses.fullScreen]: fullScreen,
22
22
  [modalClasses.withCloseIcon]: !hideCloseIcon,
23
23
  }, className), role: "dialog", children: [children, !hideCloseIcon && (jsx(Icon, { className: modalClasses.closeIcon, icon: TimesIcon, onClick: onClose }))] }) }) }));
@@ -1,5 +1,5 @@
1
1
  import { SlideFadeOverlayProps } from '../_internal/SlideFadeOverlay';
2
2
  export default function useModalContainer(): {
3
3
  Container: import("react").ForwardRefExoticComponent<SlideFadeOverlayProps & import("react").RefAttributes<HTMLDivElement>>;
4
- defaultOptions: Pick<SlideFadeOverlayProps, "className" | "open" | "disablePortal" | "direction" | "disableCloseOnBackdropClick" | "hideBackdrop" | "invisibleBackdrop" | "disableCloseOnEscapeKeyDown">;
4
+ defaultOptions: Pick<SlideFadeOverlayProps, "className" | "open" | "disablePortal" | "direction" | "disableCloseOnBackdropClick" | "disableCloseOnEscapeKeyDown">;
5
5
  };
@@ -9,13 +9,11 @@ const defaultOptions = {
9
9
  disableCloseOnBackdropClick: false,
10
10
  disableCloseOnEscapeKeyDown: false,
11
11
  disablePortal: false,
12
- hideBackdrop: false,
13
- invisibleBackdrop: false,
14
12
  open: false,
15
13
  };
16
14
  const ModalContainer = forwardRef((props, ref) => {
17
- const { className = defaultOptions.className, children, container, direction = defaultOptions.direction, disableCloseOnBackdropClick = defaultOptions.disableCloseOnBackdropClick, disableCloseOnEscapeKeyDown = defaultOptions.disableCloseOnEscapeKeyDown, disablePortal = defaultOptions.disablePortal, hideBackdrop = defaultOptions.hideBackdrop, invisibleBackdrop = defaultOptions.invisibleBackdrop, onBackdropClick, onClose, open = defaultOptions.open, } = props;
18
- return (jsx(SlideFadeOverlay, { className: className, container: container, direction: direction, disableCloseOnBackdropClick: disableCloseOnBackdropClick, disableCloseOnEscapeKeyDown: disableCloseOnEscapeKeyDown, disablePortal: disablePortal, hideBackdrop: hideBackdrop, invisibleBackdrop: invisibleBackdrop, onBackdropClick: onBackdropClick, onClose: onClose, open: open, ref: ref, children: children }));
15
+ const { className = defaultOptions.className, children, container, direction = defaultOptions.direction, disableCloseOnBackdropClick = defaultOptions.disableCloseOnBackdropClick, disableCloseOnEscapeKeyDown = defaultOptions.disableCloseOnEscapeKeyDown, disablePortal = defaultOptions.disablePortal, onBackdropClick, onClose, open = defaultOptions.open, } = props;
16
+ return (jsx(SlideFadeOverlay, { className: className, container: container, direction: direction, disableCloseOnBackdropClick: disableCloseOnBackdropClick, disableCloseOnEscapeKeyDown: disableCloseOnEscapeKeyDown, disablePortal: disablePortal, onBackdropClick: onBackdropClick, onClose: onClose, open: open, ref: ref, children: children }));
19
17
  });
20
18
  function useModalContainer() {
21
19
  return {
@@ -3,6 +3,7 @@ import { jsx, jsxs } from 'react/jsx-runtime';
3
3
  import { forwardRef, useState, useRef, useContext } from 'react';
4
4
  import { navigationSubMenuClasses } from '@mezzanine-ui/core/navigation';
5
5
  import { ChevronUpIcon, ChevronDownIcon } from '@mezzanine-ui/icons';
6
+ import { offset, size } from '@floating-ui/react-dom';
6
7
  import { useClickAway } from '../hooks/useClickAway.js';
7
8
  import { useComposeRefs } from '../hooks/useComposeRefs.js';
8
9
  import NavigationItem from './NavigationItem.js';
@@ -12,31 +13,14 @@ import Popper from '../Popper/Popper.js';
12
13
  import Collapse from '../Transition/Collapse.js';
13
14
  import cx from 'clsx';
14
15
 
15
- /* istanbul ignore next */
16
- const popperOptions = {
17
- modifiers: [
18
- {
19
- name: 'offset',
20
- options: {
21
- offset: [0, 0],
22
- },
23
- },
24
- {
25
- name: 'sameWidth',
26
- enabled: true,
27
- phase: 'beforeWrite',
28
- requires: ['computeStyles'],
29
- fn: ({ state }) => {
30
- const reassignState = state;
31
- reassignState.styles.popper.width = `${state.rects.reference.width}px`;
32
- },
33
- effect: ({ state }) => {
34
- const reassignState = state;
35
- reassignState.elements.popper.style.width = `${state.elements.reference.getBoundingClientRect().width}px`;
36
- },
37
- },
38
- ],
39
- };
16
+ // Middleware to make the submenu have the same width as the reference element
17
+ const sameWidthMiddleware = size({
18
+ apply({ rects, elements }) {
19
+ Object.assign(elements.floating.style, {
20
+ width: `${rects.reference.width}px`,
21
+ });
22
+ },
23
+ });
40
24
  const NavigationSubMenu = forwardRef((props, ref) => {
41
25
  const { active, className, children = [], defaultOpen = false, icon, title, ...rest } = props;
42
26
  const [open, setOpen] = useState(defaultOpen);
@@ -53,7 +37,12 @@ const NavigationSubMenu = forwardRef((props, ref) => {
53
37
  };
54
38
  }, nodeRef, [open, orientation]);
55
39
  const WrapChildren = jsx("ul", { className: navigationSubMenuClasses.group, children: children });
56
- return (jsxs(NavigationItem, { ...rest, ref: composedNodeRef, className: cx(navigationSubMenuClasses.host, active && navigationSubMenuClasses.active, open && navigationSubMenuClasses.open, icon && orientation === 'vertical' && navigationSubMenuClasses.indent, className), onClick: () => setOpen(!open), children: [jsxs("div", { className: navigationSubMenuClasses.title, children: [icon && jsx(Icon, { className: navigationSubMenuClasses.icon, icon: icon }), title, jsx(Icon, { className: navigationSubMenuClasses.toggleIcon, icon: GroupToggleIcon })] }), orientation === 'horizontal' && (jsx(Popper, { anchor: nodeRef, disablePortal: true, open: !!open, options: popperOptions, children: WrapChildren })), orientation === 'vertical' && (jsx(Collapse, { style: {
40
+ return (jsxs(NavigationItem, { ...rest, ref: composedNodeRef, className: cx(navigationSubMenuClasses.host, active && navigationSubMenuClasses.active, open && navigationSubMenuClasses.open, icon && orientation === 'vertical' && navigationSubMenuClasses.indent, className), onClick: () => setOpen(!open), children: [jsxs("div", { className: navigationSubMenuClasses.title, children: [icon && jsx(Icon, { className: navigationSubMenuClasses.icon, icon: icon }), title, jsx(Icon, { className: navigationSubMenuClasses.toggleIcon, icon: GroupToggleIcon })] }), orientation === 'horizontal' && (jsx(Popper, { anchor: nodeRef, disablePortal: true, open: !!open, options: {
41
+ middleware: [
42
+ offset({ mainAxis: 0, crossAxis: 0 }),
43
+ sameWidthMiddleware,
44
+ ],
45
+ }, children: WrapChildren })), orientation === 'vertical' && (jsx(Collapse, { style: {
57
46
  width: '100%',
58
47
  }, in: !!open, children: WrapChildren }))] }));
59
48
  });
@@ -1,5 +1,6 @@
1
+ import type { ReactNode } from 'react';
1
2
  import { Key, RefObject } from 'react';
2
- import { NotifierData, NotifierConfig, RenderNotifier } from './typings';
3
+ import { NotifierConfig, NotifierData, RenderNotifier } from './typings';
3
4
  export interface NotifierController<N extends NotifierData> {
4
5
  add: (notifier: N & {
5
6
  key: Key;
@@ -12,6 +13,18 @@ export interface NotifierManagerProps<N extends NotifierData> extends Pick<Notif
12
13
  key: Key;
13
14
  })[];
14
15
  render: RenderNotifier<N>;
16
+ /**
17
+ * Custom wrapper for rendered notifiers (e.g. AlertBanner group container).
18
+ */
19
+ renderContainer?: (children: ReactNode) => ReactNode;
20
+ /**
21
+ * Sorting hook to enforce display/queue ordering before updates.
22
+ */
23
+ sortBeforeUpdate?: (notifiers: (N & {
24
+ key: Key;
25
+ })[]) => (N & {
26
+ key: Key;
27
+ })[];
15
28
  }
16
- declare function NotifierManager<N extends NotifierData>(props: NotifierManagerProps<N>): import("react/jsx-runtime").JSX.Element;
29
+ declare function NotifierManager<N extends NotifierData>(props: NotifierManagerProps<N>): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react/jsx-runtime").JSX.Element | null | undefined;
17
30
  export default NotifierManager;
@@ -1,31 +1,73 @@
1
- import { jsx, Fragment } from 'react/jsx-runtime';
2
- import { useState, useMemo, useImperativeHandle, Fragment as Fragment$1 } from 'react';
1
+ import { jsx, Fragment as Fragment$1 } from 'react/jsx-runtime';
2
+ import { useCallback, useState, useEffect, useMemo, useImperativeHandle, Fragment } from 'react';
3
3
 
4
4
  function NotifierManager(props) {
5
- const { controllerRef, defaultNotifiers = [], maxCount, render } = props;
6
- const [notifiers, setNotifiers] = useState(defaultNotifiers);
7
- const notifiersShouldRendered = typeof maxCount === 'number' && notifiers.length > maxCount
8
- ? notifiers.slice(0, maxCount)
9
- : notifiers;
5
+ const { controllerRef, defaultNotifiers = [], maxCount, render, renderContainer, sortBeforeUpdate, } = props;
6
+ // 只有在呼叫端有提供 sortBeforeUpdate 時才執行排序,否則維持原本順序。
7
+ const sortNotifiers = useCallback((notifiers) => sortBeforeUpdate ? sortBeforeUpdate(notifiers) : notifiers, [sortBeforeUpdate]);
8
+ const [displayedNotifiers, setDisplayedNotifiers] = useState(() => sortNotifiers(defaultNotifiers));
9
+ const [queuedNotifiers, setQueuedNotifiers] = useState([]);
10
+ // 當有空位時,從 queue 中補上
11
+ useEffect(() => {
12
+ if (queuedNotifiers.length > 0) {
13
+ const hasMaxCount = typeof maxCount === 'number';
14
+ const availableSlots = hasMaxCount
15
+ ? maxCount - displayedNotifiers.length
16
+ : Infinity;
17
+ if (availableSlots > 0) {
18
+ const notifiersToDisplay = queuedNotifiers.slice(0, availableSlots);
19
+ setDisplayedNotifiers((prev) => sortNotifiers([...prev, ...notifiersToDisplay]));
20
+ setQueuedNotifiers((prev) => prev.slice(availableSlots));
21
+ }
22
+ }
23
+ }, [displayedNotifiers.length, queuedNotifiers, maxCount, sortNotifiers]);
10
24
  const controller = useMemo(() => ({
11
25
  add(notifier) {
12
- setNotifiers((prev) => {
26
+ setDisplayedNotifiers((prev) => {
13
27
  const notifierIndex = prev.findIndex(({ key }) => key === notifier.key);
14
- return ~notifierIndex
15
- ? [
16
- ...prev.slice(0, notifierIndex),
17
- notifier,
18
- ...prev.slice(notifierIndex + 1, prev.length),
19
- ]
20
- : [...prev, notifier];
28
+ // 如果已存在,則更新該訊息
29
+ if (~notifierIndex) {
30
+ const next = [...prev];
31
+ next[notifierIndex] = notifier;
32
+ return sortNotifiers(next);
33
+ }
34
+ // 新訊息:檢查是否超過 maxCount
35
+ const hasMaxCount = typeof maxCount === 'number';
36
+ if (hasMaxCount && prev.length >= maxCount) {
37
+ if (sortBeforeUpdate) {
38
+ // 需要排序時,先把舊有與新增併在一起排序,
39
+ // 取前 maxCount 個顯示,其餘補進 queue。
40
+ const allNotifiers = sortNotifiers([...prev, notifier]);
41
+ const toDisplay = allNotifiers.slice(0, maxCount);
42
+ const toQueue = allNotifiers.slice(maxCount);
43
+ if (toQueue.length > 0) {
44
+ setQueuedNotifiers((queue) => [...queue, ...toQueue]);
45
+ }
46
+ return toDisplay;
47
+ }
48
+ // 超過上限,加入 queue
49
+ setQueuedNotifiers((queue) => [...queue, notifier]);
50
+ return prev;
51
+ }
52
+ // 未超過上限,直接加到最後
53
+ // 直接加入並依需求排序。
54
+ return sortNotifiers([...prev, notifier]);
21
55
  });
22
56
  },
23
57
  remove(key) {
24
- setNotifiers((prev) => prev.filter((m) => m.key !== key));
58
+ setDisplayedNotifiers((prev) => prev.filter((m) => m.key !== key));
59
+ setQueuedNotifiers((prev) => prev.filter((m) => m.key !== key));
25
60
  },
26
- }), []);
61
+ }), [maxCount, sortBeforeUpdate, sortNotifiers]);
27
62
  useImperativeHandle(controllerRef, () => controller, [controller]);
28
- return (jsx(Fragment, { children: notifiersShouldRendered.map((notifier) => (jsx(Fragment$1, { children: render(notifier) }, notifier.key))) }));
63
+ const renderedNotifiers = displayedNotifiers.map((notifier) => (jsx(Fragment, { children: render(notifier) }, notifier.key)));
64
+ if (renderContainer) {
65
+ if (!renderedNotifiers.length) {
66
+ return null;
67
+ }
68
+ return renderContainer(renderedNotifiers);
69
+ }
70
+ return (jsx(Fragment$1, { children: renderedNotifiers }));
29
71
  }
30
72
 
31
73
  export { NotifierManager as default };
@@ -1,3 +1,4 @@
1
+ import { type NotifierManagerProps } from './NotifierManager';
1
2
  import { Notifier, NotifierConfig, NotifierData, RenderNotifier } from './typings';
2
3
  export interface CreateNotifierProps<N extends NotifierData, C extends NotifierConfig> extends NotifierConfig {
3
4
  /**
@@ -12,6 +13,14 @@ export interface CreateNotifierProps<N extends NotifierData, C extends NotifierC
12
13
  * The method to set attributes or listeners to root.
13
14
  */
14
15
  setRoot?: (root: HTMLDivElement) => void;
16
+ /**
17
+ * Custom wrapper for rendered notifiers (e.g. AlertBanner group container).
18
+ */
19
+ renderContainer?: NotifierManagerProps<N>['renderContainer'];
20
+ /**
21
+ * Sorting hook to enforce display/queue ordering before updates.
22
+ */
23
+ sortBeforeUpdate?: NotifierManagerProps<N>['sortBeforeUpdate'];
15
24
  }
16
25
  /**
17
26
  * The utility factory for `mezzanine` to create a notifier.
@@ -1,3 +1,4 @@
1
+ 'use client';
1
2
  import { jsx } from 'react/jsx-runtime';
2
3
  import { createRef } from 'react';
3
4
  import { createRoot } from 'react-dom/client';
@@ -9,7 +10,7 @@ import NotifierManager from './NotifierManager.js';
9
10
  * When APIs are called, Notifier will dynamically render a new react instance by `ReactDOM.render` method.
10
11
  */
11
12
  function createNotifier(props) {
12
- const { config: configProp, render: renderNotifier, setRoot, duration, maxCount, ...restNotifierProps } = props;
13
+ const { config: configProp, render: renderNotifier, setRoot, duration, maxCount, renderContainer, sortBeforeUpdate, ...restNotifierProps } = props;
13
14
  const container = typeof document === 'undefined' ? null : document.createElement('div');
14
15
  const root = container ? createRoot(container) : null;
15
16
  const controllerRef = createRef();
@@ -40,7 +41,7 @@ function createNotifier(props) {
40
41
  controllerRef.current.add(resolvedNotifier);
41
42
  }
42
43
  else {
43
- root === null || root === void 0 ? void 0 : root.render(jsx(NotifierManager, { controllerRef: controllerRef, defaultNotifiers: [resolvedNotifier], maxCount: currentConfig.maxCount, render: renderNotifier }));
44
+ root === null || root === void 0 ? void 0 : root.render(jsx(NotifierManager, { controllerRef: controllerRef, defaultNotifiers: [resolvedNotifier], maxCount: currentConfig.maxCount, render: renderNotifier, renderContainer: renderContainer, sortBeforeUpdate: sortBeforeUpdate }));
44
45
  }
45
46
  return resolvedNotifier.key;
46
47
  },
@@ -16,7 +16,9 @@ export interface NotifierData extends Pick<NotifierConfig, 'duration'> {
16
16
  */
17
17
  onClose?: (key: Key) => void;
18
18
  }
19
- export type RenderNotifier<N extends NotifierData> = (notifier: N) => ReactNode;
19
+ export type RenderNotifier<N extends NotifierData> = (notifier: N & {
20
+ key: Key;
21
+ }) => ReactNode;
20
22
  export interface Notifier<N extends NotifierData, C extends NotifierConfig = NotifierConfig> {
21
23
  add: (notifier: N & {
22
24
  key?: Key;