Skip to content

Commit f8481ac

Browse files
committed
refactor(toast): change toast content props "string" to "react-noe"
- add toast controller test - add toast content type
1 parent 65d3f87 commit f8481ac

File tree

10 files changed

+229
-39
lines changed

10 files changed

+229
-39
lines changed

packages/bezier-react/src/components/Toast/Toast.stories.tsx

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@ import { Story, Meta } from '@storybook/react'
77
/* Internal depependencies */
88
import { styled } from 'Foundation'
99
import { iconList, getTitle } from 'Utils/storyUtils'
10+
import { ProgressBar } from 'Components/ProgressBar'
11+
import { StackItem, VStack } from 'Components/Stack'
12+
import { Button, ButtonColorVariant, ButtonStyleVariant } from 'Components/Button'
1013
import useToast from './useToast'
1114
import ToastProvider from './ToastProvider'
1215
import ToastElement from './ToastElement'
13-
import ToastProps, { ToastAppearance, ToastPreset } from './Toast.types'
16+
import ToastProps, { ToastAppearance, ToastOptions, ToastPreset } from './Toast.types'
1417

1518
export default {
1619
title: getTitle(base),
@@ -206,3 +209,53 @@ export const WithZIndex: Story<ToastProps> = () => (
206209
</ToastProvider>
207210
</Container>
208211
)
212+
213+
function CustomContentToastController() {
214+
const toast = useToast()
215+
216+
const onClickCustomButtonInToast = useCallback(() => {
217+
toast.removeAllToasts()
218+
}, [toast])
219+
220+
const handleClick = useCallback((option?: ToastOptions) => {
221+
toast.addToast((
222+
<VStack spacing={6} align="stretch">
223+
<StackItem>
224+
<Button
225+
text="눌러주세요. 모든 토스트가 사라집니다."
226+
styleVariant={ButtonStyleVariant.Primary}
227+
colorVariant={ButtonColorVariant.Blue}
228+
onClick={onClickCustomButtonInToast}
229+
/>
230+
</StackItem>
231+
<StackItem>
232+
<ProgressBar
233+
width="100%"
234+
value={Math.random()}
235+
/>
236+
</StackItem>
237+
</VStack>
238+
), {
239+
preset: ToastPreset.Default,
240+
...option,
241+
})
242+
}, [
243+
toast,
244+
onClickCustomButtonInToast,
245+
])
246+
247+
return (
248+
<div>
249+
<button type="button" onClick={() => handleClick()}>default</button>
250+
<button type="button" onClick={() => handleClick({ autoDismiss: false })}>never dismiss</button>
251+
</div>
252+
)
253+
}
254+
255+
export const CustomContent: Story<ToastProps> = () => (
256+
<Container id="story-wrapper">
257+
<ToastProvider>
258+
<CustomContentToastController />
259+
</ToastProvider>
260+
</Container>
261+
)

packages/bezier-react/src/components/Toast/Toast.styled.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,16 @@ const getEllipsisColor = (
9595
)
9696

9797
export const Content = styled.div<Pick<ToastElementProps, 'actionContent' | 'onClick'>>`
98+
display: flex;
99+
flex: 1;
98100
margin: 3px 6px;
99101
overflow: hidden;
100102
color: ${({ actionContent, onClick, foundation }) => getEllipsisColor(actionContent, onClick, foundation)};
101103
`
102104

103105
export const EllipsisableContent = styled.div`
106+
display: flex;
107+
flex: 1;
104108
${ellipsis(5, LineHeightAbsoluteNumber.Lh18)};
105109
106110
overflow: visible;

packages/bezier-react/src/components/Toast/Toast.test.tsx

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,7 @@ describe('Toast test >', () => {
2222
}
2323
})
2424

25-
const renderToast = (optionProps?: Omit<ToastElementProps,
26-
| 'content'
27-
| 'onDismiss'
28-
| 'transitionDuration'
29-
| 'transform'
30-
| 'placement'
31-
>) => render(<ToastElement {...props} {...optionProps} />)
25+
const renderToast = (optionProps?: Partial<ToastElementProps>) => render(<ToastElement {...props} {...optionProps} />)
3226

3327
it('Reset CSS', () => {
3428
const { getByTestId } = renderToast()
@@ -105,6 +99,56 @@ describe('Toast test >', () => {
10599
})
106100
})
107101

102+
describe('Props - onDismiss', () => {
103+
it('onDismiss is called when click Close button,', () => {
104+
const onDismiss = jest.fn()
105+
const { getByTestId } = renderToast({
106+
onDismiss,
107+
})
108+
const closeToast = getByTestId(`${TOAST_TEST_ID}-close`)
109+
closeToast.click()
110+
expect(onDismiss).toBeCalledTimes(1)
111+
})
112+
})
113+
114+
describe('Props - content', () => {
115+
it('content is string', () => {
116+
const onDismiss = jest.fn()
117+
const { getByTestId } = renderToast({
118+
onDismiss,
119+
content: 'Hello',
120+
})
121+
const content = getByTestId(`${TOAST_TEST_ID}-content`)
122+
expect(content).toBeInTheDocument()
123+
expect(content.childNodes.length).toBe(1)
124+
})
125+
126+
it('content is string with \n', () => {
127+
const onDismiss = jest.fn()
128+
const { getByTestId } = renderToast({
129+
onDismiss,
130+
content: 'Hello\nChannelTalk',
131+
})
132+
const content = getByTestId(`${TOAST_TEST_ID}-content`)
133+
expect(content.childNodes.length).toBe(2)
134+
})
135+
136+
it('content is ReactNode', () => {
137+
const onDismiss = jest.fn()
138+
const { getByTestId } = renderToast({
139+
onDismiss,
140+
content: (
141+
<button type="button">
142+
Hello
143+
</button>
144+
),
145+
})
146+
const content = getByTestId(`${TOAST_TEST_ID}-content`)
147+
expect(content).toBeInTheDocument()
148+
expect(content.childNodes.length).toBe(1)
149+
})
150+
})
151+
108152
it('z-index Test > ', () => {
109153
const { getByTestId } = renderToast({ zIndex: 317 })
110154
const toast = getByTestId(TOAST_TEST_ID)

packages/bezier-react/src/components/Toast/Toast.types.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* External dependencies */
2-
import { ReactNode, ComponentType } from 'react'
2+
import React, { ReactNode, ComponentType } from 'react'
33
import { noop } from 'lodash-es'
44

55
/* Internal dependencies */
@@ -44,18 +44,23 @@ interface ToastElementOptions {
4444
preset?: ToastPreset
4545
appearance?: ToastAppearance
4646
iconName?: IconName
47+
/**
48+
* @deprecated use React.ReactNode content props instead.
49+
*/
4750
actionContent?: string
4851
transitionDuration: TransitionDuration
4952
transform: InjectedInterpolation
5053
placement: ToastPlacement
5154
zIndex?: number
5255
onClick?: React.MouseEventHandler
53-
onDismiss: React.MouseEventHandler
56+
onDismiss: React.MouseEventHandler<HTMLDivElement>
5457
}
5558

59+
export type ToastContent = React.ReactNode
60+
5661
export default interface ToastElementProps extends
5762
BezierComponentProps,
58-
Required<ContentProps<string>>,
63+
Required<ContentProps<ToastContent>>,
5964
ToastElementOptions {}
6065

6166
export interface ToastProviderProps {
@@ -87,10 +92,10 @@ export const defaultOptions: ToastOptions = {
8792
rightSide: false,
8893
}
8994

90-
export type ToastType = ToastOptions & { id: ToastId, content: string }
95+
export type ToastType = ToastOptions & { id: ToastId, content: ToastContent }
9196

9297
export interface ToastContextType {
93-
add: (content: string, options: ToastOptions) => ToastId
98+
add: (content: ToastContent, options: ToastOptions) => ToastId
9499
remove: (id: ToastId) => void
95100
removeAll: () => void
96101
leftToasts: ToastType[]
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/* External dependencies */
2+
import React from 'react'
3+
import _ from 'lodash-es'
4+
import { act } from '@testing-library/react'
5+
6+
/* Internal dependencies */
7+
import { css, TransitionDuration } from 'Foundation'
8+
import { render } from 'Utils/testUtils'
9+
import { ToastControllerProps, ToastPlacement } from './Toast.types'
10+
import ToastElement from './ToastElement'
11+
import ToastController from './ToastController'
12+
13+
describe('ToastController >', () => {
14+
let props: ToastControllerProps
15+
16+
beforeEach(() => {
17+
props = {
18+
autoDismiss: true,
19+
autoDismissTimeout: 5000,
20+
transitionDuration: TransitionDuration.M,
21+
content: 'Test Toast',
22+
onDismiss: _.noop,
23+
transform: css``,
24+
placement: ToastPlacement.BottomLeft,
25+
component: ToastElement,
26+
}
27+
})
28+
29+
const renderToastController = (optionProps?: Partial<ToastControllerProps>) => render(
30+
<ToastController {...props} {...optionProps} />,
31+
)
32+
33+
describe('Props >', () => {
34+
describe('autoDismiss', () => {
35+
beforeEach(() => {
36+
jest.useFakeTimers()
37+
jest.spyOn(global, 'setTimeout')
38+
})
39+
40+
it('autoDismiss is on', () => {
41+
const autoDismissTimeout = 5000
42+
renderToastController({
43+
autoDismissTimeout,
44+
})
45+
expect(setTimeout).toHaveBeenCalledTimes(1)
46+
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), TransitionDuration.M)
47+
act(() => {
48+
jest.runOnlyPendingTimers()
49+
})
50+
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), autoDismissTimeout)
51+
})
52+
53+
it('autoDismiss is off', () => {
54+
renderToastController({
55+
autoDismiss: false,
56+
})
57+
expect(setTimeout).toHaveBeenCalledTimes(0)
58+
})
59+
})
60+
61+
describe('onDismiss >', () => {
62+
beforeEach(() => {
63+
jest.useFakeTimers()
64+
jest.spyOn(global, 'setTimeout')
65+
})
66+
67+
it('onDismiss is called automatically', () => {
68+
const onDismiss = jest.fn()
69+
renderToastController({
70+
onDismiss,
71+
})
72+
expect(onDismiss).not.toBeCalled()
73+
act(() => {
74+
jest.runAllTimers()
75+
})
76+
expect(onDismiss).toBeCalledTimes(1)
77+
})
78+
})
79+
})
80+
})

packages/bezier-react/src/components/Toast/ToastController.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ function ToastController({
1818
const [transform, setTransform] = useState(showedToastTranslateXStyle)
1919
const timer = useRef<ReturnType<Window['setTimeout']>>()
2020

21-
const handleDismiss = useCallback(() => {
21+
const handleDismiss = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
2222
setTransform(initPosition(placement))
23-
timer.current = window.setTimeout(onDismiss, transitionDuration)
23+
timer.current = window.setTimeout(() => {
24+
onDismiss(e)
25+
}, transitionDuration)
2426
}, [
2527
onDismiss,
2628
placement,

packages/bezier-react/src/components/Toast/ToastElement.tsx

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* External dependencies */
2-
import React, { Fragment, forwardRef, Ref, useMemo } from 'react'
2+
import React, { forwardRef, Ref, useMemo } from 'react'
33
import { v4 as uuid } from 'uuid'
4+
import { isString } from 'lodash-es'
45

56
/* Internal dependencies */
67
import { Typography } from 'Foundation'
@@ -27,28 +28,20 @@ const ToastElement = (
2728
}: ToastProps,
2829
forwardedRef: Ref<any>,
2930
) => {
30-
const newLineComponent = useMemo(() => (
31-
content.split('\n').map((str, index) => {
32-
if (index === 0) {
33-
return (
34-
<Text key={uuid()} typo={Typography.Size14}>
35-
{ str }
36-
</Text>
37-
)
38-
}
39-
40-
return (
41-
<Fragment key={uuid()}>
42-
<br />
31+
const ToastContentComponent = useMemo(() => {
32+
if (isString(content)) {
33+
return content.split('\n').map((str) => (
34+
<div key={uuid()}>
4335
<Text
4436
typo={Typography.Size14}
4537
>
4638
{ str }
4739
</Text>
48-
</Fragment>
49-
)
50-
})
51-
), [content])
40+
</div>
41+
))
42+
}
43+
return content
44+
}, [content])
5245

5346
const {
5447
appearance: presetAppearance,
@@ -80,8 +73,10 @@ const ToastElement = (
8073
height: '18px',
8174
}}
8275
>
83-
<NormalContent>
84-
{ newLineComponent }
76+
<NormalContent
77+
data-testid={`${TOAST_TEST_ID}-content`}
78+
>
79+
{ ToastContentComponent }
8580
</NormalContent>
8681
{ ' ' }
8782
{ actionContent && onClick && (
@@ -92,7 +87,10 @@ const ToastElement = (
9287
</Text>
9388
</EllipsisableContent>
9489
</Content>
95-
<Close onClick={onDismiss}>
90+
<Close
91+
onClick={onDismiss}
92+
data-testid={`${TOAST_TEST_ID}-close`}
93+
>
9694
<Icon
9795
source={CancelIcon}
9896
size={IconSize.XS}

packages/bezier-react/src/components/Toast/ToastProvider.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
ToastId,
1616
ToastProviderProps,
1717
ToastType,
18+
ToastContent,
1819
} from './Toast.types'
1920
import ToastContainer from './ToastContainer'
2021
import ToastController from './ToastController'
@@ -32,7 +33,7 @@ function ToastProvider({
3233
const [leftToasts, setLeftToasts] = useState<ToastType[]>([])
3334
const [rightToasts, setRightToasts] = useState<ToastType[]>([])
3435

35-
const add = useCallback((content: string, options: ToastOptions = defaultOptions) => {
36+
const add = useCallback((content: ToastContent, options: ToastOptions = defaultOptions) => {
3637
let result = ''
3738
if (options.rightSide) {
3839
result = rightToastService.add(content, options)
@@ -123,7 +124,7 @@ function ToastProvider({
123124
autoDismissTimeout={autoDismissTimeout}
124125
preset={preset}
125126
appearance={appearance}
126-
content={content}
127+
content={content || ''}
127128
iconName={iconName}
128129
component={ToastElement}
129130
onDismiss={() => handleDismiss(id, onDismiss)}

0 commit comments

Comments
 (0)