@rainbow-oh/yee-c 0.8.2 → 0.8.4

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 (123) hide show
  1. package/dist/cjs/Form/form-store.js +3 -0
  2. package/dist/cjs/Input/style/index.less +1 -0
  3. package/dist/cjs/Select/interface.d.ts +1 -1
  4. package/dist/cjs/Selector/style/index.less +1 -0
  5. package/dist/cjs/Table/icon/arrow-icon.js +1 -1
  6. package/dist/cjs/Table/theader/cell.js +6 -3
  7. package/dist/cjs/TextArea/style/index.less +1 -0
  8. package/dist/cjs/VerificationCode/style/index.less +1 -0
  9. package/dist/cjs/hooks/useCountdown.d.ts +2 -1
  10. package/dist/cjs/hooks/useCountdown.js +46 -23
  11. package/dist/cjs/locale/interface.d.ts +3 -0
  12. package/dist/cjs/locale/lang/en_US.js +4 -1
  13. package/dist/cjs/locale/lang/ja_JP.js +4 -1
  14. package/dist/cjs/locale/lang/zh_CN.js +4 -1
  15. package/dist/cjs/locale/lang/zh_TW.js +4 -1
  16. package/dist/cjs/message/demo/base.js +1 -1
  17. package/dist/cjs/message/index.d.ts +1 -1
  18. package/dist/cjs/message/interface.d.ts +16 -7
  19. package/dist/cjs/message/message-provider.d.ts +2 -1
  20. package/dist/cjs/message/message-provider.js +1 -1
  21. package/dist/cjs/message/message.js +14 -9
  22. package/dist/cjs/message/style/index.less +3 -1
  23. package/dist/cjs/message/use-message.d.ts +3 -12
  24. package/dist/cjs/message/use-message.js +103 -45
  25. package/dist/cjs/message/wrapper.d.ts +21 -27
  26. package/dist/cjs/message/wrapper.js +49 -19
  27. package/dist/cjs/notice/index.d.ts +1 -1
  28. package/dist/cjs/notice/interface.d.ts +17 -7
  29. package/dist/cjs/notice/notice-list.d.ts +3 -3
  30. package/dist/cjs/notice/notice-provider.d.ts +2 -1
  31. package/dist/cjs/notice/notice-provider.js +1 -1
  32. package/dist/cjs/notice/notice.js +63 -20
  33. package/dist/cjs/notice/style/index.less +13 -1
  34. package/dist/cjs/notice/use-notice.d.ts +2 -11
  35. package/dist/cjs/notice/use-notice.js +156 -71
  36. package/dist/cjs/notice/wrapper.d.ts +19 -15
  37. package/dist/cjs/notice/wrapper.js +122 -52
  38. package/dist/cjs/style/size.css +3 -3
  39. package/dist/cjs/style/themes.css +1 -1
  40. package/dist/cjs/style/variables.css +4 -4
  41. package/dist/cjs/version.d.ts +1 -1
  42. package/dist/cjs/version.js +1 -1
  43. package/dist/esm/Form/form-store.js +25 -17
  44. package/dist/esm/Input/style/index.less +1 -0
  45. package/dist/esm/Select/interface.d.ts +1 -1
  46. package/dist/esm/Selector/style/index.less +1 -0
  47. package/dist/esm/Table/icon/arrow-icon.js +1 -1
  48. package/dist/esm/Table/theader/cell.js +7 -3
  49. package/dist/esm/TextArea/style/index.less +1 -0
  50. package/dist/esm/VerificationCode/style/index.less +1 -0
  51. package/dist/esm/hooks/useCountdown.d.ts +2 -1
  52. package/dist/esm/hooks/useCountdown.js +44 -14
  53. package/dist/esm/locale/interface.d.ts +3 -0
  54. package/dist/esm/locale/lang/en_US.js +4 -1
  55. package/dist/esm/locale/lang/ja_JP.js +4 -1
  56. package/dist/esm/locale/lang/zh_CN.js +4 -1
  57. package/dist/esm/locale/lang/zh_TW.js +4 -1
  58. package/dist/esm/message/demo/base.js +4 -1
  59. package/dist/esm/message/index.d.ts +1 -1
  60. package/dist/esm/message/interface.d.ts +16 -7
  61. package/dist/esm/message/message-provider.d.ts +2 -1
  62. package/dist/esm/message/message-provider.js +1 -1
  63. package/dist/esm/message/message.js +22 -9
  64. package/dist/esm/message/style/index.less +3 -1
  65. package/dist/esm/message/use-message.d.ts +3 -12
  66. package/dist/esm/message/use-message.js +129 -79
  67. package/dist/esm/message/wrapper.d.ts +21 -27
  68. package/dist/esm/message/wrapper.js +60 -18
  69. package/dist/esm/notice/index.d.ts +1 -1
  70. package/dist/esm/notice/interface.d.ts +17 -7
  71. package/dist/esm/notice/notice-list.d.ts +3 -3
  72. package/dist/esm/notice/notice-provider.d.ts +2 -1
  73. package/dist/esm/notice/notice-provider.js +1 -1
  74. package/dist/esm/notice/notice.js +73 -21
  75. package/dist/esm/notice/style/index.less +13 -1
  76. package/dist/esm/notice/use-notice.d.ts +2 -11
  77. package/dist/esm/notice/use-notice.js +180 -100
  78. package/dist/esm/notice/wrapper.d.ts +19 -15
  79. package/dist/esm/notice/wrapper.js +144 -62
  80. package/dist/esm/style/size.css +3 -3
  81. package/dist/esm/style/themes.css +1 -1
  82. package/dist/esm/style/variables.css +4 -4
  83. package/dist/esm/version.d.ts +1 -1
  84. package/dist/esm/version.js +1 -1
  85. package/dist/umd/yee-c.min.css +2 -2
  86. package/dist/umd/yee-c.min.js +1 -1
  87. package/package.json +1 -1
  88. package/src/Form/form-store.ts +5 -0
  89. package/src/Input/style/index.less +1 -0
  90. package/src/Select/interface.ts +1 -1
  91. package/src/Selector/style/index.less +1 -0
  92. package/src/Table/icon/arrow-icon.tsx +1 -1
  93. package/src/Table/theader/cell.tsx +6 -3
  94. package/src/TextArea/style/index.less +1 -0
  95. package/src/VerificationCode/style/index.less +1 -0
  96. package/src/hooks/useCountdown.ts +52 -21
  97. package/src/locale/interface.ts +3 -0
  98. package/src/locale/lang/en_US.ts +3 -0
  99. package/src/locale/lang/ja_JP.ts +3 -0
  100. package/src/locale/lang/zh_CN.ts +3 -0
  101. package/src/locale/lang/zh_TW.ts +3 -0
  102. package/src/message/demo/base.tsx +1 -1
  103. package/src/message/index.ts +1 -1
  104. package/src/message/interface.ts +22 -7
  105. package/src/message/message-provider.tsx +3 -2
  106. package/src/message/message.test.tsx +283 -0
  107. package/src/message/message.tsx +14 -9
  108. package/src/message/style/index.less +3 -1
  109. package/src/message/use-message.tsx +144 -67
  110. package/src/message/wrapper.tsx +62 -20
  111. package/src/notice/index.en-US.md +76 -75
  112. package/src/notice/index.ts +6 -1
  113. package/src/notice/index.zh-CN.md +3 -2
  114. package/src/notice/interface.ts +31 -14
  115. package/src/notice/notice-list.tsx +3 -9
  116. package/src/notice/notice-provider.tsx +3 -2
  117. package/src/notice/notice.test.tsx +450 -0
  118. package/src/notice/notice.tsx +69 -21
  119. package/src/notice/style/index.less +13 -1
  120. package/src/notice/use-notice.tsx +210 -94
  121. package/src/notice/wrapper.tsx +159 -71
  122. package/src/style/size.css +3 -3
  123. package/src/version.ts +1 -1
