@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
package/package.json
CHANGED
package/src/Form/form-store.ts
CHANGED
|
@@ -242,6 +242,11 @@ export class FormStore {
|
|
|
242
242
|
return false;
|
|
243
243
|
});
|
|
244
244
|
}
|
|
245
|
+
// If no rules match the current trigger, skip validation entirely
|
|
246
|
+
// to avoid clearing existing errors from a previous validation pass.
|
|
247
|
+
if (trigger && mergedRules.length === 0) {
|
|
248
|
+
return [];
|
|
249
|
+
}
|
|
245
250
|
const value = this.getFieldValue(name);
|
|
246
251
|
for (const rule of mergedRules) {
|
|
247
252
|
const result = await validate(rule, value);
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
padding: var(--yee-input-padding, 2px 8px);
|
|
10
10
|
font-size: var(--yee-input-font-size);
|
|
11
11
|
line-height: var(--yee-input-line-height);
|
|
12
|
+
background-color: var(--yee-input-content-bg-color);
|
|
12
13
|
border: var(--yee-input-border-width) solid var(--yee-input-border-color);
|
|
13
14
|
border-radius: var(--yee-input-border-radius, 4px);
|
|
14
15
|
font-family: var(--yee-font-family);
|
package/src/Select/interface.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
box-sizing: border-box;
|
|
6
6
|
width: 100%;
|
|
7
7
|
padding: var(--yee-selector-padding);
|
|
8
|
+
background-color: var(--yee-input-content-bg-color);
|
|
8
9
|
border: var(--yee-selector-border-width) solid var(--yee-input-border-color);
|
|
9
10
|
border-radius: var(--yee-selector-border-radius);
|
|
10
11
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import clsx from 'clsx';
|
|
2
2
|
import React, { useContext } from 'react';
|
|
3
|
+
import { useLocale } from '../../locale';
|
|
3
4
|
import Tooltip from '../../Tooltip';
|
|
4
5
|
import ArrowIcon from '../icon/arrow-icon';
|
|
5
6
|
import { HeadCellProps } from '../interface';
|
|
@@ -35,6 +36,8 @@ const HeadCell: React.FC<HeadCellProps> = (props) => {
|
|
|
35
36
|
} = props;
|
|
36
37
|
|
|
37
38
|
const { prefixCls } = useContext(TableCtx);
|
|
39
|
+
const { locale } = useLocale();
|
|
40
|
+
const { table: tableLocale } = locale;
|
|
38
41
|
|
|
39
42
|
const showFilter = !!filter;
|
|
40
43
|
|
|
@@ -80,11 +83,11 @@ const HeadCell: React.FC<HeadCellProps> = (props) => {
|
|
|
80
83
|
if (showSorterTooltip && sorter) {
|
|
81
84
|
let title = '';
|
|
82
85
|
if (displaySortOrder === 0) {
|
|
83
|
-
title = 'Click to sort ascending';
|
|
86
|
+
title = tableLocale.sortAscending || 'Click to sort ascending';
|
|
84
87
|
} else if (displaySortOrder === 1) {
|
|
85
|
-
title = 'Click to sort descending';
|
|
88
|
+
title = tableLocale.sortDescending || 'Click to sort descending';
|
|
86
89
|
} else if (displaySortOrder === 2) {
|
|
87
|
-
title = 'Click to cancel sorting';
|
|
90
|
+
title = tableLocale.sortCancel || 'Click to cancel sorting';
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
return <Tooltip title={title}>{trigger}</Tooltip>;
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
padding: 4px 8px;
|
|
67
67
|
font-size: var(--yee-textarea-font-size);
|
|
68
68
|
line-height: var(--yee-textarea-line-height);
|
|
69
|
+
background-color: var(--yee-input-content-bg-color);
|
|
69
70
|
border-radius: var(--yee-textarea-border-radius, 4px);
|
|
70
71
|
font-family: var(--yee-font-family);
|
|
71
72
|
width: 100%;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
width: var(--yee-verification-code-item-width);
|
|
10
10
|
height: var(--yee-verification-code-item-height);
|
|
11
11
|
text-align: center;
|
|
12
|
+
background-color: var(--yee-input-content-bg-color);
|
|
12
13
|
border: var(--yee-verification-code-item-border-width) solid
|
|
13
14
|
var(--yee-input-border-color);
|
|
14
15
|
border-radius: var(--yee-verification-code-item-border-radius);
|
|
@@ -4,19 +4,33 @@ export type CountdownOptions = {
|
|
|
4
4
|
duration: number;
|
|
5
5
|
interval?: number;
|
|
6
6
|
onComplete?: () => void;
|
|
7
|
+
resetKey?: string | number;
|
|
7
8
|
};
|
|
8
9
|
|
|
9
10
|
export default function useCountdown({
|
|
10
11
|
duration,
|
|
11
12
|
interval = 100,
|
|
12
13
|
onComplete,
|
|
14
|
+
resetKey,
|
|
13
15
|
}: CountdownOptions) {
|
|
14
|
-
const
|
|
16
|
+
const initialRemaining = Math.max(0, duration);
|
|
17
|
+
const [remaining, setRemaining] = useState(initialRemaining);
|
|
15
18
|
const [isPaused, setIsPaused] = useState(false);
|
|
16
19
|
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
17
|
-
const remainingRef = useRef(
|
|
20
|
+
const remainingRef = useRef(initialRemaining);
|
|
21
|
+
const isPausedRef = useRef(false);
|
|
18
22
|
const onCompleteRef = useRef(onComplete);
|
|
23
|
+
const identityRef = useRef({ duration, resetKey });
|
|
24
|
+
const epochRef = useRef(0);
|
|
25
|
+
|
|
19
26
|
onCompleteRef.current = onComplete;
|
|
27
|
+
if (
|
|
28
|
+
identityRef.current.duration !== duration ||
|
|
29
|
+
identityRef.current.resetKey !== resetKey
|
|
30
|
+
) {
|
|
31
|
+
identityRef.current = { duration, resetKey };
|
|
32
|
+
epochRef.current += 1;
|
|
33
|
+
}
|
|
20
34
|
|
|
21
35
|
const clear = useCallback(() => {
|
|
22
36
|
if (timerRef.current !== null) {
|
|
@@ -25,41 +39,58 @@ export default function useCountdown({
|
|
|
25
39
|
}
|
|
26
40
|
}, []);
|
|
27
41
|
|
|
28
|
-
const tick = useCallback(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
42
|
+
const tick = useCallback(
|
|
43
|
+
(epoch: number) => {
|
|
44
|
+
if (epoch !== epochRef.current) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
32
47
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
48
|
+
const next = Math.max(0, remainingRef.current - interval);
|
|
49
|
+
remainingRef.current = next;
|
|
50
|
+
setRemaining(next);
|
|
51
|
+
|
|
52
|
+
if (next <= 0) {
|
|
53
|
+
onCompleteRef.current?.();
|
|
54
|
+
} else {
|
|
55
|
+
timerRef.current = setTimeout(() => tick(epoch), interval);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
[interval],
|
|
59
|
+
);
|
|
39
60
|
|
|
40
61
|
const start = useCallback(() => {
|
|
41
62
|
clear();
|
|
42
|
-
|
|
63
|
+
if (remainingRef.current > 0 && !isPausedRef.current) {
|
|
64
|
+
const epoch = epochRef.current;
|
|
65
|
+
timerRef.current = setTimeout(() => tick(epoch), interval);
|
|
66
|
+
}
|
|
43
67
|
}, [clear, interval, tick]);
|
|
44
68
|
|
|
45
69
|
useEffect(() => {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
70
|
+
clear();
|
|
71
|
+
const nextRemaining = Math.max(0, duration);
|
|
72
|
+
remainingRef.current = nextRemaining;
|
|
73
|
+
setRemaining(nextRemaining);
|
|
74
|
+
|
|
75
|
+
if (nextRemaining > 0 && !isPausedRef.current) {
|
|
76
|
+
start();
|
|
77
|
+
}
|
|
78
|
+
|
|
50
79
|
return clear;
|
|
51
|
-
|
|
52
|
-
}, [duration]);
|
|
80
|
+
}, [clear, duration, resetKey, start]);
|
|
53
81
|
|
|
54
82
|
const onPause = useCallback(() => {
|
|
83
|
+
isPausedRef.current = true;
|
|
55
84
|
clear();
|
|
56
85
|
setIsPaused(true);
|
|
57
86
|
}, [clear]);
|
|
58
87
|
|
|
59
88
|
const onResume = useCallback(() => {
|
|
60
|
-
|
|
89
|
+
isPausedRef.current = false;
|
|
61
90
|
setIsPaused(false);
|
|
62
|
-
|
|
91
|
+
if (remainingRef.current > 0) {
|
|
92
|
+
start();
|
|
93
|
+
}
|
|
63
94
|
}, [start]);
|
|
64
95
|
|
|
65
96
|
return { remaining, isPaused, onPause, onResume };
|
package/src/locale/interface.ts
CHANGED
|
@@ -76,6 +76,9 @@ export interface LocaleTable {
|
|
|
76
76
|
selectionAll?: string; // "Select all on current page"
|
|
77
77
|
selectionInvert?: string; // "Invert selection on current page"
|
|
78
78
|
selectionNone?: string; // "Clear selection on current page"
|
|
79
|
+
sortAscending?: string; // "Click to sort ascending"
|
|
80
|
+
sortDescending?: string; // "Click to sort descending"
|
|
81
|
+
sortCancel?: string; // "Click to cancel sorting"
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
// Pagination component
|
package/src/locale/lang/en_US.ts
CHANGED
|
@@ -102,6 +102,9 @@ const locale: Locale = {
|
|
|
102
102
|
selectionAll: 'Select All on Page',
|
|
103
103
|
selectionInvert: 'Invert Selection on Page',
|
|
104
104
|
selectionNone: 'Clear Selection on Page',
|
|
105
|
+
sortAscending: 'Click to sort ascending',
|
|
106
|
+
sortDescending: 'Click to sort descending',
|
|
107
|
+
sortCancel: 'Click to cancel sorting',
|
|
105
108
|
},
|
|
106
109
|
|
|
107
110
|
pagination: {
|
package/src/locale/lang/ja_JP.ts
CHANGED
|
@@ -102,6 +102,9 @@ const locale: Locale = {
|
|
|
102
102
|
selectionAll: 'ページ内すべて選択',
|
|
103
103
|
selectionInvert: 'ページ内選択を反転',
|
|
104
104
|
selectionNone: 'ページ内選択をクリア',
|
|
105
|
+
sortAscending: 'クリックで昇順に並べ替え',
|
|
106
|
+
sortDescending: 'クリックで降順に並べ替え',
|
|
107
|
+
sortCancel: 'クリックで並べ替えを解除',
|
|
105
108
|
},
|
|
106
109
|
|
|
107
110
|
pagination: {
|
package/src/locale/lang/zh_CN.ts
CHANGED
package/src/locale/lang/zh_TW.ts
CHANGED
|
@@ -6,7 +6,7 @@ const BaseDemo = () => {
|
|
|
6
6
|
const showMessage = (type: string) => {
|
|
7
7
|
switch (type) {
|
|
8
8
|
case 'info':
|
|
9
|
-
messageApi.open('This is an info message');
|
|
9
|
+
messageApi.open({content: 'This is an info message', duration: 0});
|
|
10
10
|
break;
|
|
11
11
|
case 'success':
|
|
12
12
|
message.success('This is a success message');
|
package/src/message/index.ts
CHANGED
package/src/message/interface.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export type MessageConfig = {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
[dataAttribute: `data-${string}`]:
|
|
3
|
+
| string
|
|
4
|
+
| number
|
|
5
|
+
| boolean
|
|
6
|
+
| undefined;
|
|
6
7
|
/**
|
|
7
8
|
* Custom prefix class name
|
|
8
9
|
*/
|
|
@@ -20,8 +21,8 @@ export type MessageConfig = {
|
|
|
20
21
|
*/
|
|
21
22
|
content: React.ReactNode;
|
|
22
23
|
/**
|
|
23
|
-
* Auto close time (
|
|
24
|
-
* @default
|
|
24
|
+
* Auto close time (seconds), 0 means no auto close
|
|
25
|
+
* @default 3
|
|
25
26
|
*/
|
|
26
27
|
duration?: number;
|
|
27
28
|
/**
|
|
@@ -42,6 +43,19 @@ export type MessageConfig = {
|
|
|
42
43
|
onClose?: () => void;
|
|
43
44
|
};
|
|
44
45
|
|
|
46
|
+
export type MessageClose = () => void;
|
|
47
|
+
|
|
48
|
+
export interface MessageApi {
|
|
49
|
+
open: (config: string | MessageConfig) => MessageClose;
|
|
50
|
+
info: (config: string | MessageConfig) => MessageClose;
|
|
51
|
+
success: (config: string | MessageConfig) => MessageClose;
|
|
52
|
+
warning: (config: string | MessageConfig) => MessageClose;
|
|
53
|
+
error: (config: string | MessageConfig) => MessageClose;
|
|
54
|
+
loading: (config: string | MessageConfig) => MessageClose;
|
|
55
|
+
destroy: (key: string | number) => void;
|
|
56
|
+
clear: () => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
45
59
|
// With status
|
|
46
60
|
export type WrapperedMessageConfig = MessageConfig & {
|
|
47
61
|
status?: 'info' | 'success' | 'warning' | 'error' | 'loading';
|
|
@@ -50,11 +64,12 @@ export type WrapperedMessageConfig = MessageConfig & {
|
|
|
50
64
|
// With key
|
|
51
65
|
export type MessageType = WrapperedMessageConfig & {
|
|
52
66
|
key: string | number;
|
|
67
|
+
timerGeneration: number;
|
|
53
68
|
};
|
|
54
69
|
|
|
55
70
|
export interface MessageProps extends MessageType {
|
|
56
71
|
/**
|
|
57
72
|
* Destroy component callback
|
|
58
73
|
*/
|
|
59
|
-
onDestroy?: (key: string | number) => void;
|
|
74
|
+
onDestroy?: (key: string | number, timerGeneration?: number) => void;
|
|
60
75
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import React, { createContext, useContext } from 'react';
|
|
2
|
+
import type { MessageApi } from './interface';
|
|
2
3
|
import useMessage from './use-message';
|
|
3
4
|
|
|
4
|
-
const MessageContext = createContext<
|
|
5
|
+
const MessageContext = createContext<MessageApi | undefined>(undefined);
|
|
5
6
|
|
|
6
7
|
export function MessageProvider({ children }: { children: React.ReactNode }) {
|
|
7
8
|
const { messageApi, messageHolder } = useMessage();
|
|
@@ -14,7 +15,7 @@ export function MessageProvider({ children }: { children: React.ReactNode }) {
|
|
|
14
15
|
);
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
export function useGlobalMessage() {
|
|
18
|
+
export function useGlobalMessage(): MessageApi {
|
|
18
19
|
const context = useContext(MessageContext);
|
|
19
20
|
|
|
20
21
|
if (context === undefined) {
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { act, cleanup, render, renderHook, screen } from '@testing-library/react';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { renderToString } from 'react-dom/server';
|
|
6
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
7
|
+
import type { MessageApi, MessageClose } from './interface';
|
|
8
|
+
import { useGlobalMessage } from './message-provider';
|
|
9
|
+
import useMessage from './use-message';
|
|
10
|
+
import messageWrapper, { message } from './wrapper';
|
|
11
|
+
|
|
12
|
+
let currentApi: MessageApi;
|
|
13
|
+
|
|
14
|
+
function MessageHarness() {
|
|
15
|
+
const { messageApi, messageHolder } = useMessage();
|
|
16
|
+
currentApi = messageApi;
|
|
17
|
+
return <>{messageHolder}</>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
cleanup();
|
|
22
|
+
messageWrapper.unmount();
|
|
23
|
+
vi.useRealTimers();
|
|
24
|
+
vi.unstubAllGlobals();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('message', () => {
|
|
28
|
+
it('renders safely without a document during SSR', () => {
|
|
29
|
+
vi.stubGlobal('document', undefined);
|
|
30
|
+
|
|
31
|
+
expect(() => renderToString(<MessageHarness />)).not.toThrow();
|
|
32
|
+
expect(() => message.info('server message')).not.toThrow();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('keeps the hook API reference stable across message updates', () => {
|
|
36
|
+
render(<MessageHarness />);
|
|
37
|
+
const initialApi = currentApi;
|
|
38
|
+
|
|
39
|
+
act(() => {
|
|
40
|
+
currentApi.info({ content: 'saved', duration: 0 });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
expect(currentApi).toBe(initialApi);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('does not restart existing timers when another message is added', () => {
|
|
47
|
+
vi.useFakeTimers();
|
|
48
|
+
const firstOnClose = vi.fn();
|
|
49
|
+
render(<MessageHarness />);
|
|
50
|
+
|
|
51
|
+
act(() => {
|
|
52
|
+
currentApi.info({
|
|
53
|
+
content: 'first',
|
|
54
|
+
duration: 3,
|
|
55
|
+
onClose: firstOnClose,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
act(() => {
|
|
59
|
+
vi.advanceTimersByTime(2000);
|
|
60
|
+
});
|
|
61
|
+
act(() => {
|
|
62
|
+
currentApi.info({ content: 'second', duration: 3 });
|
|
63
|
+
});
|
|
64
|
+
act(() => {
|
|
65
|
+
vi.advanceTimersByTime(1000);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(firstOnClose).toHaveBeenCalledTimes(1);
|
|
69
|
+
expect(screen.queryByText('first')).toBeNull();
|
|
70
|
+
expect(screen.queryByText('second')).not.toBeNull();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('does not let an old timer close a reused key in the same batch', () => {
|
|
74
|
+
vi.useFakeTimers();
|
|
75
|
+
const secondOnClose = vi.fn();
|
|
76
|
+
render(<MessageHarness />);
|
|
77
|
+
|
|
78
|
+
act(() => {
|
|
79
|
+
currentApi.info({ key: 'timer', content: 'first', duration: 3 });
|
|
80
|
+
});
|
|
81
|
+
act(() => {
|
|
82
|
+
vi.advanceTimersByTime(2000);
|
|
83
|
+
});
|
|
84
|
+
act(() => {
|
|
85
|
+
currentApi.destroy('timer');
|
|
86
|
+
currentApi.info({
|
|
87
|
+
key: 'timer',
|
|
88
|
+
content: 'second',
|
|
89
|
+
duration: 3,
|
|
90
|
+
onClose: secondOnClose,
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
act(() => {
|
|
94
|
+
vi.advanceTimersByTime(1000);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
expect(screen.queryByText('second')).not.toBeNull();
|
|
98
|
+
expect(secondOnClose).not.toHaveBeenCalled();
|
|
99
|
+
|
|
100
|
+
act(() => {
|
|
101
|
+
vi.advanceTimersByTime(2000);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(screen.queryByText('second')).toBeNull();
|
|
105
|
+
expect(secondOnClose).toHaveBeenCalledTimes(1);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('restarts the timer when an existing key is updated', () => {
|
|
109
|
+
vi.useFakeTimers();
|
|
110
|
+
const onClose = vi.fn();
|
|
111
|
+
render(<MessageHarness />);
|
|
112
|
+
|
|
113
|
+
act(() => {
|
|
114
|
+
currentApi.info({
|
|
115
|
+
key: 'updating-timer',
|
|
116
|
+
content: 'working',
|
|
117
|
+
duration: 3,
|
|
118
|
+
onClose,
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
act(() => {
|
|
122
|
+
vi.advanceTimersByTime(2000);
|
|
123
|
+
});
|
|
124
|
+
act(() => {
|
|
125
|
+
currentApi.info({
|
|
126
|
+
key: 'updating-timer',
|
|
127
|
+
content: 'finished',
|
|
128
|
+
duration: 3,
|
|
129
|
+
onClose,
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
act(() => {
|
|
133
|
+
vi.advanceTimersByTime(1000);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(screen.queryByText('finished')).not.toBeNull();
|
|
137
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
138
|
+
|
|
139
|
+
act(() => {
|
|
140
|
+
vi.advanceTimersByTime(2000);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(screen.queryByText('finished')).toBeNull();
|
|
144
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('updates duplicate keys and closes the current message with its handle', () => {
|
|
148
|
+
const firstOnClose = vi.fn();
|
|
149
|
+
const updatedOnClose = vi.fn();
|
|
150
|
+
let close!: MessageClose;
|
|
151
|
+
render(<MessageHarness />);
|
|
152
|
+
|
|
153
|
+
act(() => {
|
|
154
|
+
close = currentApi.info({
|
|
155
|
+
key: 'job',
|
|
156
|
+
content: 'working',
|
|
157
|
+
duration: 0,
|
|
158
|
+
onClose: firstOnClose,
|
|
159
|
+
});
|
|
160
|
+
currentApi.success({
|
|
161
|
+
key: 'job',
|
|
162
|
+
content: 'finished',
|
|
163
|
+
duration: 0,
|
|
164
|
+
onClose: updatedOnClose,
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(screen.queryByText('working')).toBeNull();
|
|
169
|
+
expect(screen.queryByText('finished')).not.toBeNull();
|
|
170
|
+
expect(document.querySelectorAll('.yee-message')).toHaveLength(1);
|
|
171
|
+
|
|
172
|
+
act(() => close());
|
|
173
|
+
|
|
174
|
+
expect(screen.queryByText('finished')).toBeNull();
|
|
175
|
+
expect(firstOnClose).not.toHaveBeenCalled();
|
|
176
|
+
expect(updatedOnClose).toHaveBeenCalledTimes(1);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('does not let stale hook close handles close a reused key', () => {
|
|
180
|
+
let staleClose!: MessageClose;
|
|
181
|
+
render(<MessageHarness />);
|
|
182
|
+
|
|
183
|
+
act(() => {
|
|
184
|
+
staleClose = currentApi.info({
|
|
185
|
+
key: 'reused',
|
|
186
|
+
content: 'first generation',
|
|
187
|
+
duration: 0,
|
|
188
|
+
});
|
|
189
|
+
staleClose();
|
|
190
|
+
currentApi.info({
|
|
191
|
+
key: 'reused',
|
|
192
|
+
content: 'second generation',
|
|
193
|
+
duration: 0,
|
|
194
|
+
});
|
|
195
|
+
staleClose();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
expect(screen.queryByText('second generation')).not.toBeNull();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('does not let stale global close handles close a reused key', () => {
|
|
202
|
+
let staleClose!: MessageClose;
|
|
203
|
+
|
|
204
|
+
act(() => {
|
|
205
|
+
staleClose = message.info({
|
|
206
|
+
key: 'global-reused',
|
|
207
|
+
content: 'first generation',
|
|
208
|
+
duration: 0,
|
|
209
|
+
});
|
|
210
|
+
staleClose();
|
|
211
|
+
message.info({
|
|
212
|
+
key: 'global-reused',
|
|
213
|
+
content: 'second generation',
|
|
214
|
+
duration: 0,
|
|
215
|
+
});
|
|
216
|
+
staleClose();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
expect(screen.queryByText('second generation')).not.toBeNull();
|
|
220
|
+
act(() => message.destroy('global-reused'));
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('calls onClose when clearing hook messages', () => {
|
|
224
|
+
const firstOnClose = vi.fn();
|
|
225
|
+
const secondOnClose = vi.fn();
|
|
226
|
+
render(<MessageHarness />);
|
|
227
|
+
|
|
228
|
+
act(() => {
|
|
229
|
+
currentApi.info({ content: 'first', duration: 0, onClose: firstOnClose });
|
|
230
|
+
currentApi.info({ content: 'second', duration: 0, onClose: secondOnClose });
|
|
231
|
+
currentApi.clear();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
expect(firstOnClose).toHaveBeenCalledTimes(1);
|
|
235
|
+
expect(secondOnClose).toHaveBeenCalledTimes(1);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('preserves a falsy global key without mutating the config', () => {
|
|
239
|
+
const onClose = vi.fn();
|
|
240
|
+
const config = { key: 0, content: 'zero', duration: 0, onClose };
|
|
241
|
+
|
|
242
|
+
act(() => {
|
|
243
|
+
message.open(config);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
expect(config.key).toBe(0);
|
|
247
|
+
expect(screen.queryByText('zero')).not.toBeNull();
|
|
248
|
+
|
|
249
|
+
act(() => {
|
|
250
|
+
message.destroy(0);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
expect(screen.queryByText('zero')).toBeNull();
|
|
254
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('throws a clear error outside MessageProvider', () => {
|
|
258
|
+
expect(() => renderHook(() => useGlobalMessage())).toThrow(
|
|
259
|
+
'useGlobalMessage must be used within MessageProvider',
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('forwards data attributes and exposes live-region semantics', () => {
|
|
264
|
+
render(<MessageHarness />);
|
|
265
|
+
|
|
266
|
+
act(() => {
|
|
267
|
+
currentApi.error({
|
|
268
|
+
content: <div>failed</div>,
|
|
269
|
+
duration: 0,
|
|
270
|
+
'data-track-id': 'failure-message',
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const element = screen.getByText('failed').closest('.yee-message');
|
|
275
|
+
expect(element?.getAttribute('data-track-id')).toBe('failure-message');
|
|
276
|
+
expect(element?.getAttribute('role')).toBe('alert');
|
|
277
|
+
expect(element?.getAttribute('aria-live')).toBe('assertive');
|
|
278
|
+
expect(element?.querySelector('.yee-message-content')?.tagName).toBe('DIV');
|
|
279
|
+
expect(element?.querySelector('.yee-message-icon')?.getAttribute('aria-hidden')).toBe(
|
|
280
|
+
'true',
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
});
|