@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.
- package/dist/cjs/Form/form-store.js +3 -0
- package/dist/cjs/Input/style/index.less +1 -0
- package/dist/cjs/Select/interface.d.ts +1 -1
- package/dist/cjs/Selector/style/index.less +1 -0
- package/dist/cjs/Table/icon/arrow-icon.js +1 -1
- package/dist/cjs/Table/theader/cell.js +6 -3
- package/dist/cjs/TextArea/style/index.less +1 -0
- package/dist/cjs/VerificationCode/style/index.less +1 -0
- package/dist/cjs/hooks/useCountdown.d.ts +2 -1
- package/dist/cjs/hooks/useCountdown.js +46 -23
- package/dist/cjs/locale/interface.d.ts +3 -0
- package/dist/cjs/locale/lang/en_US.js +4 -1
- package/dist/cjs/locale/lang/ja_JP.js +4 -1
- package/dist/cjs/locale/lang/zh_CN.js +4 -1
- package/dist/cjs/locale/lang/zh_TW.js +4 -1
- package/dist/cjs/message/demo/base.js +1 -1
- package/dist/cjs/message/index.d.ts +1 -1
- package/dist/cjs/message/interface.d.ts +16 -7
- package/dist/cjs/message/message-provider.d.ts +2 -1
- package/dist/cjs/message/message-provider.js +1 -1
- package/dist/cjs/message/message.js +14 -9
- package/dist/cjs/message/style/index.less +3 -1
- package/dist/cjs/message/use-message.d.ts +3 -12
- package/dist/cjs/message/use-message.js +103 -45
- package/dist/cjs/message/wrapper.d.ts +21 -27
- package/dist/cjs/message/wrapper.js +49 -19
- package/dist/cjs/notice/index.d.ts +1 -1
- package/dist/cjs/notice/interface.d.ts +17 -7
- package/dist/cjs/notice/notice-list.d.ts +3 -3
- package/dist/cjs/notice/notice-provider.d.ts +2 -1
- package/dist/cjs/notice/notice-provider.js +1 -1
- package/dist/cjs/notice/notice.js +63 -20
- package/dist/cjs/notice/style/index.less +13 -1
- package/dist/cjs/notice/use-notice.d.ts +2 -11
- package/dist/cjs/notice/use-notice.js +156 -71
- package/dist/cjs/notice/wrapper.d.ts +19 -15
- package/dist/cjs/notice/wrapper.js +122 -52
- package/dist/cjs/style/size.css +3 -3
- package/dist/cjs/style/themes.css +1 -1
- package/dist/cjs/style/variables.css +4 -4
- package/dist/cjs/version.d.ts +1 -1
- package/dist/cjs/version.js +1 -1
- package/dist/esm/Form/form-store.js +25 -17
- package/dist/esm/Input/style/index.less +1 -0
- package/dist/esm/Select/interface.d.ts +1 -1
- package/dist/esm/Selector/style/index.less +1 -0
- package/dist/esm/Table/icon/arrow-icon.js +1 -1
- package/dist/esm/Table/theader/cell.js +7 -3
- package/dist/esm/TextArea/style/index.less +1 -0
- package/dist/esm/VerificationCode/style/index.less +1 -0
- package/dist/esm/hooks/useCountdown.d.ts +2 -1
- package/dist/esm/hooks/useCountdown.js +44 -14
- package/dist/esm/locale/interface.d.ts +3 -0
- package/dist/esm/locale/lang/en_US.js +4 -1
- package/dist/esm/locale/lang/ja_JP.js +4 -1
- package/dist/esm/locale/lang/zh_CN.js +4 -1
- package/dist/esm/locale/lang/zh_TW.js +4 -1
- package/dist/esm/message/demo/base.js +4 -1
- package/dist/esm/message/index.d.ts +1 -1
- package/dist/esm/message/interface.d.ts +16 -7
- package/dist/esm/message/message-provider.d.ts +2 -1
- package/dist/esm/message/message-provider.js +1 -1
- package/dist/esm/message/message.js +22 -9
- package/dist/esm/message/style/index.less +3 -1
- package/dist/esm/message/use-message.d.ts +3 -12
- package/dist/esm/message/use-message.js +129 -79
- package/dist/esm/message/wrapper.d.ts +21 -27
- package/dist/esm/message/wrapper.js +60 -18
- package/dist/esm/notice/index.d.ts +1 -1
- package/dist/esm/notice/interface.d.ts +17 -7
- package/dist/esm/notice/notice-list.d.ts +3 -3
- package/dist/esm/notice/notice-provider.d.ts +2 -1
- package/dist/esm/notice/notice-provider.js +1 -1
- package/dist/esm/notice/notice.js +73 -21
- package/dist/esm/notice/style/index.less +13 -1
- package/dist/esm/notice/use-notice.d.ts +2 -11
- package/dist/esm/notice/use-notice.js +180 -100
- package/dist/esm/notice/wrapper.d.ts +19 -15
- package/dist/esm/notice/wrapper.js +144 -62
- package/dist/esm/style/size.css +3 -3
- package/dist/esm/style/themes.css +1 -1
- package/dist/esm/style/variables.css +4 -4
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/umd/yee-c.min.css +2 -2
- package/dist/umd/yee-c.min.js +1 -1
- package/package.json +1 -1
- package/src/Form/form-store.ts +5 -0
- package/src/Input/style/index.less +1 -0
- package/src/Select/interface.ts +1 -1
- package/src/Selector/style/index.less +1 -0
- package/src/Table/icon/arrow-icon.tsx +1 -1
- package/src/Table/theader/cell.tsx +6 -3
- package/src/TextArea/style/index.less +1 -0
- package/src/VerificationCode/style/index.less +1 -0
- package/src/hooks/useCountdown.ts +52 -21
- package/src/locale/interface.ts +3 -0
- package/src/locale/lang/en_US.ts +3 -0
- package/src/locale/lang/ja_JP.ts +3 -0
- package/src/locale/lang/zh_CN.ts +3 -0
- package/src/locale/lang/zh_TW.ts +3 -0
- package/src/message/demo/base.tsx +1 -1
- package/src/message/index.ts +1 -1
- package/src/message/interface.ts +22 -7
- package/src/message/message-provider.tsx +3 -2
- package/src/message/message.test.tsx +283 -0
- package/src/message/message.tsx +14 -9
- package/src/message/style/index.less +3 -1
- package/src/message/use-message.tsx +144 -67
- package/src/message/wrapper.tsx +62 -20
- package/src/notice/index.en-US.md +76 -75
- package/src/notice/index.ts +6 -1
- package/src/notice/index.zh-CN.md +3 -2
- package/src/notice/interface.ts +31 -14
- package/src/notice/notice-list.tsx +3 -9
- package/src/notice/notice-provider.tsx +3 -2
- package/src/notice/notice.test.tsx +450 -0
- package/src/notice/notice.tsx +69 -21
- package/src/notice/style/index.less +13 -1
- package/src/notice/use-notice.tsx +210 -94
- package/src/notice/wrapper.tsx +159 -71
- package/src/style/size.css +3 -3
- package/src/version.ts +1 -1
|
@@ -32,7 +32,7 @@ toc: 'content'
|
|
|
32
32
|
| --- | --- | --- | --- |
|
|
33
33
|
| title | 通知标题 | ReactNode | - |
|
|
34
34
|
| content | 通知内容 | ReactNode | - |
|
|
35
|
-
| duration | 自动关闭时间(
|
|
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 | 自动关闭时间(
|
|
67
|
+
| duration | 自动关闭时间(毫秒),设为 0 时不自动关闭 | number | `4500` |
|
|
67
68
|
| icon | 自定义图标 | ReactNode | - |
|
|
68
69
|
| key | 当前通知的唯一标识 | string \| number | - |
|
|
69
70
|
| placement | 显示位置 | `'topLeft' \| 'topRight' \| 'bottomLeft' \| 'bottomRight' \| 'top' \| 'bottom'` | `'topRight'` |
|
package/src/notice/interface.ts
CHANGED
|
@@ -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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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 (
|
|
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
|
-
//
|
|
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
|
-
|
|
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<
|
|
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
|
+
});
|