@nuskin/foundation-ui-components 1.23.0 → 1.24.1
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/package.json +2 -1
- package/src/common/autocomplete/NsAutocomplete.tsx +2 -0
- package/src/common/autocomplete/types.d.ts +1 -0
- package/src/common/button/helper.ts +50 -48
- package/src/common/index.ts +1 -0
- package/src/common/toast/Fade.tsx +101 -0
- package/src/common/toast/ToastProvider.tsx +76 -0
- package/src/common/toast/index.ts +3 -0
- package/src/common/toast/shared.ts +124 -0
- package/src/common/toast/specs/Toast.spec.tsx +130 -0
- package/src/common/toast/stories/Toast.stories.tsx +81 -0
- package/src/common/toast/useToast.tsx +64 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nuskin/foundation-ui-components",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.24.1",
|
|
4
4
|
"description": "It is a foundation library written in react, that provides common components to all UI pages",
|
|
5
5
|
"main": "src/common/index.ts",
|
|
6
6
|
"repository": {
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"cliui": "^8.0.1",
|
|
53
53
|
"framer-motion": "^10.16.4",
|
|
54
54
|
"next": "13.5.6",
|
|
55
|
+
"notistack": "^3.0.1",
|
|
55
56
|
"react": "18.2.0",
|
|
56
57
|
"react-dom": "^18.2.0",
|
|
57
58
|
"react-material-ui-carousel": "^3.4.2",
|
|
@@ -34,6 +34,7 @@ const NsAutocomplete = ({
|
|
|
34
34
|
noOptionsText,
|
|
35
35
|
onInputChange,
|
|
36
36
|
onChange,
|
|
37
|
+
onFocus,
|
|
37
38
|
clear,
|
|
38
39
|
seeMore,
|
|
39
40
|
placeholder,
|
|
@@ -124,6 +125,7 @@ const NsAutocomplete = ({
|
|
|
124
125
|
onChange={onChange}
|
|
125
126
|
onInputChange={onInputChange}
|
|
126
127
|
value={value}
|
|
128
|
+
onFocus={onFocus ?? noop}
|
|
127
129
|
inputValue={inputValue}
|
|
128
130
|
filterOptions={
|
|
129
131
|
isFilterEnabled
|
|
@@ -12,20 +12,20 @@ export function generateContainedStyle({
|
|
|
12
12
|
hovered,
|
|
13
13
|
pressed,
|
|
14
14
|
focused,
|
|
15
|
-
}: ContainedParams): string {
|
|
16
|
-
return
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
15
|
+
}: ContainedParams): Record<string, any> {
|
|
16
|
+
return {
|
|
17
|
+
backgroundColor,
|
|
18
|
+
color: `${textColor} !important`,
|
|
19
|
+
'&:hover': {
|
|
20
|
+
backgroundColor: `${hovered}`
|
|
21
|
+
},
|
|
22
|
+
'&:active': {
|
|
23
|
+
backgroundColor: `${pressed}`
|
|
24
|
+
},
|
|
25
|
+
'&:focus': {
|
|
26
|
+
backgroundColor: `${focused}`
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
29
|
}
|
|
30
30
|
type OutlinedParams = {
|
|
31
31
|
borderColor: string;
|
|
@@ -34,18 +34,18 @@ type OutlinedParams = {
|
|
|
34
34
|
activeColor: string;
|
|
35
35
|
};
|
|
36
36
|
export function generateOutlinedStyle({ borderColor, hovered, pressed, activeColor }: OutlinedParams) {
|
|
37
|
-
return
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
37
|
+
return {
|
|
38
|
+
borderColor: `${borderColor}`,
|
|
39
|
+
color: `${borderColor}`,
|
|
40
|
+
'&:hover': {
|
|
41
|
+
borderColor: `${hovered}`,
|
|
42
|
+
color: `${borderColor}`
|
|
43
|
+
},
|
|
44
|
+
'&:active': {
|
|
45
|
+
borderColor: `${pressed}`,
|
|
46
|
+
color: `${activeColor}`
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
type TextParams = {
|
|
@@ -54,29 +54,31 @@ type TextParams = {
|
|
|
54
54
|
pressed: string;
|
|
55
55
|
};
|
|
56
56
|
|
|
57
|
-
export function generateTextStyles({ underlineColor, hovered, pressed }: TextParams):
|
|
58
|
-
return
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
57
|
+
export function generateTextStyles({ underlineColor, hovered, pressed }: TextParams): Record<string, any> {
|
|
58
|
+
return {
|
|
59
|
+
textDecoration: 'underline 1px !important',
|
|
60
|
+
textUnderlineOffset: '4px',
|
|
61
|
+
minWidth: 'auto',
|
|
62
|
+
backgroundColor: 'transparent !important',
|
|
63
|
+
textDecorationColor: `${underlineColor}`,
|
|
64
|
+
color: `${underlineColor}`,
|
|
65
|
+
'&:hover': {
|
|
66
|
+
color: `${hovered}`,
|
|
67
|
+
textDecorationColor: `${hovered}`,
|
|
68
|
+
backgroundColor: 'transparent !important',
|
|
69
|
+
border: 0,
|
|
70
|
+
},
|
|
71
|
+
'&:active, &:focus': {
|
|
72
|
+
color: `${underlineColor}`,
|
|
73
|
+
textDecorationColor: `${pressed}`,
|
|
74
|
+
border: 'none !important',
|
|
75
|
+
}
|
|
76
|
+
};
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
export function generateIconOnlyStyles() {
|
|
78
|
-
return
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
return {
|
|
81
|
+
padding: '10px',
|
|
82
|
+
minWidth: 'unset',
|
|
83
|
+
};
|
|
82
84
|
}
|
package/src/common/index.ts
CHANGED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credit to MUI team @ https://mui.com
|
|
3
|
+
*/
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import { TransitionProps, TransitionStatus, Transition } from 'notistack';
|
|
6
|
+
import {
|
|
7
|
+
useForkRef,
|
|
8
|
+
reflow,
|
|
9
|
+
getTransitionProps,
|
|
10
|
+
createTransition,
|
|
11
|
+
} from './shared';
|
|
12
|
+
|
|
13
|
+
const styles: Partial<Record<TransitionStatus, React.CSSProperties>> = {
|
|
14
|
+
entering: {
|
|
15
|
+
opacity: 1,
|
|
16
|
+
},
|
|
17
|
+
entered: {
|
|
18
|
+
opacity: 1,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const Fade = React.forwardRef<HTMLDivElement, TransitionProps>((props, ref) => {
|
|
23
|
+
const {
|
|
24
|
+
children,
|
|
25
|
+
in: inProp,
|
|
26
|
+
timeout = 0,
|
|
27
|
+
style,
|
|
28
|
+
onEnter,
|
|
29
|
+
onEntered,
|
|
30
|
+
onExit,
|
|
31
|
+
onExited,
|
|
32
|
+
direction, // Take this out since this is a Slide-only prop
|
|
33
|
+
...other
|
|
34
|
+
} = props;
|
|
35
|
+
|
|
36
|
+
const nodeRef = React.useRef<HTMLDivElement>(null);
|
|
37
|
+
const handleRefIntermediary = useForkRef((children as any).ref, ref);
|
|
38
|
+
const handleRef = useForkRef(nodeRef, handleRefIntermediary);
|
|
39
|
+
|
|
40
|
+
const handleEnter: TransitionProps['onEnter'] = (node, isAppearing) => {
|
|
41
|
+
reflow(node);
|
|
42
|
+
|
|
43
|
+
const transitionProps = getTransitionProps({
|
|
44
|
+
style,
|
|
45
|
+
timeout,
|
|
46
|
+
mode: 'enter',
|
|
47
|
+
});
|
|
48
|
+
node.style.webkitTransition = createTransition('opacity', transitionProps);
|
|
49
|
+
node.style.transition = createTransition('opacity', transitionProps);
|
|
50
|
+
|
|
51
|
+
if (onEnter) {
|
|
52
|
+
onEnter(node, isAppearing);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const handleExit: TransitionProps['onExit'] = (node) => {
|
|
57
|
+
const transitionProps = getTransitionProps({
|
|
58
|
+
style,
|
|
59
|
+
timeout,
|
|
60
|
+
mode: 'exit',
|
|
61
|
+
});
|
|
62
|
+
node.style.webkitTransition = createTransition('opacity', transitionProps);
|
|
63
|
+
node.style.transition = createTransition('opacity', transitionProps);
|
|
64
|
+
|
|
65
|
+
if (onExit) {
|
|
66
|
+
onExit(node);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<Transition
|
|
72
|
+
appear
|
|
73
|
+
in={inProp}
|
|
74
|
+
nodeRef={nodeRef}
|
|
75
|
+
onEnter={handleEnter}
|
|
76
|
+
onEntered={onEntered}
|
|
77
|
+
onExit={handleExit}
|
|
78
|
+
onExited={onExited}
|
|
79
|
+
timeout={timeout}
|
|
80
|
+
{...other}
|
|
81
|
+
>
|
|
82
|
+
{(status, childProps) =>
|
|
83
|
+
React.cloneElement(children as any, {
|
|
84
|
+
style: {
|
|
85
|
+
opacity: 0,
|
|
86
|
+
visibility: status === 'exited' && !inProp ? 'hidden' : undefined,
|
|
87
|
+
...styles[status],
|
|
88
|
+
...style,
|
|
89
|
+
...(children as any).props.style,
|
|
90
|
+
},
|
|
91
|
+
ref: handleRef,
|
|
92
|
+
...childProps,
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
</Transition>
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
Fade.displayName = 'Fade';
|
|
100
|
+
|
|
101
|
+
export default Fade;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { PropsWithChildren } from 'react';
|
|
2
|
+
import { SnackbarProvider, SnackbarProviderProps, MaterialDesignContent } from 'notistack';
|
|
3
|
+
|
|
4
|
+
import { styled, ColorUtils, ScreenSize } from '@nuskin/foundation-theme';
|
|
5
|
+
|
|
6
|
+
const { getGenomeColor } = ColorUtils;
|
|
7
|
+
|
|
8
|
+
const ToastNotification = styled(MaterialDesignContent)`
|
|
9
|
+
width: 610px;
|
|
10
|
+
height: 46px;
|
|
11
|
+
box-shadow: none;
|
|
12
|
+
padding: 0 20px;
|
|
13
|
+
|
|
14
|
+
@media (max-width: ${ScreenSize.md}px) {
|
|
15
|
+
width: 343px;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
#notistack-snackbar svg {
|
|
19
|
+
font-size: 24px !important;
|
|
20
|
+
}
|
|
21
|
+
&.notistack-MuiContent-success {
|
|
22
|
+
background-color: ${({ theme }) => getGenomeColor(theme, 'Success-10')};
|
|
23
|
+
border: 1px solid ${({ theme }) => getGenomeColor(theme, 'Success-30')};
|
|
24
|
+
color: ${({ theme }) => getGenomeColor(theme, 'Success-80')};
|
|
25
|
+
|
|
26
|
+
svg g {
|
|
27
|
+
stroke: ${({ theme }) => getGenomeColor(theme, 'Success-80')};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
&.notistack-MuiContent-error {
|
|
31
|
+
background-color: ${({ theme }) => getGenomeColor(theme, 'Error-10')};
|
|
32
|
+
border: 1px solid ${({ theme }) => getGenomeColor(theme, 'Error-50')};
|
|
33
|
+
color: ${({ theme }) => getGenomeColor(theme, 'Error-60')};
|
|
34
|
+
|
|
35
|
+
svg g {
|
|
36
|
+
stroke: ${({ theme }) => getGenomeColor(theme, 'Error-60')};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
&.notistack-MuiContent-warning {
|
|
40
|
+
background-color: ${({ theme }) => getGenomeColor(theme, 'Warning-10')};
|
|
41
|
+
border: 1px solid ${({ theme }) => getGenomeColor(theme, 'Warning-40')};
|
|
42
|
+
color: ${({ theme }) => getGenomeColor(theme, 'Warning-80')};
|
|
43
|
+
|
|
44
|
+
svg g {
|
|
45
|
+
stroke: ${({ theme }) => getGenomeColor(theme, 'Warning-80')};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
&.notistack-MuiContent-info {
|
|
49
|
+
background-color: ${({ theme }) => getGenomeColor(theme, 'Info-10')};
|
|
50
|
+
border: 1px solid ${({ theme }) => getGenomeColor(theme, 'Info-40')};
|
|
51
|
+
color: ${({ theme }) => getGenomeColor(theme, 'Info-60')};
|
|
52
|
+
|
|
53
|
+
svg g {
|
|
54
|
+
stroke: ${({ theme }) => getGenomeColor(theme, 'Info-60')};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
const NsToastProvider = (props: PropsWithChildren<SnackbarProviderProps>) => {
|
|
60
|
+
const {
|
|
61
|
+
children,
|
|
62
|
+
autoHideDuration = 5000
|
|
63
|
+
} = props;
|
|
64
|
+
return (
|
|
65
|
+
<SnackbarProvider autoHideDuration={autoHideDuration} Components={{
|
|
66
|
+
success: ToastNotification,
|
|
67
|
+
error: ToastNotification,
|
|
68
|
+
info: ToastNotification,
|
|
69
|
+
warning: ToastNotification,
|
|
70
|
+
}}>
|
|
71
|
+
{ children }
|
|
72
|
+
</SnackbarProvider>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export default NsToastProvider;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { TransitionDuration } from 'notistack';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* passes {value} to {ref}
|
|
6
|
+
*
|
|
7
|
+
* Useful if you want to expose the ref of an inner component to the public API
|
|
8
|
+
* while still using it inside the component.
|
|
9
|
+
* @param ref A ref callback or ref object. If anything falsy, this is a no-op.
|
|
10
|
+
*/
|
|
11
|
+
function setRef<T>(
|
|
12
|
+
ref:
|
|
13
|
+
| React.MutableRefObject<T | null>
|
|
14
|
+
| ((instance: T | null) => void)
|
|
15
|
+
| null
|
|
16
|
+
| undefined,
|
|
17
|
+
value: T | null
|
|
18
|
+
): void {
|
|
19
|
+
if (typeof ref === 'function') {
|
|
20
|
+
ref(value);
|
|
21
|
+
} else if (ref) {
|
|
22
|
+
ref.current = value;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function useForkRef<Instance>(
|
|
27
|
+
refA: React.Ref<Instance> | null | undefined,
|
|
28
|
+
refB: React.Ref<Instance> | null | undefined
|
|
29
|
+
): React.Ref<Instance> | null {
|
|
30
|
+
/**
|
|
31
|
+
* This will create a new function if the ref props change and are defined.
|
|
32
|
+
* This means react will call the old forkRef with `null` and the new forkRef
|
|
33
|
+
* with the ref. Cleanup naturally emerges from this behavior.
|
|
34
|
+
*/
|
|
35
|
+
return React.useMemo(() => {
|
|
36
|
+
if (refA == null && refB == null) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return (refValue) => {
|
|
40
|
+
setRef(refA, refValue);
|
|
41
|
+
setRef(refB, refValue);
|
|
42
|
+
};
|
|
43
|
+
}, [refA, refB]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface ComponentProps {
|
|
47
|
+
style?: React.CSSProperties | undefined;
|
|
48
|
+
/**
|
|
49
|
+
* number: e.g. 400
|
|
50
|
+
* TransitionDuration: e.g. { enter: 200, exit: 400 }
|
|
51
|
+
*/
|
|
52
|
+
timeout: number | TransitionDuration;
|
|
53
|
+
mode: 'enter' | 'exit';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface TransitionPropsReturnType {
|
|
57
|
+
duration: number;
|
|
58
|
+
easing: string | undefined;
|
|
59
|
+
delay: string | undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getTransitionProps(
|
|
63
|
+
props: ComponentProps
|
|
64
|
+
): TransitionPropsReturnType {
|
|
65
|
+
const { timeout, style = {}, mode } = props;
|
|
66
|
+
return {
|
|
67
|
+
duration: typeof timeout === 'object' ? timeout[mode] || 0 : timeout,
|
|
68
|
+
easing: style.transitionTimingFunction,
|
|
69
|
+
delay: style.transitionDelay,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* CSS hack to force a repaint
|
|
75
|
+
*/
|
|
76
|
+
export const reflow = (node: Element): void => {
|
|
77
|
+
// We have to do something with node.scrollTop.
|
|
78
|
+
// Otherwise it's removed from the compiled code by optimisers
|
|
79
|
+
// eslint-disable-next-line no-self-assign
|
|
80
|
+
node.scrollTop = node.scrollTop;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
interface CreateTransitionOptions {
|
|
84
|
+
duration: number;
|
|
85
|
+
easing?: string;
|
|
86
|
+
delay?: string | number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const defaultEasing = {
|
|
90
|
+
// This is the most common easing curve.
|
|
91
|
+
easeInOut: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
|
92
|
+
// Objects enter the screen at full velocity from off-screen and
|
|
93
|
+
// slowly decelerate to a resting point.
|
|
94
|
+
easeOut: 'cubic-bezier(0.0, 0, 0.2, 1)',
|
|
95
|
+
// Objects leave the screen at full velocity. They do not decelerate when off-screen.
|
|
96
|
+
easeIn: 'cubic-bezier(0.4, 0, 1, 1)',
|
|
97
|
+
// The sharp curve is used by objects that may return to the screen at any time.
|
|
98
|
+
sharp: 'cubic-bezier(0.4, 0, 0.6, 1)',
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const formatMs = (milliseconds: number) => `${Math.round(milliseconds)}ms`;
|
|
102
|
+
|
|
103
|
+
export function createTransition(
|
|
104
|
+
props: string | string[] = ['all'],
|
|
105
|
+
options?: CreateTransitionOptions
|
|
106
|
+
): string {
|
|
107
|
+
const {
|
|
108
|
+
duration = 300,
|
|
109
|
+
easing = defaultEasing.easeInOut,
|
|
110
|
+
delay = 0,
|
|
111
|
+
} = options || {};
|
|
112
|
+
|
|
113
|
+
const properties = Array.isArray(props) ? props : [props];
|
|
114
|
+
|
|
115
|
+
return properties
|
|
116
|
+
.map((animatedProp) => {
|
|
117
|
+
const formattedDuration =
|
|
118
|
+
typeof duration === 'string' ? duration : formatMs(duration);
|
|
119
|
+
const formattedDelay =
|
|
120
|
+
typeof delay === 'string' ? delay : formatMs(delay);
|
|
121
|
+
return `${animatedProp} ${formattedDuration} ${easing} ${formattedDelay}`;
|
|
122
|
+
})
|
|
123
|
+
.join(',');
|
|
124
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { VariantType, closeSnackbar } from 'notistack';
|
|
2
|
+
import { render, screen } from '@/test-utils/index';
|
|
3
|
+
import React, { PropsWithChildren, useEffect } from 'react';
|
|
4
|
+
|
|
5
|
+
import { NsToastProvider, ToastOptions, useToast } from '../';
|
|
6
|
+
|
|
7
|
+
const ToastWrapper = ({ children }: PropsWithChildren<{}>) => {
|
|
8
|
+
return (
|
|
9
|
+
<NsToastProvider>
|
|
10
|
+
{ children }
|
|
11
|
+
</NsToastProvider>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const ToastComponent = (props: {
|
|
16
|
+
variant: VariantType;
|
|
17
|
+
message?: string;
|
|
18
|
+
options?: ToastOptions;
|
|
19
|
+
}) => {
|
|
20
|
+
const { showToast } = useToast();
|
|
21
|
+
const {
|
|
22
|
+
variant,
|
|
23
|
+
message,
|
|
24
|
+
options,
|
|
25
|
+
} = props;
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
showToast(variant, message, options);
|
|
28
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
29
|
+
}, []);
|
|
30
|
+
return (
|
|
31
|
+
<div></div>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
describe('Toast', () => {
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
closeSnackbar();
|
|
38
|
+
});
|
|
39
|
+
it('should render a toast and displays the message', () => {
|
|
40
|
+
render(
|
|
41
|
+
<ToastWrapper>
|
|
42
|
+
<ToastComponent variant='success' message='Hello' />
|
|
43
|
+
</ToastWrapper>
|
|
44
|
+
);
|
|
45
|
+
expect(screen.queryByText('Hello')).toBeInTheDocument();
|
|
46
|
+
});
|
|
47
|
+
it('should render a success toast', () => {
|
|
48
|
+
const { container } = render(
|
|
49
|
+
<ToastWrapper>
|
|
50
|
+
<ToastComponent variant='success' message='Hello' />
|
|
51
|
+
</ToastWrapper>
|
|
52
|
+
);
|
|
53
|
+
expect(container.querySelector('.notistack-MuiContent-success')).toBeInTheDocument();
|
|
54
|
+
});
|
|
55
|
+
it('should render a error toast', () => {
|
|
56
|
+
const { container } = render(
|
|
57
|
+
<ToastWrapper>
|
|
58
|
+
<ToastComponent variant='error' message='Hello' />
|
|
59
|
+
</ToastWrapper>
|
|
60
|
+
);
|
|
61
|
+
expect(container.querySelector('.notistack-MuiContent-error')).toBeInTheDocument();
|
|
62
|
+
});
|
|
63
|
+
it('should render a info toast', () => {
|
|
64
|
+
const { container } = render(
|
|
65
|
+
<ToastWrapper>
|
|
66
|
+
<ToastComponent variant='info' message='Hello' />
|
|
67
|
+
</ToastWrapper>
|
|
68
|
+
);
|
|
69
|
+
expect(container.querySelector('.notistack-MuiContent-info')).toBeInTheDocument();
|
|
70
|
+
});
|
|
71
|
+
it('it should render a warning toast', () => {
|
|
72
|
+
const { container } = render(
|
|
73
|
+
<ToastWrapper>
|
|
74
|
+
<ToastComponent variant='warning' message='Hello' />
|
|
75
|
+
</ToastWrapper>
|
|
76
|
+
);
|
|
77
|
+
expect(container.querySelector('.notistack-MuiContent-warning')).toBeInTheDocument();
|
|
78
|
+
});
|
|
79
|
+
it('should render a warning toast and disappear after 3 seconds', async () => {
|
|
80
|
+
const { container } = render(
|
|
81
|
+
<ToastWrapper>
|
|
82
|
+
<ToastComponent variant='warning' message='Hello' options={{ autoHideDuration: 3000 }}/>
|
|
83
|
+
</ToastWrapper>
|
|
84
|
+
);
|
|
85
|
+
expect(container.querySelector('.notistack-MuiContent-warning')).toBeInTheDocument();
|
|
86
|
+
await new Promise((r) => setTimeout(r, 4000));
|
|
87
|
+
expect(container.querySelector('.notistack-MuiContent-warning')).not.toBeInTheDocument();
|
|
88
|
+
});
|
|
89
|
+
it('should render a action buttons', async () => {
|
|
90
|
+
|
|
91
|
+
const { getByTestId } = render(
|
|
92
|
+
<ToastWrapper>
|
|
93
|
+
<ToastComponent variant='warning' message='Hello' options={{ actions: [
|
|
94
|
+
{
|
|
95
|
+
label: 'Undo',
|
|
96
|
+
onClick: () => alert('Undo clicked'),
|
|
97
|
+
},
|
|
98
|
+
] }}/>
|
|
99
|
+
</ToastWrapper>
|
|
100
|
+
);
|
|
101
|
+
expect(getByTestId('action-button')).toBeInTheDocument();
|
|
102
|
+
});
|
|
103
|
+
it('should render close button', async () => {
|
|
104
|
+
const { getByTestId } = render(
|
|
105
|
+
<ToastWrapper>
|
|
106
|
+
<ToastComponent variant='warning' message='Hello' options={
|
|
107
|
+
{showCloseIcon: true,}}
|
|
108
|
+
/>
|
|
109
|
+
</ToastWrapper>
|
|
110
|
+
);
|
|
111
|
+
expect(getByTestId('icon')).toBeInTheDocument();
|
|
112
|
+
});
|
|
113
|
+
it('should render a action buttons and close button', async () => {
|
|
114
|
+
|
|
115
|
+
const { getAllByRole } = render(
|
|
116
|
+
<ToastWrapper>
|
|
117
|
+
<ToastComponent variant='warning' message='Hello' options={
|
|
118
|
+
{ actions: [
|
|
119
|
+
{
|
|
120
|
+
label: 'Undo',
|
|
121
|
+
onClick: () => alert('Undo clicked'),
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
showCloseIcon: true,
|
|
125
|
+
}}/>
|
|
126
|
+
</ToastWrapper>
|
|
127
|
+
);
|
|
128
|
+
expect(getAllByRole('button')?.length).toBe(2);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { VariantType } from 'notistack';
|
|
3
|
+
import type { StoryObj, Meta } from '@storybook/react';
|
|
4
|
+
import { NsToastProvider, useToast, ToastOptions } from '..';
|
|
5
|
+
|
|
6
|
+
type ComponentType = typeof NsToastProvider;
|
|
7
|
+
type Story = StoryObj<ComponentType>;
|
|
8
|
+
type MetaStory = Meta<ComponentType>;
|
|
9
|
+
|
|
10
|
+
type ToastProps = {
|
|
11
|
+
variant?: VariantType;
|
|
12
|
+
message?: string;
|
|
13
|
+
options?: ToastOptions;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const ToastDecorator = (Story: React.FC) => {
|
|
17
|
+
return (
|
|
18
|
+
<NsToastProvider>
|
|
19
|
+
<Story />
|
|
20
|
+
</NsToastProvider>
|
|
21
|
+
)
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const ToastComponent = (props: ToastProps) => {
|
|
25
|
+
const {
|
|
26
|
+
variant,
|
|
27
|
+
message = 'Toast Demo',
|
|
28
|
+
options = {
|
|
29
|
+
autoHideDuration: 2000,
|
|
30
|
+
},
|
|
31
|
+
} = props;
|
|
32
|
+
const { showToast } = useToast();
|
|
33
|
+
|
|
34
|
+
const toastHandler = () => {
|
|
35
|
+
showToast(variant, message, options);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div>
|
|
40
|
+
<button onClick={toastHandler}>
|
|
41
|
+
Show Toast
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export default {
|
|
48
|
+
title: 'Components/Feedback/NsToast',
|
|
49
|
+
component: ToastComponent,
|
|
50
|
+
decorators: [ToastDecorator],
|
|
51
|
+
argTypes: {
|
|
52
|
+
variant: { control: 'select', options: ['success', 'error', 'warning', 'info'] },
|
|
53
|
+
message: { control: 'text' },
|
|
54
|
+
options: {
|
|
55
|
+
autoHideDuration: { control: 'number' },
|
|
56
|
+
persist: { control: 'boolean' },
|
|
57
|
+
className: { control: 'text' },
|
|
58
|
+
showCloseIcon: { control: 'boolean' },
|
|
59
|
+
actions: {
|
|
60
|
+
description: 'The buttons should be passed to the Toast element',
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
args: {
|
|
65
|
+
variant: 'error',
|
|
66
|
+
message: 'Toast component',
|
|
67
|
+
options: {
|
|
68
|
+
autoHideDuration: 2000,
|
|
69
|
+
persist: false,
|
|
70
|
+
showCloseIcon: false,
|
|
71
|
+
actions: [
|
|
72
|
+
{
|
|
73
|
+
label: 'Undo',
|
|
74
|
+
onClick: () => alert('Undo clicked'),
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
} as MetaStory;
|
|
80
|
+
|
|
81
|
+
export const NsToast: Story = {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { JSX } from 'react';
|
|
2
|
+
import { closeSnackbar, enqueueSnackbar, VariantType } from 'notistack';
|
|
3
|
+
import Fade from './Fade';
|
|
4
|
+
import NsIcon from '../icon/NsIcon';
|
|
5
|
+
import NsButton from '../button/NsButton';
|
|
6
|
+
|
|
7
|
+
type ToastActionLink = {
|
|
8
|
+
label: string;
|
|
9
|
+
onClick?: () => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type ToastOptions = {
|
|
13
|
+
className?: string;
|
|
14
|
+
autoHideDuration?: number | null;
|
|
15
|
+
persist?: boolean;
|
|
16
|
+
showCloseIcon?: boolean;
|
|
17
|
+
actions?: ToastActionLink[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
const useToast = () => {
|
|
22
|
+
const closeToast = (toastId?: string) => {
|
|
23
|
+
closeSnackbar(toastId);
|
|
24
|
+
};
|
|
25
|
+
const generateActionButtons = (showCloseIcon: boolean = false, variant: VariantType = 'success', actions?: ToastActionLink[]) => {
|
|
26
|
+
let actionButtons: JSX.Element[] = [];
|
|
27
|
+
if (actions?.length) {
|
|
28
|
+
actionButtons = actions.map((action) => (
|
|
29
|
+
<NsButton data-testid="action-button" variant="text" color={variant as any} key={action?.label} onClick={() => action.onClick?.()}>
|
|
30
|
+
{action.label}
|
|
31
|
+
</NsButton>
|
|
32
|
+
));
|
|
33
|
+
}
|
|
34
|
+
if (showCloseIcon) {
|
|
35
|
+
actionButtons.push (
|
|
36
|
+
<NsButton variant="text" onClick={() => closeToast()}>
|
|
37
|
+
<NsIcon name="close" size="small" cursor="pointer" />
|
|
38
|
+
</NsButton>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return actionButtons?.length ? actionButtons : null;
|
|
42
|
+
};
|
|
43
|
+
const showToast = (variant: VariantType = 'default', message?: string, options?: ToastOptions) => {
|
|
44
|
+
try {
|
|
45
|
+
return enqueueSnackbar(message, {
|
|
46
|
+
variant,
|
|
47
|
+
...options,
|
|
48
|
+
anchorOrigin: { horizontal: 'center', vertical: 'bottom' },
|
|
49
|
+
preventDuplicate: true,
|
|
50
|
+
TransitionComponent: Fade,
|
|
51
|
+
action: generateActionButtons(options?.showCloseIcon, variant, options?.actions),
|
|
52
|
+
});
|
|
53
|
+
} catch (err) {
|
|
54
|
+
console.error('CFT::UI:COMPONENTS showToast should be used inside NsToastProvider');
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
showToast,
|
|
60
|
+
closeToast
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export default useToast;
|