@@ -32,7 +32,7 @@ toc: 'content'
32
32
  | --- | --- | --- | --- |
33
33
  | title | 通知标题 | ReactNode | - |
34
34
  | content | 通知内容 | ReactNode | - |
35
- | duration | 自动关闭时间(秒),设为 0 时不自动关闭 | number | `4.5` |
35
+ | duration | 自动关闭时间(毫秒),设为 0 时不自动关闭 | number | `4500` |
36
36
  | icon | 自定义图标 | ReactNode | - |
37
37
  | key | 当前通知的唯一标识 | string \| number | - |
38
38
  | placement | 显示位置 | `'topLeft' \| 'topRight' \| 'bottomLeft' \| 'bottomRight' \| 'top' \| 'bottom'` | `'topRight'` |
@@ -53,6 +53,7 @@ toc: 'content'
53
53
  | notice.warning(config) | 打开警告通知 | string \| [NoticeConfigs](#noticeconfigs) | - |
54
54
  | notice.error(config) | 打开错误通知 | string \| [NoticeConfigs](#noticeconfigs) | - |
55
55
  | notice.destroy(key) | 手动销毁指定 key 的通知 | string \| number | - |
56
+ | notice.clear(placement?) | 清除指定位置或全部通知 | placement | - |
56
57
 
57
58
  ### NoticeConfigs
58
59
 
@@ -63,7 +64,7 @@ toc: 'content'
63
64
  | style | 自定义根样式 | CSSProperties | - |
64
65
  | title | 通知标题 | ReactNode | - |
65
66
  | content | 通知内容 | ReactNode | - |
66
- | duration | 自动关闭时间(秒),设为 0 时不自动关闭 | number | `4.5` |
67
+ | duration | 自动关闭时间(毫秒),设为 0 时不自动关闭 | number | `4500` |
67
68
  | icon | 自定义图标 | ReactNode | - |
68
69
  | key | 当前通知的唯一标识 | string \| number | - |
69
70
  | placement | 显示位置 | `'topLeft' \| 'topRight' \| 'bottomLeft' \| 'bottomRight' \| 'top' \| 'bottom'` | `'topRight'` |
@@ -1,8 +1,17 @@
1
+ export type PlacementType =
2
+ | 'topLeft'
3
+ | 'topRight'
4
+ | 'bottomLeft'
5
+ | 'bottomRight'
6
+ | 'top'
7
+ | 'bottom';
8
+
1
9
  export type NoticeConfig = {
2
- /**
3
- * test identifier
4
- */
5
- 'data-testid'?: string;
10
+ [dataAttribute: `data-${string}`]:
11
+ | string
12
+ | number
13
+ | boolean
14
+ | undefined;
6
15
  /**
7
16
  * Custom prefix class name
8
17
  */
@@ -24,7 +33,7 @@ export type NoticeConfig = {
24
33
  */
25
34
  content: React.ReactNode;
26
35
  /**
27
- * Auto close time (ms), 0 means no auto close
36
+ * Auto close time (milliseconds), 0 means no auto close
28
37
  * @default 4500
29
38
  */
30
39
  duration?: number;
@@ -40,13 +49,7 @@ export type NoticeConfig = {
40
49
  * Display position
41
50
  * @default 'topRight'
42
51
  */
43
- placement?:
44
- | 'topLeft'
45
- | 'topRight'
46
- | 'bottomLeft'
47
- | 'bottomRight'
48
- | 'top'
49
- | 'bottom';
52
+ placement?: PlacementType;
50
53
  /**
51
54
  * Whether to show progress bar
52
55
  */
@@ -70,19 +73,33 @@ export type NoticeConfig = {
70
73
  closable?: boolean;
71
74
  };
72
75
 
76
+ export type NoticeClose = () => void;
77
+
78
+ export interface NoticeApi {
79
+ open: (config: string | NoticeConfig) => NoticeClose;
80
+ info: (config: string | NoticeConfig) => NoticeClose;
81
+ success: (config: string | NoticeConfig) => NoticeClose;
82
+ warning: (config: string | NoticeConfig) => NoticeClose;
83
+ error: (config: string | NoticeConfig) => NoticeClose;
84
+ destroy: (key: string | number) => void;
85
+ clear: (placement?: PlacementType) => void;
86
+ }
87
+
73
88
  // With status
74
89
  export type WrapperedNoticeConfig = NoticeConfig & {
75
90
  status?: 'info' | 'success' | 'warning' | 'error';
76
91
  };
77
92
 
78
- // With key
93
+ // Internal notice state
79
94
  export type NoticeType = WrapperedNoticeConfig & {
80
95
  key: string | number;
96
+ placement: PlacementType;
97
+ timerGeneration: number;
81
98
  };
82
99
 
83
100
  export interface NoticeProps extends NoticeType {
84
101
  /**
85
102
  * Destroy component callback
86
103
  */
87
- onDestroy?: (key: string | number) => void;
104
+ onDestroy?: (key: string | number, timerGeneration?: number) => void;
88
105
  }
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import { NoticeProps } from './interface';
2
+ import type { NoticeProps, PlacementType } from './interface';
3
3
  import Notice from './notice';
4
4
 
5
5
  import './style/index.less';
@@ -10,14 +10,8 @@ const NoticeList = ({
10
10
  onDestroy,
11
11
  }: {
12
12
  items: NoticeProps[];
13
- placement?:
14
- | 'topLeft'
15
- | 'topRight'
16
- | 'bottomLeft'
17
- | 'bottomRight'
18
- | 'top'
19
- | 'bottom';
20
- onDestroy: (key: string | number) => void;
13
+ placement?: PlacementType;
14
+ onDestroy: (key: string | number, timerGeneration?: number) => void;
21
15
  }) => {
22
16
  if (items.length === 0) {
23
17
  return null;
@@ -1,7 +1,8 @@
1
1
  import React, { createContext, useContext } from 'react';
2
+ import type { NoticeApi } from './interface';
2
3
  import useNotice from './use-notice';
3
4
 
4
- const NoticeContext = createContext<any>({});
5
+ const NoticeContext = createContext<NoticeApi | undefined>(undefined);
5
6
 
6
7
  export function NoticeProvider({ children }: { children: React.ReactNode }) {
7
8
  const { noticeApi, noticeHolders } = useNotice();
@@ -14,7 +15,7 @@ export function NoticeProvider({ children }: { children: React.ReactNode }) {
14
15
  );
15
16
  }
16
17
 
17
- export function useGlobalNotice() {
18
+ export function useGlobalNotice(): NoticeApi {
18
19
  const context = useContext(NoticeContext);
19
20
 
20
21
  if (context === undefined) {
@@ -0,0 +1,450 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import {
4
+ act,
5
+ cleanup,
6
+ fireEvent,
7
+ render,
8
+ renderHook,
9
+ screen,
10
+ } from '@testing-library/react';
11
+ import React from 'react';
12
+ import { renderToString } from 'react-dom/server';
13
+ import { afterEach, describe, expect, it, vi } from 'vitest';
14
+ import type { NoticeApi, NoticeClose } from './interface';
15
+ import { useGlobalNotice } from './notice-provider';
16
+ import useNotice from './use-notice';
17
+ import noticeWrapper, { notice } from './wrapper';
18
+
19
+ let currentApi: NoticeApi;
20
+
21
+ function NoticeHarness() {
22
+ const { noticeApi, noticeHolders } = useNotice();
23
+ currentApi = noticeApi;
24
+ return <>{noticeHolders}</>;
25
+ }
26
+
27
+ afterEach(() => {
28
+ vi.unstubAllGlobals();
29
+ cleanup();
30
+ noticeWrapper.unmount();
31
+ vi.useRealTimers();
32
+ vi.restoreAllMocks();
33
+ });
34
+
35
+ describe('notice', () => {
36
+ it('renders safely without a document during SSR', () => {
37
+ vi.stubGlobal('document', undefined);
38
+
39
+ expect(() => renderToString(<NoticeHarness />)).not.toThrow();
40
+ expect(() => notice.info('server notice')).not.toThrow();
41
+ });
42
+
43
+ it('keeps duration zero notices open', () => {
44
+ vi.useFakeTimers();
45
+ const onClose = vi.fn();
46
+ render(<NoticeHarness />);
47
+
48
+ act(() => {
49
+ currentApi.info({ content: 'persistent', duration: 0, onClose });
50
+ vi.advanceTimersByTime(10000);
51
+ });
52
+
53
+ expect(screen.queryByText('persistent')).not.toBeNull();
54
+ expect(onClose).not.toHaveBeenCalled();
55
+ });
56
+
57
+ it('keeps the hook API reference stable across updates', () => {
58
+ render(<NoticeHarness />);
59
+ const initialApi = currentApi;
60
+
61
+ act(() => {
62
+ currentApi.info({ content: 'stable', duration: 0 });
63
+ });
64
+
65
+ expect(currentApi).toBe(initialApi);
66
+ });
67
+
68
+ it('keeps automatic keys independent from explicit numeric keys', () => {
69
+ render(<NoticeHarness />);
70
+
71
+ act(() => {
72
+ currentApi.info({
73
+ key: Number.POSITIVE_INFINITY,
74
+ content: 'explicit infinity',
75
+ duration: 0,
76
+ });
77
+ currentApi.info({ content: 'automatic one', duration: 0 });
78
+ currentApi.info({ content: 'automatic two', duration: 0 });
79
+ });
80
+
81
+ expect(screen.queryByText('explicit infinity')).not.toBeNull();
82
+ expect(screen.queryByText('automatic one')).not.toBeNull();
83
+ expect(screen.queryByText('automatic two')).not.toBeNull();
84
+ expect(document.querySelectorAll('.yee-notice')).toHaveLength(3);
85
+ });
86
+
87
+ it('does not restart existing timers when another notice is added', () => {
88
+ vi.useFakeTimers();
89
+ const firstOnClose = vi.fn();
90
+ render(<NoticeHarness />);
91
+
92
+ act(() => {
93
+ currentApi.info({ content: 'first', duration: 300, onClose: firstOnClose });
94
+ });
95
+ act(() => vi.advanceTimersByTime(200));
96
+ act(() => {
97
+ currentApi.info({ content: 'second', duration: 300 });
98
+ });
99
+ act(() => vi.advanceTimersByTime(100));
100
+
101
+ expect(firstOnClose).toHaveBeenCalledTimes(1);
102
+ expect(screen.queryByText('first')).toBeNull();
103
+ expect(screen.queryByText('second')).not.toBeNull();
104
+ });
105
+
106
+ it('restarts the timer when an existing key is updated', () => {
107
+ vi.useFakeTimers();
108
+ const onClose = vi.fn();
109
+ render(<NoticeHarness />);
110
+
111
+ act(() => {
112
+ currentApi.info({
113
+ key: 'timer-update',
114
+ content: 'working',
115
+ duration: 300,
116
+ onClose,
117
+ });
118
+ });
119
+ act(() => vi.advanceTimersByTime(200));
120
+ act(() => {
121
+ currentApi.success({
122
+ key: 'timer-update',
123
+ content: 'finished',
124
+ duration: 300,
125
+ onClose,
126
+ });
127
+ });
128
+ act(() => vi.advanceTimersByTime(100));
129
+
130
+ expect(screen.queryByText('finished')).not.toBeNull();
131
+ expect(onClose).not.toHaveBeenCalled();
132
+
133
+ act(() => vi.advanceTimersByTime(200));
134
+ expect(screen.queryByText('finished')).toBeNull();
135
+ expect(onClose).toHaveBeenCalledTimes(1);
136
+ });
137
+
138
+ it('moves an existing key to its new placement', () => {
139
+ render(<NoticeHarness />);
140
+
141
+ act(() => {
142
+ currentApi.info({
143
+ key: 'moving',
144
+ content: 'before move',
145
+ placement: 'topLeft',
146
+ duration: 0,
147
+ });
148
+ currentApi.success({
149
+ key: 'moving',
150
+ content: 'after move',
151
+ placement: 'bottomRight',
152
+ duration: 0,
153
+ });
154
+ });
155
+
156
+ expect(screen.queryByText('before move')).toBeNull();
157
+ const moved = screen.getByText('after move').closest('.yee-notice');
158
+ expect(document.querySelectorAll('.yee-notice')).toHaveLength(1);
159
+ expect(moved?.getAttribute('data-placement')).toBe('bottomRight');
160
+ });
161
+
162
+ it('keeps update close handles valid but invalidates them after key reuse', () => {
163
+ let staleClose!: NoticeClose;
164
+ render(<NoticeHarness />);
165
+
166
+ act(() => {
167
+ staleClose = currentApi.info({
168
+ key: 'reused',
169
+ content: 'first',
170
+ duration: 0,
171
+ });
172
+ currentApi.success({
173
+ key: 'reused',
174
+ content: 'updated',
175
+ duration: 0,
176
+ });
177
+ staleClose();
178
+ currentApi.info({
179
+ key: 'reused',
180
+ content: 'new generation',
181
+ duration: 0,
182
+ });
183
+ staleClose();
184
+ });
185
+
186
+ expect(screen.queryByText('updated')).toBeNull();
187
+ expect(screen.queryByText('new generation')).not.toBeNull();
188
+ });
189
+
190
+ it('calls onClose exactly once for clear and destroy', () => {
191
+ const firstOnClose = vi.fn();
192
+ const secondOnClose = vi.fn();
193
+ render(<NoticeHarness />);
194
+
195
+ act(() => {
196
+ currentApi.info({
197
+ key: 'first-close',
198
+ content: 'first',
199
+ duration: 0,
200
+ onClose: firstOnClose,
201
+ });
202
+ currentApi.info({
203
+ key: 'second-close',
204
+ content: 'second',
205
+ duration: 0,
206
+ onClose: secondOnClose,
207
+ });
208
+ currentApi.destroy('first-close');
209
+ currentApi.clear();
210
+ });
211
+
212
+ expect(firstOnClose).toHaveBeenCalledTimes(1);
213
+ expect(secondOnClose).toHaveBeenCalledTimes(1);
214
+ });
215
+
216
+ it('runs every hook clear callback even when one throws', () => {
217
+ const secondOnClose = vi.fn();
218
+ const reportError = vi.fn();
219
+ vi.stubGlobal('reportError', reportError);
220
+ render(<NoticeHarness />);
221
+
222
+ act(() => {
223
+ currentApi.info({
224
+ content: 'throwing hook callback',
225
+ duration: 0,
226
+ onClose: () => {
227
+ throw new Error('hook close failed');
228
+ },
229
+ });
230
+ currentApi.info({
231
+ content: 'later hook callback',
232
+ duration: 0,
233
+ onClose: secondOnClose,
234
+ });
235
+ });
236
+
237
+ act(() => currentApi.clear());
238
+ expect(secondOnClose).toHaveBeenCalledTimes(1);
239
+ expect(reportError).toHaveBeenCalledTimes(1);
240
+ expect(screen.queryByText('later hook callback')).toBeNull();
241
+ });
242
+
243
+ it('runs every global clear callback even when one throws', () => {
244
+ const secondOnClose = vi.fn();
245
+ const reportError = vi.fn();
246
+ vi.stubGlobal('reportError', reportError);
247
+
248
+ act(() => {
249
+ notice.info({
250
+ content: 'throwing global callback',
251
+ duration: 0,
252
+ onClose: () => {
253
+ throw new Error('global close failed');
254
+ },
255
+ });
256
+ notice.info({
257
+ content: 'later global callback',
258
+ duration: 0,
259
+ onClose: secondOnClose,
260
+ });
261
+ });
262
+
263
+ act(() => notice.clear());
264
+ expect(secondOnClose).toHaveBeenCalledTimes(1);
265
+ expect(reportError).toHaveBeenCalledTimes(1);
266
+ expect(screen.queryByText('later global callback')).toBeNull();
267
+ });
268
+
269
+ it('preserves hover pause while the notice is updated', () => {
270
+ vi.useFakeTimers();
271
+ const onClose = vi.fn();
272
+ render(<NoticeHarness />);
273
+
274
+ act(() => {
275
+ currentApi.info({
276
+ key: 'paused',
277
+ content: 'hover me',
278
+ duration: 300,
279
+ onClose,
280
+ });
281
+ });
282
+ act(() => vi.advanceTimersByTime(100));
283
+ fireEvent.mouseEnter(screen.getByText('hover me').closest('.yee-notice')!);
284
+
285
+ act(() => {
286
+ currentApi.info({
287
+ key: 'paused',
288
+ content: 'still paused',
289
+ duration: 300,
290
+ onClose,
291
+ });
292
+ vi.advanceTimersByTime(1000);
293
+ });
294
+
295
+ expect(screen.queryByText('still paused')).not.toBeNull();
296
+ expect(onClose).not.toHaveBeenCalled();
297
+
298
+ fireEvent.mouseLeave(screen.getByText('still paused').closest('.yee-notice')!);
299
+ act(() => vi.advanceTimersByTime(300));
300
+ expect(onClose).toHaveBeenCalledTimes(1);
301
+ });
302
+
303
+ it('resumes a paused countdown when pauseOnHover is disabled', () => {
304
+ vi.useFakeTimers();
305
+ const onClose = vi.fn();
306
+ render(<NoticeHarness />);
307
+
308
+ act(() => {
309
+ currentApi.info({
310
+ key: 'disable-pause',
311
+ content: 'pause enabled',
312
+ duration: 300,
313
+ pauseOnHover: true,
314
+ onClose,
315
+ });
316
+ });
317
+ fireEvent.mouseEnter(
318
+ screen.getByText('pause enabled').closest('.yee-notice')!,
319
+ );
320
+
321
+ act(() => {
322
+ currentApi.info({
323
+ key: 'disable-pause',
324
+ content: 'pause disabled',
325
+ duration: 300,
326
+ pauseOnHover: false,
327
+ onClose,
328
+ });
329
+ });
330
+ act(() => vi.advanceTimersByTime(300));
331
+
332
+ expect(screen.queryByText('pause disabled')).toBeNull();
333
+ expect(onClose).toHaveBeenCalledTimes(1);
334
+ });
335
+
336
+ it('pauses when pauseOnHover is enabled while already hovered', () => {
337
+ vi.useFakeTimers();
338
+ const onClose = vi.fn();
339
+ render(<NoticeHarness />);
340
+
341
+ act(() => {
342
+ currentApi.info({
343
+ key: 'enable-pause',
344
+ content: 'pause disabled',
345
+ duration: 300,
346
+ pauseOnHover: false,
347
+ onClose,
348
+ });
349
+ });
350
+ fireEvent.mouseEnter(
351
+ screen.getByText('pause disabled').closest('.yee-notice')!,
352
+ );
353
+
354
+ act(() => {
355
+ currentApi.info({
356
+ key: 'enable-pause',
357
+ content: 'pause enabled',
358
+ duration: 300,
359
+ pauseOnHover: true,
360
+ onClose,
361
+ });
362
+ vi.advanceTimersByTime(1000);
363
+ });
364
+
365
+ expect(screen.queryByText('pause enabled')).not.toBeNull();
366
+ expect(onClose).not.toHaveBeenCalled();
367
+
368
+ fireEvent.mouseLeave(screen.getByText('pause enabled').closest('.yee-notice')!);
369
+ act(() => vi.advanceTimersByTime(300));
370
+ expect(onClose).toHaveBeenCalledTimes(1);
371
+ });
372
+
373
+ it('preserves falsy global keys and dispatches onClose on destroy', () => {
374
+ const onClose = vi.fn();
375
+ const config = { key: 0, content: 'zero', duration: 0, onClose };
376
+
377
+ act(() => {
378
+ notice.open(config);
379
+ });
380
+
381
+ expect(config.key).toBe(0);
382
+ expect(screen.queryByText('zero')).not.toBeNull();
383
+
384
+ act(() => notice.destroy(0));
385
+ expect(screen.queryByText('zero')).toBeNull();
386
+ expect(onClose).toHaveBeenCalledTimes(1);
387
+ });
388
+
389
+ it('preserves generations when global clear callbacks reopen a key', () => {
390
+ let reopenedClose!: NoticeClose;
391
+
392
+ act(() => {
393
+ notice.info({
394
+ key: 'reopener',
395
+ content: 'reopener',
396
+ duration: 0,
397
+ onClose: () => {
398
+ reopenedClose = notice.info({
399
+ key: 'reopened-key',
400
+ content: 'reopened',
401
+ duration: 0,
402
+ });
403
+ },
404
+ });
405
+ notice.info({
406
+ key: 'reopened-key',
407
+ content: 'old generation',
408
+ duration: 0,
409
+ });
410
+ notice.clear();
411
+ });
412
+
413
+ expect(screen.queryByText('reopened')).not.toBeNull();
414
+ act(() => reopenedClose());
415
+ expect(screen.queryByText('reopened')).toBeNull();
416
+ });
417
+
418
+ it('throws a clear error outside NoticeProvider', () => {
419
+ expect(() => renderHook(() => useGlobalNotice())).toThrow(
420
+ 'useGlobalNotice must be used within NoticeProvider',
421
+ );
422
+ });
423
+
424
+ it('forwards data attributes and supports keyboard activation', () => {
425
+ const onClick = vi.fn();
426
+ render(<NoticeHarness />);
427
+
428
+ act(() => {
429
+ currentApi.error({
430
+ content: 'interactive',
431
+ duration: 0,
432
+ onClick,
433
+ 'data-track-id': 'interactive-notice',
434
+ });
435
+ });
436
+
437
+ const element = screen.getByText('interactive').closest('.yee-notice')!;
438
+ expect(element.getAttribute('data-track-id')).toBe('interactive-notice');
439
+ expect(element.getAttribute('role')).toBe('button');
440
+ expect(element.getAttribute('aria-live')).toBe('assertive');
441
+ expect(element.getAttribute('tabindex')).toBe('0');
442
+ expect(element.querySelector('.yee-notice-icon')?.getAttribute('aria-hidden')).toBe(
443
+ 'true',
444
+ );
445
+
446
+ fireEvent.keyDown(element, { key: 'Enter' });
447
+ fireEvent.keyDown(element, { key: ' ' });
448
+ expect(onClick).toHaveBeenCalledTimes(2);
449
+ });
450
+ });