@rainbow-oh/yee-c 0.8.1 → 0.8.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 (225) hide show
  1. package/dist/cjs/Cascader/cascader.js +38 -30
  2. package/dist/cjs/Cascader/column.js +17 -9
  3. package/dist/cjs/Cascader/search-list.js +30 -22
  4. package/dist/cjs/Cascader/utils/index.js +1 -5
  5. package/dist/cjs/Checkbox/checkbox-group.js +23 -14
  6. package/dist/cjs/Dialog/dialog.js +1 -1
  7. package/dist/cjs/Dialog/interface.d.ts +1 -1
  8. package/dist/cjs/Dialog/style/index.less +2 -1
  9. package/dist/cjs/Drawer/drawer.js +9 -1
  10. package/dist/cjs/Form/Group.js +1 -1
  11. package/dist/cjs/Form/demo/tooltip.js +1 -3
  12. package/dist/cjs/Input/style/index.less +1 -0
  13. package/dist/cjs/List/hooks/useListKeyboard.js +3 -1
  14. package/dist/cjs/Menu/menu.js +18 -2
  15. package/dist/cjs/Pagination/items.js +54 -26
  16. package/dist/cjs/Pagination/pagination.js +25 -9
  17. package/dist/cjs/PickerPanel/panels/DateTimePanel/index.d.ts +1 -1
  18. package/dist/cjs/PickerPanel/panels/MonthPanel/MonthHeader.js +7 -1
  19. package/dist/cjs/Rate/star.js +27 -3
  20. package/dist/cjs/Segmented/demo/variant.js +15 -1
  21. package/dist/cjs/Segmented/index.d.ts +1 -1
  22. package/dist/cjs/Segmented/segmented.js +12 -15
  23. package/dist/cjs/Select/option.js +8 -1
  24. package/dist/cjs/Select/options.js +9 -1
  25. package/dist/cjs/Select/select.js +3 -1
  26. package/dist/cjs/Selector/style/index.less +1 -0
  27. package/dist/cjs/Steps/step.js +14 -2
  28. package/dist/cjs/Steps/steps.js +11 -1
  29. package/dist/cjs/Table/demo/components-override.js +31 -2
  30. package/dist/cjs/Table/demo/grouping.js +7 -1
  31. package/dist/cjs/Table/hooks/useColumns.d.ts +3 -3
  32. package/dist/cjs/Table/hooks/useColumns.js +11 -14
  33. package/dist/cjs/Table/icon/arrow-icon.js +1 -1
  34. package/dist/cjs/Table/index.d.ts +1 -1
  35. package/dist/cjs/Table/style/index.less +2 -4
  36. package/dist/cjs/Table/tbody/row.js +1 -4
  37. package/dist/cjs/Table/theader/cell.js +6 -3
  38. package/dist/cjs/Table/theader/index.js +12 -4
  39. package/dist/cjs/TextArea/style/index.less +1 -0
  40. package/dist/cjs/Tour/index.d.ts +1 -1
  41. package/dist/cjs/Tree/style/index.less +1 -5
  42. package/dist/cjs/Tree/tree.js +38 -35
  43. package/dist/cjs/VerificationCode/style/index.less +1 -0
  44. package/dist/cjs/hooks/useCountdown.d.ts +2 -1
  45. package/dist/cjs/hooks/useCountdown.js +46 -23
  46. package/dist/cjs/hooks/useEvent.js +1 -4
  47. package/dist/cjs/locale/interface.d.ts +3 -0
  48. package/dist/cjs/locale/lang/en_US.js +4 -1
  49. package/dist/cjs/locale/lang/ja_JP.js +4 -1
  50. package/dist/cjs/locale/lang/zh_CN.js +4 -1
  51. package/dist/cjs/locale/lang/zh_TW.js +4 -1
  52. package/dist/cjs/message/demo/base.js +1 -1
  53. package/dist/cjs/message/index.d.ts +1 -1
  54. package/dist/cjs/message/interface.d.ts +16 -7
  55. package/dist/cjs/message/message-provider.d.ts +2 -1
  56. package/dist/cjs/message/message-provider.js +1 -1
  57. package/dist/cjs/message/message.js +14 -9
  58. package/dist/cjs/message/style/index.less +3 -1
  59. package/dist/cjs/message/use-message.d.ts +3 -12
  60. package/dist/cjs/message/use-message.js +103 -45
  61. package/dist/cjs/message/wrapper.d.ts +21 -27
  62. package/dist/cjs/message/wrapper.js +49 -19
  63. package/dist/cjs/notice/index.d.ts +1 -1
  64. package/dist/cjs/notice/interface.d.ts +17 -7
  65. package/dist/cjs/notice/notice-list.d.ts +3 -3
  66. package/dist/cjs/notice/notice-provider.d.ts +2 -1
  67. package/dist/cjs/notice/notice-provider.js +1 -1
  68. package/dist/cjs/notice/notice.js +63 -20
  69. package/dist/cjs/notice/style/index.less +13 -1
  70. package/dist/cjs/notice/use-notice.d.ts +2 -11
  71. package/dist/cjs/notice/use-notice.js +156 -71
  72. package/dist/cjs/notice/wrapper.d.ts +19 -15
  73. package/dist/cjs/notice/wrapper.js +122 -52
  74. package/dist/cjs/style/size.css +5 -3
  75. package/dist/cjs/style/themes.css +1 -1
  76. package/dist/cjs/style/variables.css +6 -4
  77. package/dist/cjs/version.d.ts +1 -1
  78. package/dist/cjs/version.js +1 -1
  79. package/dist/esm/Dialog/dialog.js +2 -2
  80. package/dist/esm/Dialog/interface.d.ts +1 -1
  81. package/dist/esm/Dialog/style/index.less +2 -1
  82. package/dist/esm/Form/Group.js +1 -1
  83. package/dist/esm/Input/style/index.less +1 -0
  84. package/dist/esm/PickerPanel/panels/DateTimePanel/index.d.ts +1 -1
  85. package/dist/esm/Segmented/index.d.ts +1 -1
  86. package/dist/esm/Segmented/segmented.js +1 -1
  87. package/dist/esm/Selector/style/index.less +1 -0
  88. package/dist/esm/Table/demo/components-override.js +1 -0
  89. package/dist/esm/Table/hooks/useColumns.d.ts +3 -3
  90. package/dist/esm/Table/hooks/useColumns.js +13 -16
  91. package/dist/esm/Table/icon/arrow-icon.js +1 -1
  92. package/dist/esm/Table/index.d.ts +1 -1
  93. package/dist/esm/Table/style/index.less +2 -4
  94. package/dist/esm/Table/theader/cell.js +7 -3
  95. package/dist/esm/TextArea/style/index.less +1 -0
  96. package/dist/esm/Tour/index.d.ts +1 -1
  97. package/dist/esm/Tree/style/index.less +1 -5
  98. package/dist/esm/Tree/tree.js +1 -1
  99. package/dist/esm/VerificationCode/style/index.less +1 -0
  100. package/dist/esm/hooks/useCountdown.d.ts +2 -1
  101. package/dist/esm/hooks/useCountdown.js +44 -14
  102. package/dist/esm/locale/interface.d.ts +3 -0
  103. package/dist/esm/locale/lang/en_US.js +4 -1
  104. package/dist/esm/locale/lang/ja_JP.js +4 -1
  105. package/dist/esm/locale/lang/zh_CN.js +4 -1
  106. package/dist/esm/locale/lang/zh_TW.js +4 -1
  107. package/dist/esm/message/demo/base.js +4 -1
  108. package/dist/esm/message/index.d.ts +1 -1
  109. package/dist/esm/message/interface.d.ts +16 -7
  110. package/dist/esm/message/message-provider.d.ts +2 -1
  111. package/dist/esm/message/message-provider.js +1 -1
  112. package/dist/esm/message/message.js +22 -9
  113. package/dist/esm/message/style/index.less +3 -1
  114. package/dist/esm/message/use-message.d.ts +3 -12
  115. package/dist/esm/message/use-message.js +129 -79
  116. package/dist/esm/message/wrapper.d.ts +21 -27
  117. package/dist/esm/message/wrapper.js +60 -18
  118. package/dist/esm/notice/index.d.ts +1 -1
  119. package/dist/esm/notice/interface.d.ts +17 -7
  120. package/dist/esm/notice/notice-list.d.ts +3 -3
  121. package/dist/esm/notice/notice-provider.d.ts +2 -1
  122. package/dist/esm/notice/notice-provider.js +1 -1
  123. package/dist/esm/notice/notice.js +73 -21
  124. package/dist/esm/notice/style/index.less +13 -1
  125. package/dist/esm/notice/use-notice.d.ts +2 -11
  126. package/dist/esm/notice/use-notice.js +180 -100
  127. package/dist/esm/notice/wrapper.d.ts +19 -15
  128. package/dist/esm/notice/wrapper.js +144 -62
  129. package/dist/esm/style/size.css +5 -3
  130. package/dist/esm/style/themes.css +1 -1
  131. package/dist/esm/style/variables.css +6 -4
  132. package/dist/esm/version.d.ts +1 -1
  133. package/dist/esm/version.js +1 -1
  134. package/dist/umd/yee-c.min.css +2 -2
  135. package/dist/umd/yee-c.min.js +1 -1
  136. package/package.json +1 -1
  137. package/src/Anchor/anchor.tsx +2 -2
  138. package/src/Carousel/carousel.tsx +1 -2
  139. package/src/Cascader/cascader.tsx +5 -1
  140. package/src/Cascader/column.tsx +5 -1
  141. package/src/Cascader/search-list.tsx +10 -3
  142. package/src/Cascader/utils/index.ts +3 -6
  143. package/src/Checkbox/checkbox-group.tsx +6 -1
  144. package/src/Dialog/dialog.tsx +7 -3
  145. package/src/Dialog/interface.ts +1 -1
  146. package/src/Dialog/style/index.less +2 -1
  147. package/src/Drawer/drawer.tsx +7 -1
  148. package/src/Form/Group.tsx +2 -4
  149. package/src/Form/demo/tooltip.tsx +2 -11
  150. package/src/Form/utils/validate.ts +1 -4
  151. package/src/Input/style/index.less +1 -0
  152. package/src/List/hooks/useListKeyboard.ts +3 -3
  153. package/src/List/list.tsx +2 -1
  154. package/src/Menu/menu.tsx +14 -3
  155. package/src/Pagination/items.tsx +16 -4
  156. package/src/Pagination/pagination.tsx +18 -4
  157. package/src/PickerPanel/panels/DatePanel/DateBody.tsx +1 -1
  158. package/src/PickerPanel/panels/DateTimePanel/index.tsx +1 -1
  159. package/src/PickerPanel/panels/MonthPanel/MonthHeader.tsx +7 -2
  160. package/src/PickerPanel/panels/PanelBody.tsx +1 -1
  161. package/src/PickerPanel/panels/QuarterPanel/QuarterHeader.tsx +1 -1
  162. package/src/PickerPanel/panels/RangePanel/index.tsx +1 -1
  163. package/src/PickerPanel/panels/TimePanel/TimeBody.tsx +1 -1
  164. package/src/PickerPanel/panels/YearPanel/YearHeader.tsx +1 -1
  165. package/src/PickerPanel/utils/pickerUtils.ts +2 -1
  166. package/src/RangePicker/range-picker-mobile.tsx +1 -3
  167. package/src/Rate/star.tsx +16 -4
  168. package/src/Segmented/demo/variant.tsx +10 -2
  169. package/src/Segmented/index.ts +2 -2
  170. package/src/Segmented/segmented.tsx +4 -11
  171. package/src/Select/interface.ts +0 -1
  172. package/src/Select/option.tsx +8 -2
  173. package/src/Select/options.tsx +11 -4
  174. package/src/Select/select.tsx +4 -4
  175. package/src/Selector/style/index.less +1 -0
  176. package/src/Steps/step.tsx +11 -3
  177. package/src/Steps/steps.tsx +8 -1
  178. package/src/Table/demo/components-override.tsx +34 -7
  179. package/src/Table/demo/grouping.tsx +8 -4
  180. package/src/Table/hooks/useColumns.ts +14 -18
  181. package/src/Table/icon/arrow-icon.tsx +1 -1
  182. package/src/Table/index.ts +3 -3
  183. package/src/Table/style/index.less +2 -4
  184. package/src/Table/table.tsx +2 -4
  185. package/src/Table/tbody/row.tsx +1 -3
  186. package/src/Table/theader/cell.tsx +6 -3
  187. package/src/Table/theader/index.tsx +9 -10
  188. package/src/TextArea/style/index.less +1 -0
  189. package/src/Tour/demo/placement.tsx +2 -1
  190. package/src/Tour/index.ts +1 -1
  191. package/src/Tree/style/index.less +1 -5
  192. package/src/Tree/tree.tsx +16 -11
  193. package/src/Tree/utils/tree-tools.ts +4 -1
  194. package/src/VerificationCode/style/index.less +1 -0
  195. package/src/hooks/useCountdown.ts +52 -21
  196. package/src/hooks/useEvent.ts +1 -3
  197. package/src/locale/context.tsx +5 -1
  198. package/src/locale/interface.ts +6 -1
  199. package/src/locale/lang/en_US.ts +3 -0
  200. package/src/locale/lang/ja_JP.ts +3 -0
  201. package/src/locale/lang/zh_CN.ts +3 -0
  202. package/src/locale/lang/zh_TW.ts +3 -0
  203. package/src/message/demo/base.tsx +1 -1
  204. package/src/message/index.ts +1 -1
  205. package/src/message/interface.ts +22 -7
  206. package/src/message/message-provider.tsx +3 -2
  207. package/src/message/message.test.tsx +283 -0
  208. package/src/message/message.tsx +14 -9
  209. package/src/message/style/index.less +3 -1
  210. package/src/message/use-message.tsx +144 -67
  211. package/src/message/wrapper.tsx +62 -20
  212. package/src/notice/index.en-US.md +76 -75
  213. package/src/notice/index.ts +6 -1
  214. package/src/notice/index.zh-CN.md +3 -2
  215. package/src/notice/interface.ts +31 -14
  216. package/src/notice/notice-list.tsx +3 -9
  217. package/src/notice/notice-provider.tsx +3 -2
  218. package/src/notice/notice.test.tsx +450 -0
  219. package/src/notice/notice.tsx +69 -21
  220. package/src/notice/style/index.less +13 -1
  221. package/src/notice/use-notice.tsx +210 -94
  222. package/src/notice/wrapper.tsx +159 -71
  223. package/src/style/size.css +5 -3
  224. package/src/utils/debounce.ts +1 -3
  225. package/src/version.ts +1 -1
@@ -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
+ });
@@ -4,18 +4,21 @@ import React from 'react';
4
4
  import Progress from '../Progress';
5
5
  import useCountdown from '../hooks/useCountdown';
6
6
  import { NoticeProps } from './interface';
7
+ import Button from '../Button';
7
8
 
8
9
  const icons = {
9
- info: <Info size={20} strokeWidth={2} />,
10
- success: <CircleCheck size={20} strokeWidth={2} />,
11
- warning: <CircleAlert size={20} strokeWidth={2} />,
12
- error: <CircleX size={20} strokeWidth={2} />,
10
+ info: <Info size={22} fill='currentColor' stroke="#FFF" strokeWidth={2} />,
11
+ success: <CircleCheck size={22} fill='currentColor' stroke="#FFF" strokeWidth={2} />,
12
+ warning: <CircleAlert size={22} fill='currentColor' stroke="#FFF" strokeWidth={2} />,
13
+ error: <CircleX size={22} fill='currentColor' stroke="#FFF" strokeWidth={2} />,
13
14
  };
14
15
 
15
16
  const Notice: React.FC<NoticeProps & { id: string | number }> = (props) => {
16
17
  const {
17
18
  prefixCls = 'yee-notice',
18
19
  id: key,
20
+ timerGeneration,
21
+ placement,
19
22
  status,
20
23
  title,
21
24
  content,
@@ -33,18 +36,51 @@ const Notice: React.FC<NoticeProps & { id: string | number }> = (props) => {
33
36
  ...rest
34
37
  } = props;
35
38
 
36
- const { remaining, onPause, onResume } = useCountdown({
39
+ // The state owner dispatches onClose after removing the notice.
40
+ void onClose;
41
+
42
+ const isHoveredRef = React.useRef(false);
43
+ const { remaining, isPaused, onPause, onResume } = useCountdown({
37
44
  duration,
45
+ resetKey: timerGeneration,
38
46
  onComplete: () => {
39
- onDestroy?.(key);
40
- onClose?.();
47
+ onDestroy?.(key, timerGeneration);
41
48
  },
42
49
  });
43
50
 
44
- const handleClose = (e: React.MouseEvent) => {
45
- e.stopPropagation();
46
- onDestroy?.(key);
47
- onClose?.();
51
+ React.useEffect(() => {
52
+ if (pauseOnHover && isHoveredRef.current && !isPaused) {
53
+ onPause();
54
+ } else if ((!pauseOnHover || !isHoveredRef.current) && isPaused) {
55
+ onResume();
56
+ }
57
+ }, [isPaused, onPause, onResume, pauseOnHover]);
58
+
59
+ const handleMouseEnter = () => {
60
+ isHoveredRef.current = true;
61
+ if (pauseOnHover) {
62
+ onPause();
63
+ }
64
+ };
65
+
66
+ const handleMouseLeave = () => {
67
+ isHoveredRef.current = false;
68
+ if (isPaused) {
69
+ onResume();
70
+ }
71
+ };
72
+
73
+ const handleClose = (event: React.MouseEvent) => {
74
+ event.stopPropagation();
75
+ onDestroy?.(key, timerGeneration);
76
+ };
77
+
78
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
79
+ if (!onClick || (event.key !== 'Enter' && event.key !== ' ')) {
80
+ return;
81
+ }
82
+ event.preventDefault();
83
+ onClick();
48
84
  };
49
85
 
50
86
  const renderIcon = () => {
@@ -53,7 +89,7 @@ const Notice: React.FC<NoticeProps & { id: string | number }> = (props) => {
53
89
  }
54
90
 
55
91
  return (
56
- <span className={`${prefixCls}-icon`}>
92
+ <span aria-hidden="true" className={`${prefixCls}-icon`}>
57
93
  {icon ? icon : status ? icons[status] : null}
58
94
  </span>
59
95
  );
@@ -65,30 +101,42 @@ const Notice: React.FC<NoticeProps & { id: string | number }> = (props) => {
65
101
  }
66
102
 
67
103
  return (
68
- <button
104
+ <Button
105
+ variant='text'
106
+ size="small"
69
107
  className={`${prefixCls}-close`}
70
108
  onClick={handleClose}
71
- type="button"
72
- aria-label="Close"
73
- >
74
- <X size={14} strokeWidth={2} />
75
- </button>
109
+ aria-label="Close notice"
110
+ icon={<X aria-hidden="true" size={14} strokeWidth={2} />}
111
+ />
76
112
  );
77
113
  };
78
114
 
115
+ const isAssertive = status === 'error' || status === 'warning';
116
+
79
117
  return (
80
118
  <div
81
119
  {...rest}
120
+ aria-atomic="true"
121
+ aria-live={isAssertive ? 'assertive' : 'polite'}
122
+ role={onClick ? 'button' : isAssertive ? 'alert' : 'status'}
123
+ tabIndex={onClick ? 0 : undefined}
124
+ data-placement={placement}
82
125
  data-testid={dataTestId}
83
126
  className={clsx(
84
127
  prefixCls,
85
- { [`${prefixCls}-${status}`]: status },
128
+ {
129
+ [`${prefixCls}-${status}`]: status,
130
+ [`${prefixCls}-clickable`]: onClick,
131
+ [`${prefixCls}-closable`]: closable,
132
+ },
86
133
  className,
87
134
  )}
88
135
  style={style}
89
136
  onClick={onClick}
90
- onMouseEnter={pauseOnHover ? onPause : undefined}
91
- onMouseLeave={pauseOnHover ? onResume : undefined}
137
+ onKeyDown={handleKeyDown}
138
+ onMouseEnter={handleMouseEnter}
139
+ onMouseLeave={handleMouseLeave}
92
140
  >
93
141
  <div className={`${prefixCls}-content-wrapper`}>
94
142
  {renderIcon()}
@@ -12,7 +12,7 @@
12
12
  background-color: var(--yee-notice-bg-color);
13
13
  border-radius: var(--yee-notice-border-radius);
14
14
  box-shadow: var(--yee-notice-box-shadow);
15
- cursor: pointer;
15
+ cursor: default;
16
16
  transition: all 0.3s ease;
17
17
  border: var(--yee-notice-border-width) solid transparent;
18
18
  overflow: hidden;
@@ -21,6 +21,18 @@
21
21
  box-shadow: var(--yee-notice-box-shadow-hover);
22
22
  }
23
23
 
24
+ &-clickable {
25
+ cursor: pointer;
26
+ }
27
+
28
+ &-closable {
29
+ @{pls}-content-wrapper {
30
+ padding-right: calc(
31
+ var(--yee-notice-close-size) + var(--yee-notice-close-right)
32
+ );
33
+ }
34
+ }
35
+
24
36
  &-content-wrapper {
25
37
  display: flex;
26
38
  align-items: flex-start;